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:
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