mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 05:37:48 +02:00
perf(core): database corruption
This commit is contained in:
@ -17,6 +17,7 @@ class CleanCache : AbstractAction() {
|
|||||||
"databases/journal.db",
|
"databases/journal.db",
|
||||||
"databases/arroyo.db",
|
"databases/arroyo.db",
|
||||||
"databases/arroyo.db-wal",
|
"databases/arroyo.db-wal",
|
||||||
|
"databases/arroyo.db-shm",
|
||||||
"databases/native_content_manager/*"
|
"databases/native_content_manager/*"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,89 +1,119 @@
|
|||||||
package me.rhunk.snapenhance.core.database
|
package me.rhunk.snapenhance.core.database
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.database.Cursor
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import android.database.sqlite.SQLiteDatabaseCorruptException
|
||||||
import me.rhunk.snapenhance.common.database.DatabaseObject
|
import me.rhunk.snapenhance.common.database.DatabaseObject
|
||||||
import me.rhunk.snapenhance.common.database.impl.ConversationMessage
|
import me.rhunk.snapenhance.common.database.impl.ConversationMessage
|
||||||
import me.rhunk.snapenhance.common.database.impl.FriendFeedEntry
|
import me.rhunk.snapenhance.common.database.impl.FriendFeedEntry
|
||||||
import me.rhunk.snapenhance.common.database.impl.FriendInfo
|
import me.rhunk.snapenhance.common.database.impl.FriendInfo
|
||||||
import me.rhunk.snapenhance.common.database.impl.StoryEntry
|
import me.rhunk.snapenhance.common.database.impl.StoryEntry
|
||||||
import me.rhunk.snapenhance.common.database.impl.UserConversationLink
|
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.common.util.ktx.getStringOrNull
|
||||||
import me.rhunk.snapenhance.core.ModContext
|
import me.rhunk.snapenhance.core.ModContext
|
||||||
import me.rhunk.snapenhance.core.logger.CoreLogger
|
|
||||||
import me.rhunk.snapenhance.core.manager.Manager
|
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
|
|
||||||
}
|
|
||||||
return runCatching {
|
|
||||||
query()
|
|
||||||
}.onFailure {
|
|
||||||
CoreLogger.xposedLog("Database operation failed", it)
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Range")
|
|
||||||
class DatabaseAccess(
|
class DatabaseAccess(
|
||||||
private val context: ModContext
|
private val context: ModContext
|
||||||
) : Manager {
|
) : 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 {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
private val dmOtherParticipantCache by lazy {
|
private val dmOtherParticipantCache by lazy {
|
||||||
(openArroyo().performOperation {
|
(arroyoDb?.performOperation {
|
||||||
rawQuery(
|
safeRawQuery(
|
||||||
"SELECT client_conversation_id, user_id FROM user_conversation WHERE conversation_type = 0 AND user_id != ?",
|
"SELECT client_conversation_id, user_id FROM user_conversation WHERE conversation_type = 0 AND user_id != ?",
|
||||||
arrayOf(myUserId)
|
arrayOf(myUserId)
|
||||||
).use { query ->
|
)?.use { query ->
|
||||||
val participants = mutableMapOf<String, String?>()
|
val participants = mutableMapOf<String, String?>()
|
||||||
if (!query.moveToFirst()) {
|
if (!query.moveToFirst()) {
|
||||||
return@performOperation null
|
return@performOperation null
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
participants[query.getString(query.getColumnIndex("client_conversation_id"))] =
|
participants[query.getStringOrNull("client_conversation_id")!!] = query.getStringOrNull("user_id")!!
|
||||||
query.getString(query.getColumnIndex("user_id"))
|
|
||||||
} while (query.moveToNext())
|
} while (query.moveToNext())
|
||||||
participants
|
participants
|
||||||
}
|
}
|
||||||
} ?: emptyMap()).toMutableMap()
|
} ?: emptyMap()).toMutableMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var databaseWeakMap = mutableMapOf<String, WeakReference<SQLiteDatabase>?>()
|
private fun openLocalDatabase(fileName: String): SQLiteDatabase? {
|
||||||
|
val dbPath = context.androidContext.getDatabasePath(fileName)
|
||||||
private fun openLocalDatabase(fileName: String): SQLiteDatabase {
|
if (!dbPath.exists()) return null
|
||||||
if (databaseWeakMap.containsKey(fileName)) {
|
|
||||||
val database = databaseWeakMap[fileName]?.get()
|
|
||||||
if (database != null && database.isOpen) return database
|
|
||||||
}
|
|
||||||
|
|
||||||
return runCatching {
|
return runCatching {
|
||||||
SQLiteDatabase.openDatabase(
|
SQLiteDatabase.openDatabase(
|
||||||
context.androidContext.getDatabasePath(fileName).absolutePath,
|
dbPath.absolutePath,
|
||||||
null,
|
null,
|
||||||
SQLiteDatabase.OPEN_READONLY
|
SQLiteDatabase.OPEN_READONLY or SQLiteDatabase.NO_LOCALIZED_COLLATORS
|
||||||
)?.also {
|
)
|
||||||
databaseWeakMap[fileName] = WeakReference(it)
|
|
||||||
}
|
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
context.log.error("Failed to open database $fileName, restarting!", it)
|
context.log.error("Failed to open database $fileName!", it)
|
||||||
}.getOrNull() ?: throw IllegalStateException("Failed to open database $fileName")
|
showDatabaseError(dbPath.absolutePath, it)
|
||||||
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openMain() = openLocalDatabase("main.db")
|
fun hasMain(): Boolean = mainDb?.isOpen == true
|
||||||
private fun openArroyo() = openLocalDatabase("arroyo.db")
|
fun hasArroyo(): Boolean = arroyoDb?.isOpen == true
|
||||||
|
|
||||||
fun hasMain(): Boolean = context.androidContext.getDatabasePath("main.db").exists()
|
fun finalize() {
|
||||||
fun hasArroyo(): Boolean = context.androidContext.getDatabasePath("arroyo.db").exists()
|
mainDb?.close()
|
||||||
|
arroyoDb?.close()
|
||||||
|
context.log.verbose("Database closed")
|
||||||
|
}
|
||||||
|
|
||||||
private fun <T : DatabaseObject> readDatabaseObject(
|
private fun <T : DatabaseObject> SQLiteDatabase.readDatabaseObject(
|
||||||
obj: T,
|
obj: T,
|
||||||
database: SQLiteDatabase,
|
|
||||||
table: String,
|
table: String,
|
||||||
where: String,
|
where: String,
|
||||||
args: Array<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()) {
|
if (!it.moveToFirst()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -96,10 +126,9 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFeedEntryByUserId(userId: String): FriendFeedEntry? {
|
fun getFeedEntryByUserId(userId: String): FriendFeedEntry? {
|
||||||
return openMain().performOperation {
|
return mainDb?.performOperation {
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
FriendFeedEntry(),
|
FriendFeedEntry(),
|
||||||
this,
|
|
||||||
"FriendsFeedView",
|
"FriendsFeedView",
|
||||||
"friendUserId = ?",
|
"friendUserId = ?",
|
||||||
arrayOf(userId)
|
arrayOf(userId)
|
||||||
@ -108,10 +137,10 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val myUserId by lazy {
|
val myUserId by lazy {
|
||||||
openArroyo().performOperation {
|
arroyoDb?.performOperation {
|
||||||
rawQuery(buildString {
|
safeRawQuery(buildString {
|
||||||
append("SELECT value FROM required_values WHERE key = 'USERID'")
|
append("SELECT value FROM required_values WHERE key = 'USERID'")
|
||||||
}, null).use { query ->
|
}, null)?.use { query ->
|
||||||
if (!query.moveToFirst()) {
|
if (!query.moveToFirst()) {
|
||||||
return@performOperation null
|
return@performOperation null
|
||||||
}
|
}
|
||||||
@ -121,10 +150,9 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? {
|
fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? {
|
||||||
return openMain().performOperation {
|
return mainDb?.performOperation {
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
FriendFeedEntry(),
|
FriendFeedEntry(),
|
||||||
this,
|
|
||||||
"FriendsFeedView",
|
"FriendsFeedView",
|
||||||
"key = ?",
|
"key = ?",
|
||||||
arrayOf(conversationId)
|
arrayOf(conversationId)
|
||||||
@ -133,10 +161,9 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFriendInfo(userId: String): FriendInfo? {
|
fun getFriendInfo(userId: String): FriendInfo? {
|
||||||
return openMain().performOperation {
|
return mainDb?.performOperation {
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
FriendInfo(),
|
FriendInfo(),
|
||||||
this,
|
|
||||||
"FriendWithUsername",
|
"FriendWithUsername",
|
||||||
"userId = ?",
|
"userId = ?",
|
||||||
arrayOf(userId)
|
arrayOf(userId)
|
||||||
@ -145,11 +172,11 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFeedEntries(limit: Int): List<FriendFeedEntry> {
|
fun getFeedEntries(limit: Int): List<FriendFeedEntry> {
|
||||||
return openMain().performOperation {
|
return mainDb?.performOperation {
|
||||||
rawQuery(
|
safeRawQuery(
|
||||||
"SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?",
|
"SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?",
|
||||||
arrayOf(limit.toString())
|
arrayOf(limit.toString())
|
||||||
).use { query ->
|
)?.use { query ->
|
||||||
val list = mutableListOf<FriendFeedEntry>()
|
val list = mutableListOf<FriendFeedEntry>()
|
||||||
while (query.moveToNext()) {
|
while (query.moveToNext()) {
|
||||||
val friendFeedEntry = FriendFeedEntry()
|
val friendFeedEntry = FriendFeedEntry()
|
||||||
@ -164,10 +191,9 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? {
|
fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? {
|
||||||
return openArroyo().performOperation {
|
return arroyoDb?.performOperation {
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
ConversationMessage(),
|
ConversationMessage(),
|
||||||
this,
|
|
||||||
"conversation_message",
|
"conversation_message",
|
||||||
"client_message_id = ?",
|
"client_message_id = ?",
|
||||||
arrayOf(clientMessageId.toString())
|
arrayOf(clientMessageId.toString())
|
||||||
@ -176,24 +202,23 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getConversationType(conversationId: String): Int? {
|
fun getConversationType(conversationId: String): Int? {
|
||||||
return openArroyo().performOperation {
|
return arroyoDb?.performOperation {
|
||||||
rawQuery(
|
safeRawQuery(
|
||||||
"SELECT conversation_type FROM user_conversation WHERE client_conversation_id = ?",
|
"SELECT conversation_type FROM user_conversation WHERE client_conversation_id = ?",
|
||||||
arrayOf(conversationId)
|
arrayOf(conversationId)
|
||||||
).use { query ->
|
)?.use { query ->
|
||||||
if (!query.moveToFirst()) {
|
if (!query.moveToFirst()) {
|
||||||
return@performOperation null
|
return@performOperation null
|
||||||
}
|
}
|
||||||
query.getInt(query.getColumnIndex("conversation_type"))
|
query.getInteger("conversation_type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConversationLinkFromUserId(userId: String): UserConversationLink? {
|
fun getConversationLinkFromUserId(userId: String): UserConversationLink? {
|
||||||
return openArroyo().performOperation {
|
return arroyoDb?.performOperation {
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
UserConversationLink(),
|
UserConversationLink(),
|
||||||
this,
|
|
||||||
"user_conversation",
|
"user_conversation",
|
||||||
"user_id = ? AND conversation_type = 0",
|
"user_id = ? AND conversation_type = 0",
|
||||||
arrayOf(userId)
|
arrayOf(userId)
|
||||||
@ -203,17 +228,17 @@ class DatabaseAccess(
|
|||||||
|
|
||||||
fun getDMOtherParticipant(conversationId: String): String? {
|
fun getDMOtherParticipant(conversationId: String): String? {
|
||||||
if (dmOtherParticipantCache.containsKey(conversationId)) return dmOtherParticipantCache[conversationId]
|
if (dmOtherParticipantCache.containsKey(conversationId)) return dmOtherParticipantCache[conversationId]
|
||||||
return openArroyo().performOperation {
|
return arroyoDb?.performOperation {
|
||||||
rawQuery(
|
safeRawQuery(
|
||||||
"SELECT user_id FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0",
|
"SELECT user_id FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0",
|
||||||
arrayOf(conversationId)
|
arrayOf(conversationId)
|
||||||
).use { query ->
|
)?.use { query ->
|
||||||
val participants = mutableListOf<String>()
|
val participants = mutableListOf<String>()
|
||||||
if (!query.moveToFirst()) {
|
if (!query.moveToFirst()) {
|
||||||
return@performOperation null
|
return@performOperation null
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
participants.add(query.getString(query.getColumnIndex("user_id")))
|
participants.add(query.getStringOrNull("user_id")!!)
|
||||||
} while (query.moveToNext())
|
} while (query.moveToNext())
|
||||||
participants.firstOrNull { it != myUserId }
|
participants.firstOrNull { it != myUserId }
|
||||||
}
|
}
|
||||||
@ -222,23 +247,23 @@ class DatabaseAccess(
|
|||||||
|
|
||||||
|
|
||||||
fun getStoryEntryFromId(storyId: String): StoryEntry? {
|
fun getStoryEntryFromId(storyId: String): StoryEntry? {
|
||||||
return openMain().performOperation {
|
return mainDb?.performOperation {
|
||||||
readDatabaseObject(StoryEntry(), this, "Story", "storyId = ?", arrayOf(storyId))
|
readDatabaseObject(StoryEntry(), "Story", "storyId = ?", arrayOf(storyId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConversationParticipants(conversationId: String): List<String>? {
|
fun getConversationParticipants(conversationId: String): List<String>? {
|
||||||
return openArroyo().performOperation {
|
return arroyoDb?.performOperation {
|
||||||
rawQuery(
|
safeRawQuery(
|
||||||
"SELECT user_id FROM user_conversation WHERE client_conversation_id = ?",
|
"SELECT user_id FROM user_conversation WHERE client_conversation_id = ?",
|
||||||
arrayOf(conversationId)
|
arrayOf(conversationId)
|
||||||
).use {
|
)?.use {
|
||||||
if (!it.moveToFirst()) {
|
if (!it.moveToFirst()) {
|
||||||
return@performOperation null
|
return@performOperation null
|
||||||
}
|
}
|
||||||
val participants = mutableListOf<String>()
|
val participants = mutableListOf<String>()
|
||||||
do {
|
do {
|
||||||
participants.add(it.getString(it.getColumnIndex("user_id")))
|
participants.add(it.getStringOrNull("user_id")!!)
|
||||||
} while (it.moveToNext())
|
} while (it.moveToNext())
|
||||||
participants
|
participants
|
||||||
}
|
}
|
||||||
@ -249,11 +274,11 @@ class DatabaseAccess(
|
|||||||
conversationId: String,
|
conversationId: String,
|
||||||
limit: Int
|
limit: Int
|
||||||
): List<ConversationMessage>? {
|
): List<ConversationMessage>? {
|
||||||
return openArroyo().performOperation {
|
return arroyoDb?.performOperation {
|
||||||
rawQuery(
|
safeRawQuery(
|
||||||
"SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?",
|
"SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?",
|
||||||
arrayOf(conversationId, limit.toString())
|
arrayOf(conversationId, limit.toString())
|
||||||
).use { query ->
|
)?.use { query ->
|
||||||
if (!query.moveToFirst()) {
|
if (!query.moveToFirst()) {
|
||||||
return@performOperation null
|
return@performOperation null
|
||||||
}
|
}
|
||||||
@ -269,7 +294,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAddSource(userId: String): String? {
|
fun getAddSource(userId: String): String? {
|
||||||
return openMain().performOperation {
|
return mainDb?.performOperation {
|
||||||
rawQuery(
|
rawQuery(
|
||||||
"SELECT addSource FROM FriendWhoAddedMe WHERE userId = ?",
|
"SELECT addSource FROM FriendWhoAddedMe WHERE userId = ?",
|
||||||
arrayOf(userId)
|
arrayOf(userId)
|
||||||
|
@ -57,13 +57,13 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val myUserId = context.database.myUserId
|
|
||||||
|
|
||||||
context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param ->
|
context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param ->
|
||||||
val instance = param.thisObject<Any>()
|
val instance = param.thisObject<Any>()
|
||||||
val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor
|
val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor
|
||||||
val messages = (interactionInfo.getObjectFieldOrNull("mMessages") as? List<*>)?.map { Message(it) } ?: return@hookConstructor
|
val messages = (interactionInfo.getObjectFieldOrNull("mMessages") as? List<*>)?.map { Message(it) } ?: return@hookConstructor
|
||||||
val conversationId = SnapUUID(instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor).toString()
|
val conversationId = SnapUUID(instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor).toString()
|
||||||
|
val myUserId = context.database.myUserId
|
||||||
|
|
||||||
feedCachedSnapMessages[conversationId] = messages.filter { msg ->
|
feedCachedSnapMessages[conversationId] = messages.filter { msg ->
|
||||||
msg.messageMetadata?.seenBy?.none { it.toString() == myUserId } == true
|
msg.messageMetadata?.seenBy?.none { it.toString() == myUserId } == true
|
||||||
|
Reference in New Issue
Block a user