commit 691510235b9b9ca7f72ff1e60ded77d950c848b6
parent 4bf421441bdfbd2fae3cf658ccc9711a035e2729
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu,  9 Nov 2023 20:31:53 +0100

perf(core): database corruption

Diffstat:
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/CleanCache.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt | 175+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt | 2+-
3 files changed, 102 insertions(+), 76 deletions(-)

diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/CleanCache.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/CleanCache.kt @@ -17,6 +17,7 @@ class CleanCache : AbstractAction() { "databases/journal.db", "databases/arroyo.db", "databases/arroyo.db-wal", + "databases/arroyo.db-shm", "databases/native_content_manager/*" ) } 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 @@ -1,89 +1,119 @@ package me.rhunk.snapenhance.core.database -import android.annotation.SuppressLint +import android.database.Cursor import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteDatabaseCorruptException import me.rhunk.snapenhance.common.database.DatabaseObject import me.rhunk.snapenhance.common.database.impl.ConversationMessage import me.rhunk.snapenhance.common.database.impl.FriendFeedEntry import me.rhunk.snapenhance.common.database.impl.FriendInfo import me.rhunk.snapenhance.common.database.impl.StoryEntry import me.rhunk.snapenhance.common.database.impl.UserConversationLink +import me.rhunk.snapenhance.common.util.ktx.getInteger import me.rhunk.snapenhance.common.util.ktx.getStringOrNull import me.rhunk.snapenhance.core.ModContext -import me.rhunk.snapenhance.core.logger.CoreLogger import me.rhunk.snapenhance.core.manager.Manager -import java.lang.ref.WeakReference +import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper +import java.io.File -inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? { - synchronized(this) { - if (!isOpen) { - return null - } + +class DatabaseAccess( + private val context: ModContext +) : Manager { + private val mainDb by lazy { openLocalDatabase("main.db") } + private val arroyoDb by lazy { openLocalDatabase("arroyo.db") } + + private inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? { return runCatching { query() }.onFailure { - CoreLogger.xposedLog("Database operation failed", it) + context.log.error("Database operation failed", it) + }.getOrNull() + } + + private var hasShownDatabaseError = false + + private fun showDatabaseError(databasePath: String, throwable: Throwable) { + if (hasShownDatabaseError) return + hasShownDatabaseError = true + context.runOnUiThread { + if (context.mainActivity == null) return@runOnUiThread + ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) + .setTitle("SnapEnhance") + .setMessage("Failed to query $databasePath database!\n\n${throwable.localizedMessage}\n\nRestarting Snapchat may fix this issue. If the issue persists, try to clean the app data and cache.") + .setPositiveButton("Restart Snapchat") { _, _ -> + File(databasePath).takeIf { it.exists() }?.delete() + context.softRestartApp() + } + .setNegativeButton("Dismiss") { dialog, _ -> + dialog.dismiss() + }.show() + } + } + + private fun SQLiteDatabase.safeRawQuery(query: String, args: Array<String>? = null): Cursor? { + return runCatching { + rawQuery(query, args) + }.onFailure { + if (it !is SQLiteDatabaseCorruptException) { + context.log.error("Failed to execute query $query", it) + showDatabaseError(this.path, it) + return@onFailure + } + context.log.warn("Database ${this.path} is corrupted!") + context.androidContext.deleteDatabase(this.path) + showDatabaseError(this.path, it) }.getOrNull() } -} -@SuppressLint("Range") -class DatabaseAccess( - private val context: ModContext -) : Manager { private val dmOtherParticipantCache by lazy { - (openArroyo().performOperation { - rawQuery( + (arroyoDb?.performOperation { + safeRawQuery( "SELECT client_conversation_id, user_id FROM user_conversation WHERE conversation_type = 0 AND user_id != ?", arrayOf(myUserId) - ).use { query -> + )?.use { query -> val participants = mutableMapOf<String, String?>() if (!query.moveToFirst()) { return@performOperation null } do { - participants[query.getString(query.getColumnIndex("client_conversation_id"))] = - query.getString(query.getColumnIndex("user_id")) + participants[query.getStringOrNull("client_conversation_id")!!] = query.getStringOrNull("user_id")!! } while (query.moveToNext()) participants } } ?: emptyMap()).toMutableMap() } - private var databaseWeakMap = mutableMapOf<String, WeakReference<SQLiteDatabase>?>() - - private fun openLocalDatabase(fileName: String): SQLiteDatabase { - if (databaseWeakMap.containsKey(fileName)) { - val database = databaseWeakMap[fileName]?.get() - if (database != null && database.isOpen) return database - } - + private fun openLocalDatabase(fileName: String): SQLiteDatabase? { + val dbPath = context.androidContext.getDatabasePath(fileName) + if (!dbPath.exists()) return null return runCatching { SQLiteDatabase.openDatabase( - context.androidContext.getDatabasePath(fileName).absolutePath, + dbPath.absolutePath, null, - SQLiteDatabase.OPEN_READONLY - )?.also { - databaseWeakMap[fileName] = WeakReference(it) - } + SQLiteDatabase.OPEN_READONLY or SQLiteDatabase.NO_LOCALIZED_COLLATORS + ) }.onFailure { - context.log.error("Failed to open database $fileName, restarting!", it) - }.getOrNull() ?: throw IllegalStateException("Failed to open database $fileName") + context.log.error("Failed to open database $fileName!", it) + showDatabaseError(dbPath.absolutePath, it) + }.getOrNull() } - private fun openMain() = openLocalDatabase("main.db") - private fun openArroyo() = openLocalDatabase("arroyo.db") + fun hasMain(): Boolean = mainDb?.isOpen == true + fun hasArroyo(): Boolean = arroyoDb?.isOpen == true - fun hasMain(): Boolean = context.androidContext.getDatabasePath("main.db").exists() - fun hasArroyo(): Boolean = context.androidContext.getDatabasePath("arroyo.db").exists() + fun finalize() { + mainDb?.close() + arroyoDb?.close() + context.log.verbose("Database closed") + } - private fun <T : DatabaseObject> readDatabaseObject( + private fun <T : DatabaseObject> SQLiteDatabase.readDatabaseObject( obj: T, - database: SQLiteDatabase, table: String, where: String, args: Array<String> - ): T? = database.rawQuery("SELECT * FROM $table WHERE $where", args).use { + ): T? = this.safeRawQuery("SELECT * FROM $table WHERE $where", args)?.use { if (!it.moveToFirst()) { return null } @@ -96,10 +126,9 @@ class DatabaseAccess( } fun getFeedEntryByUserId(userId: String): FriendFeedEntry? { - return openMain().performOperation { + return mainDb?.performOperation { readDatabaseObject( FriendFeedEntry(), - this, "FriendsFeedView", "friendUserId = ?", arrayOf(userId) @@ -108,10 +137,10 @@ class DatabaseAccess( } val myUserId by lazy { - openArroyo().performOperation { - rawQuery(buildString { + arroyoDb?.performOperation { + safeRawQuery(buildString { append("SELECT value FROM required_values WHERE key = 'USERID'") - }, null).use { query -> + }, null)?.use { query -> if (!query.moveToFirst()) { return@performOperation null } @@ -121,10 +150,9 @@ class DatabaseAccess( } fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? { - return openMain().performOperation { + return mainDb?.performOperation { readDatabaseObject( FriendFeedEntry(), - this, "FriendsFeedView", "key = ?", arrayOf(conversationId) @@ -133,10 +161,9 @@ class DatabaseAccess( } fun getFriendInfo(userId: String): FriendInfo? { - return openMain().performOperation { + return mainDb?.performOperation { readDatabaseObject( FriendInfo(), - this, "FriendWithUsername", "userId = ?", arrayOf(userId) @@ -145,11 +172,11 @@ class DatabaseAccess( } fun getFeedEntries(limit: Int): List<FriendFeedEntry> { - return openMain().performOperation { - rawQuery( + return mainDb?.performOperation { + safeRawQuery( "SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?", arrayOf(limit.toString()) - ).use { query -> + )?.use { query -> val list = mutableListOf<FriendFeedEntry>() while (query.moveToNext()) { val friendFeedEntry = FriendFeedEntry() @@ -164,10 +191,9 @@ class DatabaseAccess( } fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? { - return openArroyo().performOperation { + return arroyoDb?.performOperation { readDatabaseObject( ConversationMessage(), - this, "conversation_message", "client_message_id = ?", arrayOf(clientMessageId.toString()) @@ -176,24 +202,23 @@ class DatabaseAccess( } fun getConversationType(conversationId: String): Int? { - return openArroyo().performOperation { - rawQuery( + return arroyoDb?.performOperation { + safeRawQuery( "SELECT conversation_type FROM user_conversation WHERE client_conversation_id = ?", arrayOf(conversationId) - ).use { query -> + )?.use { query -> if (!query.moveToFirst()) { return@performOperation null } - query.getInt(query.getColumnIndex("conversation_type")) + query.getInteger("conversation_type") } } } fun getConversationLinkFromUserId(userId: String): UserConversationLink? { - return openArroyo().performOperation { + return arroyoDb?.performOperation { readDatabaseObject( UserConversationLink(), - this, "user_conversation", "user_id = ? AND conversation_type = 0", arrayOf(userId) @@ -203,17 +228,17 @@ class DatabaseAccess( fun getDMOtherParticipant(conversationId: String): String? { if (dmOtherParticipantCache.containsKey(conversationId)) return dmOtherParticipantCache[conversationId] - return openArroyo().performOperation { - rawQuery( + return arroyoDb?.performOperation { + safeRawQuery( "SELECT user_id FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0", arrayOf(conversationId) - ).use { query -> + )?.use { query -> val participants = mutableListOf<String>() if (!query.moveToFirst()) { return@performOperation null } do { - participants.add(query.getString(query.getColumnIndex("user_id"))) + participants.add(query.getStringOrNull("user_id")!!) } while (query.moveToNext()) participants.firstOrNull { it != myUserId } } @@ -222,23 +247,23 @@ class DatabaseAccess( fun getStoryEntryFromId(storyId: String): StoryEntry? { - return openMain().performOperation { - readDatabaseObject(StoryEntry(), this, "Story", "storyId = ?", arrayOf(storyId)) + return mainDb?.performOperation { + readDatabaseObject(StoryEntry(), "Story", "storyId = ?", arrayOf(storyId)) } } fun getConversationParticipants(conversationId: String): List<String>? { - return openArroyo().performOperation { - rawQuery( + return arroyoDb?.performOperation { + safeRawQuery( "SELECT user_id FROM user_conversation WHERE client_conversation_id = ?", arrayOf(conversationId) - ).use { + )?.use { if (!it.moveToFirst()) { return@performOperation null } val participants = mutableListOf<String>() do { - participants.add(it.getString(it.getColumnIndex("user_id"))) + participants.add(it.getStringOrNull("user_id")!!) } while (it.moveToNext()) participants } @@ -249,11 +274,11 @@ class DatabaseAccess( conversationId: String, limit: Int ): List<ConversationMessage>? { - return openArroyo().performOperation { - rawQuery( + return arroyoDb?.performOperation { + safeRawQuery( "SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?", arrayOf(conversationId, limit.toString()) - ).use { query -> + )?.use { query -> if (!query.moveToFirst()) { return@performOperation null } @@ -269,7 +294,7 @@ class DatabaseAccess( } fun getAddSource(userId: String): String? { - return openMain().performOperation { + return mainDb?.performOperation { rawQuery( "SELECT addSource FROM FriendWhoAddedMe WHERE userId = ?", arrayOf(userId) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt @@ -57,13 +57,13 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } } - val myUserId = context.database.myUserId context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param -> val instance = param.thisObject<Any>() val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor val messages = (interactionInfo.getObjectFieldOrNull("mMessages") as? List<*>)?.map { Message(it) } ?: return@hookConstructor val conversationId = SnapUUID(instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor).toString() + val myUserId = context.database.myUserId feedCachedSnapMessages[conversationId] = messages.filter { msg -> msg.messageMetadata?.seenBy?.none { it.toString() == myUserId } == true