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

@ -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
}
}
}

View File

@ -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()

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
})
}
}

View File

@ -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)
}
}

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()
}
}

View File

@ -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()
}

View File

@ -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()

View File

@ -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() }

View File

@ -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)

View File

@ -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")

View File

@ -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)