commit c31e6fd0c2e16546137d596d71ea9d2d960a993b
parent 84fc7c6ee344860cb9a4f5bfe81d2afaff05995d
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Tue, 28 May 2024 23:41:52 +0200

refactor: file handle manager

Diffstat:
Aapp/src/main/kotlin/me/rhunk/snapenhance/RemoteFileHandleManager.kt | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt | 15+++++++--------
Mapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 52++++------------------------------------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt | 4+---
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt | 12+++++-------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt | 3+--
Mcommon/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl | 22++++++----------------
Acommon/src/main/aidl/me/rhunk/snapenhance/bridge/storage/FileHandle.aidl | 10++++++++++
Acommon/src/main/aidl/me/rhunk/snapenhance/bridge/storage/FileHandleManager.aidl | 8++++++++
Acommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/BridgeFiles.kt | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/FileLoaderWrapper.kt | 30------------------------------
Acommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/InternalFileWrapper.kt | 18++++++++++++++++++
Dcommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/BridgeFileType.kt | 27---------------------------
Dcommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/FileActionType.kt | 6------
Dcommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/LocalePair.kt | 18------------------
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LocaleWrapper.kt | 75++++++++++++++++++++++++++++++++++-----------------------------------------
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt | 4++++
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt | 17++++++++++-------
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/ModConfig.kt | 30+++++++++++-------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt | 13+++++--------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 6+-----
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt | 48+++---------------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/BridgeFileFeature.kt | 30++++++++++++++++++------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BestFriendPinning.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt | 4++--
25 files changed, 332 insertions(+), 306 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteFileHandleManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteFileHandleManager.kt @@ -0,0 +1,87 @@ +package me.rhunk.snapenhance + +import android.os.ParcelFileDescriptor +import me.rhunk.snapenhance.bridge.storage.FileHandle +import me.rhunk.snapenhance.bridge.storage.FileHandleManager +import me.rhunk.snapenhance.common.bridge.FileHandleScope +import me.rhunk.snapenhance.common.bridge.InternalFileHandleType +import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper +import me.rhunk.snapenhance.common.logger.AbstractLogger +import me.rhunk.snapenhance.common.util.ktx.toParcelFileDescriptor +import java.io.File + + +class LocalFileHandle( + private val file: File +): FileHandle.Stub() { + override fun exists() = file.exists() + override fun create() = file.createNewFile() + override fun delete() = file.delete() + + override fun open(mode: Int): ParcelFileDescriptor? { + return runCatching { + ParcelFileDescriptor.open(file, mode) + }.onFailure { + AbstractLogger.directError("Failed to open file handle: ${it.message}", it) + }.getOrNull() + } +} + +class AssetFileHandle( + private val context: RemoteSideContext, + private val assetPath: String +): FileHandle.Stub() { + override fun exists() = true + override fun create() = false + override fun delete() = false + + override fun open(mode: Int): ParcelFileDescriptor? { + return runCatching { + context.androidContext.assets.open(assetPath).toParcelFileDescriptor(context.coroutineScope) + }.onFailure { + AbstractLogger.directError("Failed to open asset handle: ${it.message}", it) + }.getOrNull() + } +} + + +class RemoteFileHandleManager( + private val context: RemoteSideContext +): FileHandleManager.Stub() { + override fun getFileHandle(scope: String, name: String): FileHandle? { + val fileHandleScope = FileHandleScope.fromValue(scope) ?: run { + context.log.error("invalid file handle scope: $scope", "FileHandleManager") + return null + } + when (fileHandleScope) { + FileHandleScope.INTERNAL -> { + val fileHandleType = InternalFileHandleType.fromValue(name) ?: run { + context.log.error("invalid file handle name: $name", "FileHandleManager") + return null + } + + return LocalFileHandle( + fileHandleType.resolve(context.androidContext) + ) + } + FileHandleScope.LOCALE -> { + val foundLocale = context.androidContext.resources.assets.list("lang")?.firstOrNull { + it.startsWith(name) + }?.substringBefore(".") ?: return null + + if (name == LocaleWrapper.DEFAULT_LOCALE) { + return AssetFileHandle( + context, + "lang/${LocaleWrapper.DEFAULT_LOCALE}.json" + ) + } + + return AssetFileHandle( + context, + "lang/$foundLocale.json" + ) + } + else -> return null + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -23,7 +23,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.bridge.BridgeService import me.rhunk.snapenhance.common.BuildConfig -import me.rhunk.snapenhance.common.bridge.types.BridgeFileType import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.common.bridge.wrapper.LoggerWrapper import me.rhunk.snapenhance.common.bridge.wrapper.MappingsWrapper @@ -60,9 +59,10 @@ class RemoteSideContext( set(value) { _activity?.clear(); _activity = WeakReference(value) } val sharedPreferences: SharedPreferences get() = androidContext.getSharedPreferences("prefs", 0) - val config = ModConfig(androidContext) - val translation = LocaleWrapper() - val mappings = MappingsWrapper() + val fileHandleManager = RemoteFileHandleManager(this) + val config = ModConfig(androidContext, fileHandleManager) + val translation = LocaleWrapper(fileHandleManager) + val mappings = MappingsWrapper(fileHandleManager) val taskManager = TaskManager(this) val database = AppDatabase(this) val streaksReminder = StreaksReminder(this) @@ -70,7 +70,7 @@ class RemoteSideContext( val scriptManager = RemoteScriptManager(this) val settingsOverlay = SettingsOverlay(this) val e2eeImplementation = E2EEImplementation(this) - val messageLogger by lazy { LoggerWrapper(androidContext.getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)) } + val messageLogger by lazy { LoggerWrapper(androidContext) } val tracker = RemoteTracker(this) val accountStorage = RemoteAccountStorage(this) @@ -99,16 +99,15 @@ class RemoteSideContext( runBlocking(Dispatchers.IO) { log.init() log.verbose("Loading RemoteSideContext") - config.loadFromContext(androidContext) + config.load() launch { mappings.apply { - loadFromContext(androidContext) init(androidContext) } } translation.apply { userLocale = config.locale - loadFromContext(androidContext) + load() } database.init() streaksReminder.init() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -8,9 +8,6 @@ import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge -import me.rhunk.snapenhance.common.bridge.types.BridgeFileType -import me.rhunk.snapenhance.common.bridge.types.FileActionType -import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.common.data.MessagingFriendInfo import me.rhunk.snapenhance.common.data.MessagingGroupInfo import me.rhunk.snapenhance.common.data.SocialScope @@ -84,54 +81,11 @@ class BridgeService : Service() { } inner class BridgeBinder : BridgeInterface.Stub() { + override fun getApplicationApkPath(): String = applicationInfo.publicSourceDir + override fun broadcastLog(tag: String, level: String, message: String) { remoteSideContext.log.internalLog(tag, LogLevel.fromShortName(level) ?: LogLevel.INFO, message) } - - override fun fileOperation(action: Int, fileType: Int, content: ByteArray?): ByteArray { - val resolvedFile = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService) - - return when (FileActionType.entries[action]) { - FileActionType.CREATE_AND_READ -> { - resolvedFile?.let { - if (!it.exists()) { - return content?.also { content -> it.writeBytes(content) } ?: ByteArray( - 0 - ) - } - - it.readBytes() - } ?: ByteArray(0) - } - - FileActionType.READ -> { - resolvedFile?.takeIf { it.exists() }?.readBytes() ?: ByteArray(0) - } - - FileActionType.WRITE -> { - content?.also { resolvedFile?.writeBytes(content) } ?: ByteArray(0) - } - - FileActionType.DELETE -> { - resolvedFile?.takeIf { it.exists() }?.delete() - ByteArray(0) - } - - FileActionType.EXISTS -> { - if (resolvedFile?.exists() == true) - ByteArray(1) - else ByteArray(0) - } - } - } - - override fun getApplicationApkPath(): String = applicationInfo.publicSourceDir - - override fun fetchLocales(userLocale: String) = - LocaleWrapper.fetchLocales(context = this@BridgeService, userLocale).associate { - it.locale to it.content - } - override fun enqueueDownload(intent: Intent, callback: DownloadCallback) { DownloadProcessor( remoteSideContext = remoteSideContext, @@ -242,6 +196,8 @@ class BridgeService : Service() { override fun getLogger() = remoteSideContext.messageLogger override fun getTracker() = remoteSideContext.tracker override fun getAccountStorage() = remoteSideContext.accountStorage + override fun getFileHandleManager() = remoteSideContext.fileHandleManager + override fun registerMessagingBridge(bridge: MessagingBridge) { messagingBridge = bridge } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt @@ -222,9 +222,7 @@ class LoggerHistoryRoot : Routes.Route() { @OptIn(ExperimentalMaterial3Api::class) override val content: @Composable (NavBackStackEntry) -> Unit = { LaunchedEffect(Unit) { - loggerWrapper = LoggerWrapper( - context.androidContext.getDatabasePath("message_logger.db") - ) + loggerWrapper = LoggerWrapper(context.androidContext) } Column { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt @@ -17,12 +17,10 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.core.net.toUri import androidx.navigation.NavBackStackEntry -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import me.rhunk.snapenhance.common.Constants import me.rhunk.snapenhance.common.action.EnumAction -import me.rhunk.snapenhance.common.bridge.types.BridgeFileType +import me.rhunk.snapenhance.common.bridge.InternalFileHandleType import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState import me.rhunk.snapenhance.ui.manager.Routes import me.rhunk.snapenhance.ui.setup.Requirements @@ -239,7 +237,7 @@ class HomeSettings : Routes.Route() { horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - var selectedFileType by remember { mutableStateOf(BridgeFileType.entries.first()) } + var selectedFileType by remember { mutableStateOf(InternalFileHandleType.entries.first()) } Box( modifier = Modifier .weight(1f) @@ -253,19 +251,19 @@ class HomeSettings : Routes.Route() { modifier = Modifier.fillMaxWidth(0.7f) ) { TextField( - value = selectedFileType.displayName, + value = selectedFileType.fileName, onValueChange = {}, readOnly = true, modifier = Modifier.menuAnchor() ) ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - BridgeFileType.entries.forEach { fileType -> + InternalFileHandleType.entries.forEach { fileType -> DropdownMenuItem(onClick = { expanded = false selectedFileType = fileType }, text = { - Text(text = fileType.displayName) + Text(text = fileType.fileName) }) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt @@ -46,14 +46,13 @@ class PickLanguageScreen : SetupScreen(){ } private fun reloadTranslation(selectedLocale: String) { - context.translation.reloadFromContext(context.androidContext, selectedLocale) + context.translation.reload(selectedLocale) } private fun setLocale(locale: String) { with(context) { config.locale = locale config.writeConfig() - translation.reloadFromContext(androidContext, locale) reloadTranslation(locale) } } diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -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(); diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/storage/FileHandle.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/storage/FileHandle.aidl @@ -0,0 +1,9 @@ +package me.rhunk.snapenhance.bridge.storage; + +interface FileHandle { + boolean exists(); + boolean create(); + boolean delete(); + + @nullable ParcelFileDescriptor open(int mode); +}+ \ No newline at end of file diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/storage/FileHandleManager.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/storage/FileHandleManager.aidl @@ -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); +}+ \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/BridgeFiles.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/BridgeFiles.kt @@ -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) + } + } +} + + + diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/FileLoaderWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/FileLoaderWrapper.kt @@ -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() } - } - -}- \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/InternalFileWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/InternalFileWrapper.kt @@ -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() + } +}+ \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/BridgeFileType.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/BridgeFileType.kt @@ -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 } - } - } -}- \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/FileActionType.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/FileActionType.kt @@ -1,5 +0,0 @@ -package me.rhunk.snapenhance.common.bridge.types - -enum class FileActionType { - CREATE_AND_READ, READ, WRITE, DELETE, EXISTS -}- \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/LocalePair.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/LocalePair.kt @@ -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) - } -}- \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LocaleWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LocaleWrapper.kt @@ -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") + } + ) - fun loadFromContext(context: Context) { - fetchLocales(context, userLocale).forEach { - load(it) + if (userLocale != DEFAULT_LOCALE) { + fileHandleManager.getFileHandle(FileHandleScope.LOCALE.key, "$userLocale.json")?.open(ParcelFileDescriptor.MODE_READ_ONLY)?.let { + load(userLocale, 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.") } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt @@ -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)) diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt @@ -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()) } } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/ModConfig.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/ModConfig.kt @@ -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() - } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt @@ -19,7 +19,6 @@ import me.rhunk.snapenhance.common.bridge.wrapper.MappingsWrapper import me.rhunk.snapenhance.common.config.ModConfig import me.rhunk.snapenhance.core.action.ActionManager import me.rhunk.snapenhance.core.bridge.BridgeClient -import me.rhunk.snapenhance.core.bridge.loadFromBridge import me.rhunk.snapenhance.core.database.DatabaseAccess import me.rhunk.snapenhance.core.event.EventBus import me.rhunk.snapenhance.core.event.EventDispatcher @@ -48,15 +47,15 @@ class ModContext( val resources: Resources get() = androidContext.resources val gson: Gson = GsonBuilder().create() - private val _config = ModConfig(androidContext) - val config by _config::root + private val _config by lazy { ModConfig(androidContext, bridgeClient.getFileHandlerManager()) } + val config get() = _config.root val log by lazy { CoreLogger(this.bridgeClient) } - val translation = LocaleWrapper() + val translation by lazy { LocaleWrapper(bridgeClient.getFileHandlerManager()) } val httpServer = HttpServer() val messageSender = MessageSender(this) val features = FeatureManager(this) - val mappings = MappingsWrapper() + val mappings by lazy { MappingsWrapper(bridgeClient.getFileHandlerManager()) } val actionManager = ActionManager(this) val database = DatabaseAccess(this) val event = EventBus(this) @@ -144,9 +143,7 @@ class ModContext( fun reloadConfig() { log.verbose("reloading config") - _config.loadFromCallback { file -> - file.loadFromBridge(bridgeClient) - } + _config.load() reloadNativeConfig() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -19,7 +19,6 @@ import me.rhunk.snapenhance.common.data.MessagingFriendInfo import me.rhunk.snapenhance.common.data.MessagingGroupInfo import me.rhunk.snapenhance.common.util.toSerialized import me.rhunk.snapenhance.core.bridge.BridgeClient -import me.rhunk.snapenhance.core.bridge.loadFromBridge import me.rhunk.snapenhance.core.data.SnapClassCache import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent @@ -148,12 +147,9 @@ class SnapEnhance { log.error("Failed to sync remote", it) } translation.userLocale = getConfigLocale() - translation.loadFromCallback { locale -> - bridgeClient.fetchLocales(locale) - } + translation.load() } - mappings.loadFromBridge(bridgeClient) mappings.init(androidContext) database.init() eventDispatcher.init() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt @@ -18,11 +18,8 @@ import me.rhunk.snapenhance.bridge.logger.LoggerInterface import me.rhunk.snapenhance.bridge.logger.TrackerInterface import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge +import me.rhunk.snapenhance.bridge.storage.FileHandleManager 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.types.FileActionType -import me.rhunk.snapenhance.common.bridge.types.LocalePair import me.rhunk.snapenhance.common.data.MessagingFriendInfo import me.rhunk.snapenhance.common.data.MessagingGroupInfo import me.rhunk.snapenhance.common.data.MessagingRuleType @@ -34,14 +31,6 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.system.exitProcess -fun FileLoaderWrapper.loadFromBridge(bridgeClient: BridgeClient) { - isFileExists = { bridgeClient.isFileExists(fileType) } - read = { bridgeClient.createAndReadFile(fileType, defaultContent) } - write = { bridgeClient.writeFile(fileType, it) } - delete = { bridgeClient.deleteFile(fileType) } -} - - class BridgeClient( private val context: ModContext ): ServiceConnection { @@ -125,39 +114,6 @@ class BridgeClient( } } - //TODO: use interfaces instead of direct file access - fun createAndReadFile( - fileType: BridgeFileType, - defaultContent: ByteArray - ): ByteArray = safeServiceCall { - service.fileOperation(FileActionType.CREATE_AND_READ.ordinal, fileType.value, defaultContent) - } - - fun readFile(fileType: BridgeFileType): ByteArray = safeServiceCall { service.fileOperation(FileActionType.READ.ordinal, fileType.value, null) } - - fun writeFile( - fileType: BridgeFileType, - content: ByteArray? - ): ByteArray = safeServiceCall { - service.fileOperation(FileActionType.WRITE.ordinal, fileType.value, content) - } - - fun deleteFile(fileType: BridgeFileType) { - safeServiceCall { - service.fileOperation(FileActionType.DELETE.ordinal, fileType.value, null) - } - } - - fun isFileExists(fileType: BridgeFileType) = safeServiceCall { - service.fileOperation(FileActionType.EXISTS.ordinal, fileType.value, null).isNotEmpty() - } - - fun fetchLocales(userLocale: String) = safeServiceCall { - service.fetchLocales(userLocale).map { - LocalePair(it.key, it.value) - } - } - fun getApplicationApkPath(): String = safeServiceCall { service.applicationApkPath } fun enqueueDownload(intent: Intent, callback: DownloadCallback) = safeServiceCall { @@ -215,6 +171,8 @@ class BridgeClient( fun getAccountStorage(): AccountStorage = safeServiceCall { service.accountStorage } + fun getFileHandlerManager(): FileHandleManager = safeServiceCall { service.fileHandleManager } + fun registerMessagingBridge(bridge: MessagingBridge) = safeServiceCall { service.registerMessagingBridge(bridge) } fun openSettingsOverlay() = safeServiceCall { service.openSettingsOverlay() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/BridgeFileFeature.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/BridgeFileFeature.kt @@ -1,32 +1,38 @@ package me.rhunk.snapenhance.core.features -import me.rhunk.snapenhance.common.bridge.types.BridgeFileType +import me.rhunk.snapenhance.common.bridge.FileHandleScope +import me.rhunk.snapenhance.common.bridge.InternalFileHandleType +import me.rhunk.snapenhance.common.bridge.toWrapper import java.io.BufferedReader -import java.io.ByteArrayInputStream import java.io.InputStreamReader import java.nio.charset.StandardCharsets -abstract class BridgeFileFeature(name: String, private val bridgeFileType: BridgeFileType, loadParams: Int) : Feature(name, loadParams) { +abstract class BridgeFileFeature(name: String, private val bridgeFileType: InternalFileHandleType, loadParams: Int) : Feature(name, loadParams) { private val fileLines = mutableListOf<String>() + private val fileWrapper by lazy { context.bridgeClient.getFileHandlerManager().getFileHandle(FileHandleScope.INTERNAL.key, bridgeFileType.key)!!.toWrapper() } protected fun readFile() { val temporaryLines = mutableListOf<String>() - val fileData: ByteArray = context.bridgeClient.createAndReadFile(bridgeFileType, ByteArray(0)) - with(BufferedReader(InputStreamReader(ByteArrayInputStream(fileData), StandardCharsets.UTF_8))) { - var line = "" - while (readLine()?.also { line = it } != null) temporaryLines.add(line) - close() + fileWrapper.inputStream { stream -> + with(BufferedReader(InputStreamReader(stream, StandardCharsets.UTF_8))) { + var line = "" + while (readLine()?.also { line = it } != null) temporaryLines.add(line) + close() + } } + fileLines.clear() fileLines.addAll(temporaryLines) } private fun updateFile() { - val sb = StringBuilder() - fileLines.forEach { - sb.append(it).append("\n") + fileWrapper.outputStream { stream -> + fileLines.forEach { + stream.write(it.toByteArray()) + stream.write("\n".toByteArray()) + stream.flush() + } } - context.bridgeClient.writeFile(bridgeFileType, sb.toString().toByteArray(Charsets.UTF_8)) } protected fun exists(line: String) = fileLines.contains(line) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BestFriendPinning.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BestFriendPinning.kt @@ -7,7 +7,7 @@ import com.google.gson.JsonObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import me.rhunk.snapenhance.common.bridge.types.BridgeFileType +import me.rhunk.snapenhance.common.bridge.InternalFileHandleType import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent @@ -18,7 +18,7 @@ import java.io.InputStreamReader import java.nio.ByteBuffer import java.util.UUID -class BestFriendPinning: BridgeFileFeature("Best Friend Pinning", BridgeFileType.PINNED_BEST_FRIEND, loadParams = FeatureLoadParams.INIT_SYNC) { +class BestFriendPinning: BridgeFileFeature("Best Friend Pinning", InternalFileHandleType.PINNED_BEST_FRIEND, loadParams = FeatureLoadParams.INIT_SYNC) { private fun updatePinnedBestFriendStatus() { lines().firstOrNull()?.trim()?.let { context.database.updatePinnedBestFriendStatus(it.substring(0, 36), "number_one_bf_for_two_months") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SuspendLocationUpdates.kt @@ -2,7 +2,7 @@ package me.rhunk.snapenhance.core.features.impl.global import android.view.ViewGroup import android.widget.Switch -import me.rhunk.snapenhance.common.bridge.types.BridgeFileType +import me.rhunk.snapenhance.common.bridge.InternalFileHandleType import me.rhunk.snapenhance.core.event.events.impl.LayoutInflateEvent import me.rhunk.snapenhance.core.features.BridgeFileFeature import me.rhunk.snapenhance.core.features.FeatureLoadParams @@ -12,7 +12,7 @@ import me.rhunk.snapenhance.core.util.ktx.getLayoutId class SuspendLocationUpdates : BridgeFileFeature( "Suspend Location Updates", - loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC, bridgeFileType = BridgeFileType.SUSPEND_LOCATION_STATE) { + loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC, bridgeFileType = InternalFileHandleType.SUSPEND_LOCATION_STATE) { fun isSuspended() = exists("true") private fun setSuspended(suspended: Boolean) = setState("true", suspended)