refactor: file handle manager

This commit is contained in:
rhunk
2024-05-28 23:41:52 +02:00
parent 84fc7c6ee3
commit c31e6fd0c2
25 changed files with 329 additions and 303 deletions

View File

@@ -10,30 +10,18 @@ import me.rhunk.snapenhance.bridge.logger.TrackerInterface;
import me.rhunk.snapenhance.bridge.ConfigStateListener;
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge;
import me.rhunk.snapenhance.bridge.AccountStorage;
import me.rhunk.snapenhance.bridge.storage.FileHandleManager;
interface BridgeInterface {
/**
* broadcast a log message
*/
oneway void broadcastLog(String tag, String level, String message);
/**
* Execute a file operation
* @param fileType the corresponding file type (see BridgeFileType)
*/
byte[] fileOperation(int action, int fileType, in @nullable byte[] content);
/**
* Get the application APK path (assets for the conversation exporter)
*/
String getApplicationApkPath();
/**
* Fetch the locales
*
* @return the map of locales (key: locale short name, value: locale data as json)
*/
Map<String, ParcelFileDescriptor> fetchLocales(String userLocale);
* broadcast a log message
*/
oneway void broadcastLog(String tag, String level, String message);
/**
* Enqueue a download
@@ -92,6 +80,8 @@ interface BridgeInterface {
AccountStorage getAccountStorage();
FileHandleManager getFileHandleManager();
oneway void registerMessagingBridge(MessagingBridge bridge);
oneway void openSettingsOverlay();

View File

@@ -0,0 +1,9 @@
package me.rhunk.snapenhance.bridge.storage;
interface FileHandle {
boolean exists();
boolean create();
boolean delete();
@nullable ParcelFileDescriptor open(int mode);
}

View File

@@ -0,0 +1,7 @@
package me.rhunk.snapenhance.bridge.storage;
import me.rhunk.snapenhance.bridge.storage.FileHandle;
interface FileHandleManager {
@nullable FileHandle getFileHandle(String scope, String name);
}

View File

@@ -0,0 +1,94 @@
package me.rhunk.snapenhance.common.bridge
import android.content.Context
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.AutoCloseInputStream
import android.os.ParcelFileDescriptor.AutoCloseOutputStream
import me.rhunk.snapenhance.bridge.storage.FileHandle
import java.io.File
enum class FileHandleScope(
val key: String
) {
INTERNAL("internal"),
LOCALE("locale");
companion object {
fun fromValue(name: String): FileHandleScope? = entries.find { it.key == name }
}
}
enum class InternalFileHandleType(
val key: String,
val fileName: String,
val isDatabase: Boolean = false
) {
CONFIG("config", "config.json"),
MAPPINGS("mappings", "mappings.json"),
MESSAGE_LOGGER("message_logger", "message_logger.db", isDatabase = true),
SUSPEND_LOCATION_STATE("suspend_location_state", "suspend_location_state.txt"),
PINNED_BEST_FRIEND("pinned_best_friend", "pinned_best_friend.txt");
fun resolve(context: Context): File = if (isDatabase) {
context.getDatabasePath(fileName)
} else {
File(context.filesDir, fileName)
}
companion object {
fun fromValue(name: String): InternalFileHandleType? = entries.find { it.key == name }
}
}
fun FileHandle.toWrapper() = FileHandleWrapper(lazy { this })
open class FileHandleWrapper(
private val fileHandle: Lazy<FileHandle>
) {
fun exists() = fileHandle.value.exists()
fun create() = fileHandle.value.create()
fun delete() = fileHandle.value.delete()
fun writeBytes(data: ByteArray) = fileHandle.value.open(
ParcelFileDescriptor.MODE_WRITE_ONLY or
ParcelFileDescriptor.MODE_CREATE or
ParcelFileDescriptor.MODE_TRUNCATE
).use { pfd ->
AutoCloseOutputStream(pfd).use {
it.write(data)
}
}
open fun readBytes(): ByteArray = fileHandle.value.open(
ParcelFileDescriptor.MODE_READ_ONLY or
ParcelFileDescriptor.MODE_CREATE
).use { pfd ->
AutoCloseInputStream(pfd).use {
it.readBytes()
}
}
fun inputStream(block: (AutoCloseInputStream) -> Unit) = fileHandle.value.open(
ParcelFileDescriptor.MODE_READ_ONLY or
ParcelFileDescriptor.MODE_CREATE
).use { pfd ->
AutoCloseInputStream(pfd).use {
block(it)
}
}
fun outputStream(block: (AutoCloseOutputStream) -> Unit) = fileHandle.value.open(
ParcelFileDescriptor.MODE_WRITE_ONLY or
ParcelFileDescriptor.MODE_CREATE or
ParcelFileDescriptor.MODE_TRUNCATE
).use { pfd ->
AutoCloseOutputStream(pfd).use {
block(it)
}
}
}

View File

@@ -1,29 +0,0 @@
package me.rhunk.snapenhance.common.bridge
import android.content.Context
import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
open class FileLoaderWrapper(
val fileType: BridgeFileType,
val defaultContent: ByteArray
) {
lateinit var isFileExists: () -> Boolean
lateinit var write: (ByteArray) -> Unit
lateinit var read: () -> ByteArray
lateinit var delete: () -> Unit
fun loadFromContext(context: Context) {
val file = fileType.resolve(context)
isFileExists = { file.exists() }
read = {
if (!file.exists()) {
file.createNewFile()
file.writeBytes("{}".toByteArray(Charsets.UTF_8))
}
file.readBytes()
}
write = { file.writeBytes(it) }
delete = { file.delete() }
}
}

View File

@@ -0,0 +1,17 @@
package me.rhunk.snapenhance.common.bridge
import me.rhunk.snapenhance.bridge.storage.FileHandleManager
open class InternalFileWrapper(
fileHandleManager: FileHandleManager,
private val fileType: InternalFileHandleType,
val defaultValue: String? = null
): FileHandleWrapper(lazy { fileHandleManager.getFileHandle(FileHandleScope.INTERNAL.key, fileType.key)!! }) {
override fun readBytes(): ByteArray {
val bytes = super.readBytes()
if (bytes.isEmpty()) {
defaultValue?.let { writeBytes(it.toByteArray()) }
}
return super.readBytes()
}
}

View File

@@ -1,26 +0,0 @@
package me.rhunk.snapenhance.common.bridge.types
import android.content.Context
import java.io.File
enum class BridgeFileType(val value: Int, val fileName: String, val displayName: String, private val isDatabase: Boolean = false) {
CONFIG(0, "config.json", "Config"),
MAPPINGS(1, "mappings.json", "Mappings"),
MESSAGE_LOGGER_DATABASE(2, "message_logger.db", "Message Logger",true),
PINNED_CONVERSATIONS(3, "pinned_conversations.txt", "Pinned Conversations"),
SUSPEND_LOCATION_STATE(4, "suspend_location_state.txt", "Suspend Location State"),
PINNED_BEST_FRIEND(5, "pinned_best_friend.txt", "Pinned Best Friend");
fun resolve(context: Context): File = if (isDatabase) {
context.getDatabasePath(fileName)
} else {
File(context.filesDir, fileName)
}
companion object {
fun fromValue(value: Int): BridgeFileType? {
return entries.firstOrNull { it.value == value }
}
}
}

View File

@@ -1,5 +0,0 @@
package me.rhunk.snapenhance.common.bridge.types
enum class FileActionType {
CREATE_AND_READ, READ, WRITE, DELETE, EXISTS
}

View File

@@ -1,17 +0,0 @@
package me.rhunk.snapenhance.common.bridge.types
import android.os.ParcelFileDescriptor
import java.util.Locale
data class LocalePair(
val locale: String,
val content: ParcelFileDescriptor
) {
fun getLocale(): Locale {
if (locale.contains("_")) {
val split = locale.split("_")
return Locale(split[0], split[1])
}
return Locale(locale)
}
}

View File

@@ -1,41 +1,22 @@
package me.rhunk.snapenhance.common.bridge.wrapper
import android.content.Context
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.AutoCloseInputStream
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import me.rhunk.snapenhance.common.bridge.types.LocalePair
import me.rhunk.snapenhance.bridge.storage.FileHandleManager
import me.rhunk.snapenhance.common.bridge.FileHandleScope
import me.rhunk.snapenhance.common.logger.AbstractLogger
import me.rhunk.snapenhance.common.util.ktx.toParcelFileDescriptor
import java.util.Locale
class LocaleWrapper {
class LocaleWrapper(
private val fileHandleManager: FileHandleManager
) {
companion object {
const val DEFAULT_LOCALE = "en_US"
fun fetchLocales(context: Context, locale: String = DEFAULT_LOCALE): List<LocalePair> {
val coroutineScope = CoroutineScope(Dispatchers.IO)
val locales = mutableListOf<LocalePair>().apply {
add(LocalePair(DEFAULT_LOCALE, context.resources.assets.open("lang/$DEFAULT_LOCALE.json").toParcelFileDescriptor(coroutineScope)))
}
if (locale == DEFAULT_LOCALE) return locales
val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(locale) }?.substringBefore(".") ?: return locales
locales.add(
LocalePair(
compatibleLocale,
context.resources.assets.open("lang/$compatibleLocale.json").toParcelFileDescriptor(coroutineScope)
)
)
return locales
}
fun fetchAvailableLocales(context: Context): List<String> {
return context.resources.assets.list("lang")?.map { it.substringBefore(".") }?.sorted() ?: listOf(DEFAULT_LOCALE)
}
@@ -47,14 +28,23 @@ class LocaleWrapper {
lateinit var loadedLocale: Locale
private fun load(localePair: LocalePair) {
loadedLocale = localePair.getLocale()
private fun load(locale: String, pfd: ParcelFileDescriptor) {
loadedLocale = if (locale.contains("_")) {
val split = locale.split("_")
Locale(split[0], split[1])
} else {
Locale(locale)
}
val translations = AutoCloseInputStream(localePair.content).use {
JsonParser.parseReader(it.reader()).asJsonObject
val translations = AutoCloseInputStream(pfd).use {
runCatching {
JsonParser.parseReader(it.reader()).asJsonObject
}.onFailure {
AbstractLogger.directError("Failed to parse locale file: ${it.message}", it)
}.getOrNull()
}
if (translations == null || translations.isJsonNull) {
return
throw IllegalStateException("Failed to parse $locale.json")
}
fun scanObject(jsonObject: JsonObject, prefix: String = "") {
@@ -71,22 +61,25 @@ class LocaleWrapper {
scanObject(translations)
}
fun loadFromCallback(callback: (String) -> List<LocalePair>) {
callback(userLocale).forEach {
load(it)
fun load() {
load(
DEFAULT_LOCALE,
fileHandleManager.getFileHandle(FileHandleScope.LOCALE.key, "$DEFAULT_LOCALE.json")?.open(ParcelFileDescriptor.MODE_READ_ONLY) ?: run {
throw IllegalStateException("Failed to load default locale")
}
)
if (userLocale != DEFAULT_LOCALE) {
fileHandleManager.getFileHandle(FileHandleScope.LOCALE.key, "$userLocale.json")?.open(ParcelFileDescriptor.MODE_READ_ONLY)?.let {
load(userLocale, it)
}
}
}
fun loadFromContext(context: Context) {
fetchLocales(context, userLocale).forEach {
load(it)
}
}
fun reloadFromContext(context: Context, locale: String) {
fun reload(locale: String) {
userLocale = locale
translationMap.clear()
loadFromContext(context)
load()
}
operator fun get(key: String) = translationMap[key] ?: key.also { AbstractLogger.directDebug("Missing translation for $key") }
@@ -99,7 +92,7 @@ class LocaleWrapper {
}
fun getCategory(key: String): LocaleWrapper {
return LocaleWrapper().apply {
return LocaleWrapper(fileHandleManager).apply {
translationMap.putAll(
this@LocaleWrapper.translationMap
.filterKeys { it.startsWith("$key.") }

View File

@@ -1,11 +1,13 @@
package me.rhunk.snapenhance.common.bridge.wrapper
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import kotlinx.coroutines.*
import me.rhunk.snapenhance.bridge.logger.LoggerInterface
import me.rhunk.snapenhance.common.bridge.InternalFileHandleType
import me.rhunk.snapenhance.common.data.StoryData
import me.rhunk.snapenhance.common.logger.AbstractLogger
import me.rhunk.snapenhance.common.util.SQLiteDatabaseHelper
@@ -61,6 +63,8 @@ class TrackerLog(
class LoggerWrapper(
val databaseFile: File
): LoggerInterface.Stub() {
constructor(context: Context): this(File(context.getDatabasePath(InternalFileHandleType.MESSAGE_LOGGER.fileName).absolutePath))
private var _database: SQLiteDatabase? = null
@OptIn(ExperimentalCoroutinesApi::class)
private val coroutineScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1))

View File

@@ -3,16 +3,19 @@ package me.rhunk.snapenhance.common.bridge.wrapper
import android.content.Context
import com.google.gson.JsonParser
import kotlinx.coroutines.runBlocking
import me.rhunk.snapenhance.bridge.storage.FileHandleManager
import me.rhunk.snapenhance.common.BuildConfig
import me.rhunk.snapenhance.common.Constants
import me.rhunk.snapenhance.common.bridge.FileLoaderWrapper
import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
import me.rhunk.snapenhance.common.bridge.InternalFileHandleType
import me.rhunk.snapenhance.common.bridge.InternalFileWrapper
import me.rhunk.snapenhance.common.logger.AbstractLogger
import me.rhunk.snapenhance.mapper.AbstractClassMapper
import me.rhunk.snapenhance.mapper.ClassMapper
import kotlin.reflect.KClass
class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteArray(Charsets.UTF_8)) {
class MappingsWrapper(
fileHandleManager: FileHandleManager
): InternalFileWrapper(fileHandleManager, InternalFileHandleType.MAPPINGS, defaultValue = "{}") {
private lateinit var context: Context
private var mappingUniqueHash: Long = 0
var isMappingsLoaded = false
@@ -26,7 +29,7 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr
this.context = context
mappingUniqueHash = getUniqueBuildId()
if (isFileExists()) {
if (exists()) {
runCatching {
loadCached()
}.onFailure {
@@ -46,10 +49,10 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr
fun isMappingsOutdated() = mappingUniqueHash != getUniqueBuildId() || isMappingsLoaded.not()
private fun loadCached() {
if (!isFileExists()) {
if (!exists()) {
throw Exception("Mappings file does not exist")
}
val mappingsObject = JsonParser.parseString(read().toString(Charsets.UTF_8)).asJsonObject.also {
val mappingsObject = JsonParser.parseString(readBytes().toString(Charsets.UTF_8)).asJsonObject.also {
mappingUniqueHash = it["unique_hash"].asLong
}
@@ -76,7 +79,7 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr
val result = classMapper.run().apply {
addProperty("unique_hash", mappingUniqueHash)
}
write(result.toString().toByteArray())
writeBytes(result.toString().toByteArray())
}
}

View File

@@ -5,20 +5,22 @@ import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import me.rhunk.snapenhance.bridge.ConfigStateListener
import me.rhunk.snapenhance.common.bridge.FileLoaderWrapper
import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
import me.rhunk.snapenhance.bridge.storage.FileHandleManager
import me.rhunk.snapenhance.common.bridge.InternalFileHandleType
import me.rhunk.snapenhance.common.bridge.InternalFileWrapper
import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.common.config.impl.RootConfig
import me.rhunk.snapenhance.common.logger.AbstractLogger
import kotlin.properties.Delegates
class ModConfig(
private val context: Context
private val context: Context,
fileHandleManager: FileHandleManager
) {
private val fileWrapper = InternalFileWrapper(fileHandleManager, InternalFileHandleType.CONFIG, "{}")
var locale: String = LocaleWrapper.DEFAULT_LOCALE
private val gson: Gson = GsonBuilder().setPrettyPrinting().create()
private val file = FileLoaderWrapper(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8))
var wasPresent by Delegates.notNull<Boolean>()
/* Used to notify the bridge client about config changes */
@@ -30,9 +32,9 @@ class ModConfig(
private fun createRootConfig() = RootConfig().apply { lateInit(context) }
private fun load() {
fun load() {
root = createRootConfig()
wasPresent = file.isFileExists()
wasPresent = fileWrapper.exists()
if (!wasPresent) {
writeConfig()
return
@@ -46,7 +48,7 @@ class ModConfig(
}
private fun loadConfig() {
val configFileContent = file.read()
val configFileContent = fileWrapper.readBytes()
val configObject = gson.fromJson(configFileContent.toString(Charsets.UTF_8), JsonObject::class.java)
locale = configObject.get("_locale")?.asString ?: LocaleWrapper.DEFAULT_LOCALE
root.fromJson(configObject)
@@ -99,8 +101,8 @@ class ModConfig(
}
}
val oldConfig = runCatching { file.read().toString(Charsets.UTF_8) }.getOrNull()
file.write(exportToString().toByteArray(Charsets.UTF_8))
val oldConfig = runCatching { fileWrapper.readBytes().toString(Charsets.UTF_8) }.getOrNull()
fileWrapper.writeBytes(exportToString().toByteArray(Charsets.UTF_8))
configStateListener?.also {
runCatching {
@@ -125,14 +127,4 @@ class ModConfig(
root.fromJson(configObject)
writeConfig()
}
fun loadFromContext(context: Context) {
file.loadFromContext(context)
load()
}
fun loadFromCallback(callback: (FileLoaderWrapper) -> Unit) {
callback(file)
load()
}
}