commit a90f4875a73a7ae1a985be183bf51f3bfd4a3d75
parent d1c4b4febeb8300fa7fbf6adb762134ba415a8f5
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu,  4 Jan 2024 23:09:27 +0100

fix(database): db cache

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt | 108++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportMemories.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt | 114++++++++++++++++++++++++++++++++++++++++++-------------------------------------
3 files changed, 116 insertions(+), 108 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt @@ -261,69 +261,71 @@ class ScopeContent( Spacer(modifier = Modifier.height(16.dp)) // e2ee section - SectionTitle(translation["e2ee_title"]) - var hasSecretKey by remember { mutableStateOf(context.e2eeImplementation.friendKeyExists(friend.userId))} - var importDialog by remember { mutableStateOf(false) } + if (context.config.root.experimental.e2eEncryption.globalState == true) { + SectionTitle(translation["e2ee_title"]) + var hasSecretKey by remember { mutableStateOf(context.e2eeImplementation.friendKeyExists(friend.userId))} + var importDialog by remember { mutableStateOf(false) } - if (importDialog) { - Dialog( - onDismissRequest = { importDialog = false } - ) { - dialogs.RawInputDialog(onDismiss = { importDialog = false }, onConfirm = { newKey -> - importDialog = false - runCatching { - val key = Base64.decode(newKey) - if (key.size != 32) { - context.longToast("Invalid key size (must be 32 bytes)") - return@runCatching - } + if (importDialog) { + Dialog( + onDismissRequest = { importDialog = false } + ) { + dialogs.RawInputDialog(onDismiss = { importDialog = false }, onConfirm = { newKey -> + importDialog = false + runCatching { + val key = Base64.decode(newKey) + if (key.size != 32) { + context.longToast("Invalid key size (must be 32 bytes)") + return@runCatching + } - context.e2eeImplementation.storeSharedSecretKey(friend.userId, key) - context.longToast("Successfully imported key") - hasSecretKey = true - }.onFailure { - context.longToast("Failed to import key: ${it.message}") - context.log.error("Failed to import key", it) - } - }) + context.e2eeImplementation.storeSharedSecretKey(friend.userId, key) + context.longToast("Successfully imported key") + hasSecretKey = true + }.onFailure { + context.longToast("Failed to import key: ${it.message}") + context.log.error("Failed to import key", it) + } + }) + } } - } - ContentCard { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) - ) { - if (hasSecretKey) { - OutlinedButton(onClick = { - val secretKey = Base64.encode(context.e2eeImplementation.getSharedSecretKey(friend.userId) ?: return@OutlinedButton) - //TODO: fingerprint auth - context.activity!!.startActivity(Intent.createChooser(Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, secretKey) - type = "text/plain" - }, "").apply { - putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf( - Intent().apply { - putExtra(Intent.EXTRA_TEXT, secretKey) - putExtra(Intent.EXTRA_SUBJECT, secretKey) - }) + ContentCard { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + if (hasSecretKey) { + OutlinedButton(onClick = { + val secretKey = Base64.encode(context.e2eeImplementation.getSharedSecretKey(friend.userId) ?: return@OutlinedButton) + //TODO: fingerprint auth + context.activity!!.startActivity(Intent.createChooser(Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, secretKey) + type = "text/plain" + }, "").apply { + putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf( + Intent().apply { + putExtra(Intent.EXTRA_TEXT, secretKey) + putExtra(Intent.EXTRA_SUBJECT, secretKey) + }) + ) + }) + }) { + Text( + text = "Export Base64", + maxLines = 1 ) - }) - }) { + } + } + + OutlinedButton(onClick = { importDialog = true }) { Text( - text = "Export Base64", + text = "Import Base64", maxLines = 1 ) } } - - OutlinedButton(onClick = { importDialog = true }) { - Text( - text = "Import Base64", - maxLines = 1 - ) - } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportMemories.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportMemories.kt @@ -363,7 +363,7 @@ class ExportMemories : AbstractAction() { val database = runCatching { SQLiteDatabase.openDatabase( context.androidContext.getDatabasePath("memories.db"), - OpenParams.Builder().build(), + OpenParams.Builder().setOpenFlags(SQLiteDatabase.OPEN_READONLY).build() ) }.getOrNull() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt @@ -19,17 +19,45 @@ import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.manager.Manager +enum class DatabaseType( + val fileName: String +) { + MAIN("main.db"), + ARROYO("arroyo.db") +} + class DatabaseAccess( private val context: ModContext ) : Manager { - companion object { - val DATABASES = mapOf( - "main" to "main.db", - "arroyo" to "arroyo.db" - ) + private val openedDatabases = mutableMapOf<DatabaseType, SQLiteDatabase>() + + private fun useDatabase(database: DatabaseType, writeMode: Boolean = false): SQLiteDatabase? { + if (openedDatabases.containsKey(database) && openedDatabases[database]?.isOpen == true) { + return openedDatabases[database] + } + + val dbPath = context.androidContext.getDatabasePath(database.fileName) + if (!dbPath.exists()) return null + return runCatching { + SQLiteDatabase.openDatabase( + dbPath, + OpenParams.Builder() + .setOpenFlags( + if (writeMode) SQLiteDatabase.OPEN_READWRITE or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING + else SQLiteDatabase.OPEN_READONLY + ) + .setErrorHandler { + context.androidContext.deleteDatabase(dbPath.absolutePath) + context.softRestartApp() + }.build() + ) + }.onFailure { + context.log.error("Failed to open database ${database.fileName}!", it) + }.getOrNull()?.also { + openedDatabases[database] = it + } } - private val mainDb by lazy { openLocalDatabase("main") } - private val arroyoDb by lazy { openLocalDatabase("arroyo") } + private inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? { return runCatching { @@ -56,7 +84,7 @@ class DatabaseAccess( } private val dmOtherParticipantCache by lazy { - (arroyoDb?.performOperation { + (useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT client_conversation_id, conversation_type, user_id FROM user_conversation WHERE user_id != ?", arrayOf(myUserId) @@ -79,49 +107,27 @@ class DatabaseAccess( } ?: emptyMap()).toMutableMap() } - private fun openLocalDatabase(databaseName: String, writeMode: Boolean = false): SQLiteDatabase? { - val dbPath = context.androidContext.getDatabasePath(DATABASES[databaseName]!!) - if (!dbPath.exists()) return null - return runCatching { - SQLiteDatabase.openDatabase( - dbPath, - OpenParams.Builder() - .setOpenFlags( - if (writeMode) SQLiteDatabase.OPEN_READWRITE or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING - else SQLiteDatabase.OPEN_READONLY - ) - .setErrorHandler { - context.androidContext.deleteDatabase(dbPath.absolutePath) - context.softRestartApp() - }.build() - ) - }.onFailure { - context.log.error("Failed to open database $databaseName!", it) - }.getOrNull() - } - - fun hasMain(): Boolean = mainDb?.isOpen == true - fun hasArroyo(): Boolean = arroyoDb?.isOpen == true + fun hasMain(): Boolean = useDatabase(DatabaseType.MAIN)?.isOpen == true + fun hasArroyo(): Boolean = useDatabase(DatabaseType.ARROYO)?.isOpen == true override fun init() { // perform integrity check on databases - DATABASES.forEach { (name, fileName) -> - openLocalDatabase(name, writeMode = true)?.apply { + DatabaseType.entries.forEach { type -> + useDatabase(type, writeMode = true)?.apply { rawQuery("PRAGMA integrity_check", null).use { query -> if (!query.moveToFirst() || query.getString(0).lowercase() != "ok") { - context.log.error("Failed to perform integrity check on $fileName") - context.androidContext.deleteDatabase(fileName) + context.log.error("Failed to perform integrity check on ${type.fileName}") + context.androidContext.deleteDatabase(type.fileName) return@apply } - context.log.verbose("database $fileName integrity check passed") + context.log.verbose("database ${type.fileName} integrity check passed") } }?.close() } } fun finalize() { - mainDb?.close() - arroyoDb?.close() + openedDatabases.values.forEach { it.close() } context.log.verbose("Database closed") } @@ -143,7 +149,7 @@ class DatabaseAccess( } fun getFeedEntryByUserId(userId: String): FriendFeedEntry? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { readDatabaseObject( FriendFeedEntry(), "FriendsFeedView", @@ -155,7 +161,7 @@ class DatabaseAccess( val myUserId by lazy { context.androidContext.getSharedPreferences("user_session_shared_pref", 0).getString("key_user_id", null) ?: - arroyoDb?.performOperation { + useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery(buildString { append("SELECT value FROM required_values WHERE key = 'USERID'") }, null)?.use { query -> @@ -168,7 +174,7 @@ class DatabaseAccess( } fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { readDatabaseObject( FriendFeedEntry(), "FriendsFeedView", @@ -179,7 +185,7 @@ class DatabaseAccess( } fun getFriendInfo(userId: String): FriendInfo? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { readDatabaseObject( FriendInfo(), "FriendWithUsername", @@ -190,7 +196,7 @@ class DatabaseAccess( } fun getAllFriends(): List<FriendInfo> { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { safeRawQuery( "SELECT * FROM FriendWithUsername", null @@ -209,7 +215,7 @@ class DatabaseAccess( } fun getFeedEntries(limit: Int): List<FriendFeedEntry> { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { safeRawQuery( "SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?", arrayOf(limit.toString()) @@ -228,7 +234,7 @@ class DatabaseAccess( } fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? { - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { readDatabaseObject( ConversationMessage(), "conversation_message", @@ -239,7 +245,7 @@ class DatabaseAccess( } fun getConversationType(conversationId: String): Int? { - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT conversation_type FROM user_conversation WHERE client_conversation_id = ?", arrayOf(conversationId) @@ -253,7 +259,7 @@ class DatabaseAccess( } fun getConversationLinkFromUserId(userId: String): UserConversationLink? { - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { readDatabaseObject( UserConversationLink(), "user_conversation", @@ -265,7 +271,7 @@ class DatabaseAccess( fun getDMOtherParticipant(conversationId: String): String? { if (dmOtherParticipantCache.containsKey(conversationId)) return dmOtherParticipantCache[conversationId] - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT user_id FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0", arrayOf(conversationId) @@ -284,14 +290,14 @@ class DatabaseAccess( fun getStoryEntryFromId(storyId: String): StoryEntry? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { readDatabaseObject(StoryEntry(), "Story", "storyId = ?", arrayOf(storyId)) } } fun getConversationParticipants(conversationId: String): List<String>? { if (dmOtherParticipantCache[conversationId] != null) return dmOtherParticipantCache[conversationId]?.let { listOf(myUserId, it) } - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT user_id, conversation_type FROM user_conversation WHERE client_conversation_id = ?", arrayOf(conversationId) @@ -321,7 +327,7 @@ class DatabaseAccess( conversationId: String, limit: Int ): List<ConversationMessage>? { - return arroyoDb?.performOperation { + return useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?", arrayOf(conversationId, limit.toString()) @@ -341,7 +347,7 @@ class DatabaseAccess( } fun getAddSource(userId: String): String? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { rawQuery( "SELECT addSource FROM FriendWhoAddedMe WHERE userId = ?", arrayOf(userId) @@ -355,7 +361,7 @@ class DatabaseAccess( } fun markFriendStoriesAsSeen(userId: String) { - openLocalDatabase("main", writeMode = true)?.apply { + useDatabase(DatabaseType.MAIN, writeMode = true)?.apply { performOperation { execSQL("UPDATE StorySnap SET viewed = 1 WHERE userId = ?", arrayOf(userId)) } @@ -364,7 +370,7 @@ class DatabaseAccess( } fun getAccessTokens(userId: String): Map<String, String>? { - return mainDb?.performOperation { + return useDatabase(DatabaseType.MAIN)?.performOperation { rawQuery( "SELECT accessTokensPb FROM SnapToken WHERE userId = ?", arrayOf(userId)