commit 96183921dc5febaa4bbbfdea936f08f038e6c0a6
parent da8561cddb1543af1bfeaa5f6f2a4ee22df94444
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Sat, 11 Nov 2023 23:14:42 +0100
perf(core): database access
Diffstat:
3 files changed, 149 insertions(+), 106 deletions(-)
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
@@ -2,6 +2,7 @@ package me.rhunk.snapenhance.core.database
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteDatabase.OpenParams
import android.database.sqlite.SQLiteDatabaseCorruptException
import me.rhunk.snapenhance.common.database.DatabaseObject
import me.rhunk.snapenhance.common.database.impl.ConversationMessage
@@ -9,12 +10,11 @@ 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.getIntOrNull
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.manager.Manager
-import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
-import java.io.File
class DatabaseAccess(
@@ -25,51 +25,32 @@ class DatabaseAccess(
private inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? {
return runCatching {
- query()
+ synchronized(this) {
+ 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.longToast("Database ${this.path} is corrupted! Restarting ...")
context.androidContext.deleteDatabase(this.path)
- showDatabaseError(this.path, it)
+ context.crash("Database ${this.path} is corrupted!", it)
}.getOrNull()
}
private val dmOtherParticipantCache by lazy {
(arroyoDb?.performOperation {
safeRawQuery(
- "SELECT client_conversation_id, user_id FROM user_conversation WHERE conversation_type = 0 AND user_id != ?",
+ "SELECT client_conversation_id, conversation_type, user_id FROM user_conversation WHERE user_id != ?",
arrayOf(myUserId)
)?.use { query ->
val participants = mutableMapOf<String, String?>()
@@ -77,7 +58,13 @@ class DatabaseAccess(
return@performOperation null
}
do {
- participants[query.getStringOrNull("client_conversation_id")!!] = query.getStringOrNull("user_id")!!
+ val conversationId = query.getStringOrNull("client_conversation_id") ?: continue
+ val userId = query.getStringOrNull("user_id") ?: continue
+ participants[conversationId] = when (query.getIntOrNull("conversation_type")) {
+ 0 -> userId
+ else -> null
+ }
+ participants[userId] = null
} while (query.moveToNext())
participants
}
@@ -89,13 +76,16 @@ class DatabaseAccess(
if (!dbPath.exists()) return null
return runCatching {
SQLiteDatabase.openDatabase(
- dbPath.absolutePath,
- null,
- SQLiteDatabase.OPEN_READONLY or SQLiteDatabase.NO_LOCALIZED_COLLATORS
+ dbPath,
+ OpenParams.Builder()
+ .setOpenFlags(SQLiteDatabase.OPEN_READONLY)
+ .setErrorHandler {
+ context.androidContext.deleteDatabase(dbPath.absolutePath)
+ context.softRestartApp()
+ }.build()
)
}.onFailure {
context.log.error("Failed to open database $fileName!", it)
- showDatabaseError(dbPath.absolutePath, it)
}.getOrNull()
}
@@ -137,6 +127,7 @@ class DatabaseAccess(
}
val myUserId by lazy {
+ context.androidContext.getSharedPreferences("user_session_shared_pref", 0).getString("key_user_id", null) ?:
arroyoDb?.performOperation {
safeRawQuery(buildString {
append("SELECT value FROM required_values WHERE key = 'USERID'")
@@ -146,7 +137,7 @@ class DatabaseAccess(
}
query.getStringOrNull("value")!!
}
- } ?: context.androidContext.getSharedPreferences("user_session_shared_pref", 0).getString("key_user_id", null)!!
+ }!!
}
fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? {
@@ -241,8 +232,8 @@ class DatabaseAccess(
participants.add(query.getStringOrNull("user_id")!!)
} while (query.moveToNext())
participants.firstOrNull { it != myUserId }
- }
- }.also { dmOtherParticipantCache[conversationId] = it }
+ }.also { dmOtherParticipantCache[conversationId] = it }
+ }
}
@@ -253,18 +244,28 @@ class DatabaseAccess(
}
fun getConversationParticipants(conversationId: String): List<String>? {
+ if (dmOtherParticipantCache[conversationId] != null) return dmOtherParticipantCache[conversationId]?.let { listOf(myUserId, it) }
return arroyoDb?.performOperation {
safeRawQuery(
- "SELECT user_id FROM user_conversation WHERE client_conversation_id = ?",
+ "SELECT user_id, conversation_type FROM user_conversation WHERE client_conversation_id = ?",
arrayOf(conversationId)
- )?.use {
- if (!it.moveToFirst()) {
+ )?.use { cursor ->
+ if (!cursor.moveToFirst()) {
return@performOperation null
}
val participants = mutableListOf<String>()
+ var conversationType = -1
do {
- participants.add(it.getStringOrNull("user_id")!!)
- } while (it.moveToNext())
+ if (conversationType == -1) conversationType = cursor.getInteger("conversation_type")
+ participants.add(cursor.getStringOrNull("user_id")!!)
+ } while (cursor.moveToNext())
+
+ if (!dmOtherParticipantCache.containsKey(conversationId)) {
+ dmOtherParticipantCache[conversationId] = when (conversationType) {
+ 0 -> participants.firstOrNull { it != myUserId }
+ else -> null
+ }
+ }
participants
}
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventBus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventBus.kt
@@ -18,11 +18,13 @@ class EventBus(
private val subscribers = mutableMapOf<KClass<out Event>, MutableMap<Int, IListener<out Event>>>()
fun <T : Event> subscribe(event: KClass<T>, listener: IListener<T>, priority: Int? = null) {
- if (!subscribers.containsKey(event)) {
- subscribers[event] = sortedMapOf()
+ synchronized(subscribers) {
+ if (!subscribers.containsKey(event)) {
+ subscribers[event] = sortedMapOf()
+ }
+ val lastSubscriber = subscribers[event]?.keys?.lastOrNull() ?: 0
+ subscribers[event]?.put(priority ?: (lastSubscriber + 1), listener)
}
- val lastSubscriber = subscribers[event]?.keys?.lastOrNull() ?: 0
- subscribers[event]?.put(priority ?: (lastSubscriber + 1), listener)
}
inline fun <T : Event> subscribe(event: KClass<T>, priority: Int? = null, crossinline listener: (T) -> Unit) = subscribe(event, { true }, priority, listener)
@@ -43,7 +45,9 @@ class EventBus(
}
fun <T : Event> unsubscribe(event: KClass<T>, listener: IListener<T>) {
- subscribers[event]?.values?.remove(listener)
+ synchronized(subscribers) {
+ subscribers[event]?.values?.remove(listener)
+ }
}
fun <T : Event> post(event: T, afterBlock: T.() -> Unit = {}): T? {
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/FriendFeedMessagePreview.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/FriendFeedMessagePreview.kt
@@ -1,6 +1,5 @@
package me.rhunk.snapenhance.core.features.impl.ui
-import android.annotation.SuppressLint
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
@@ -9,9 +8,14 @@ import android.graphics.drawable.shapes.Shape
import android.text.TextPaint
import android.view.View
import android.view.ViewGroup
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
+import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.impl.experiments.EndToEndEncryption
@@ -21,25 +25,65 @@ import me.rhunk.snapenhance.core.util.EvictingMap
import me.rhunk.snapenhance.core.util.ktx.getDimens
import me.rhunk.snapenhance.core.util.ktx.getId
import me.rhunk.snapenhance.core.util.ktx.getIdentifier
+import java.util.WeakHashMap
import kotlin.math.absoluteValue
-@SuppressLint("DiscouragedApi")
class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
+ private val endToEndEncryption by lazy { context.feature(EndToEndEncryption::class) }
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private val coroutineDispatcher = Dispatchers.IO.limitedParallelism(1)
+ private val setting get() = context.config.userInterface.friendFeedMessagePreview
+ private val hasE2EE get() = context.config.experimental.e2eEncryption.globalState == true
+
private val sigColorTextPrimary by lazy {
context.mainActivity!!.theme.obtainStyledAttributes(
intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr"))
).getColor(0, 0)
}
+ private val cachedLayouts = WeakHashMap<String, View>()
+ private val messageCache = EvictingMap<String, List<String>>(100)
private val friendNameCache = EvictingMap<String, String>(100)
+ private suspend fun fetchMessages(conversationId: String, callback: suspend () -> Unit) {
+ val messages = context.database.getMessagesFromConversationId(conversationId, setting.amount.get().absoluteValue)?.mapNotNull { message ->
+ val messageContainer =
+ message.messageContent
+ ?.let { ProtoReader(it) }
+ ?.followPath(4, 4)?.let { messageReader ->
+ takeIf { hasE2EE }?.let takeIf@{
+ endToEndEncryption.tryDecryptMessage(
+ senderId = message.senderId ?: return@takeIf null,
+ clientMessageId = message.clientMessageId.toLong(),
+ conversationId = message.clientConversationId ?: return@takeIf null,
+ contentType = ContentType.fromId(message.contentType),
+ messageBuffer = messageReader.getBuffer()
+ ).second
+ }?.let { ProtoReader(it) } ?: messageReader
+ }
+ ?: return@mapNotNull null
+
+ val messageString = messageContainer.getString(2, 1)
+ ?: ContentType.fromMessageContainer(messageContainer)?.name
+ ?: return@mapNotNull null
+
+ val friendName = friendNameCache.getOrPut(message.senderId ?: return@mapNotNull null) {
+ context.database.getFriendInfo(message.senderId ?: return@mapNotNull null)?.let { it.displayName?: it.mutableUsername } ?: "Unknown"
+ }
+ "$friendName: $messageString"
+ }?.takeIf { it.isNotEmpty() }?.reversed()
+
+ withContext(Dispatchers.Main) {
+ messages?.also { messageCache[conversationId] = it } ?: run {
+ messageCache.remove(conversationId)
+ }
+ callback()
+ }
+ }
+
override fun onActivityCreate() {
- val setting = context.config.userInterface.friendFeedMessagePreview
if (setting.globalState != true) return
- val hasE2EE = context.config.experimental.e2eEncryption.globalState == true
- val endToEndEncryption by lazy { context.feature(EndToEndEncryption::class) }
-
val ffItemId = context.resources.getId("ff_item")
val secondaryTextSize = context.resources.getDimens("ff_feed_cell_secondary_text_size").toFloat()
@@ -54,71 +98,65 @@ class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview", loadParams
textSize = secondaryTextSize
}
+ context.event.subscribe(BuildMessageEvent::class) { param ->
+ val conversationId = param.message.messageDescriptor?.conversationId?.toString() ?: return@subscribe
+ val cachedView = cachedLayouts[conversationId] ?: return@subscribe
+ context.coroutineScope.launch {
+ fetchMessages(conversationId) {
+ cachedView.postInvalidateDelayed(100L)
+ }
+ }
+ }
+
context.event.subscribe(BindViewEvent::class) { param ->
param.friendFeedItem { conversationId ->
val frameLayout = param.view as ViewGroup
val ffItem = frameLayout.findViewById<View>(ffItemId)
- ffItem.layoutParams = ffItem.layoutParams.apply {
- height = ViewGroup.LayoutParams.MATCH_PARENT
- }
- frameLayout.removeForegroundDrawable("ffItem")
-
- val stringMessages = context.database.getMessagesFromConversationId(conversationId, setting.amount.get().absoluteValue)?.mapNotNull { message ->
- val messageContainer =
- message.messageContent
- ?.let { ProtoReader(it) }
- ?.followPath(4, 4)?.let { messageReader ->
- takeIf { hasE2EE }?.let takeIf@{
- endToEndEncryption.tryDecryptMessage(
- senderId = message.senderId ?: return@takeIf null,
- clientMessageId = message.clientMessageId.toLong(),
- conversationId = message.clientConversationId ?: return@takeIf null,
- contentType = ContentType.fromId(message.contentType),
- messageBuffer = messageReader.getBuffer()
- ).second
- }?.let { ProtoReader(it) } ?: messageReader
- }
- ?: return@mapNotNull null
-
- val messageString = messageContainer.getString(2, 1)
- ?: ContentType.fromMessageContainer(messageContainer)?.name
- ?: return@mapNotNull null
-
- val friendName = friendNameCache.getOrPut(message.senderId ?: return@mapNotNull null) {
- context.database.getFriendInfo(message.senderId ?: return@mapNotNull null)?.let { it.displayName?: it.mutableUsername } ?: "Unknown"
+ context.coroutineScope.launch(coroutineDispatcher) {
+ withContext(Dispatchers.Main) {
+ cachedLayouts.remove(conversationId)
+ frameLayout.removeForegroundDrawable("ffItem")
}
- "$friendName: $messageString"
- }?.reversed() ?: return@friendFeedItem
-
- var maxTextHeight = 0
- val previewContainerHeight = stringMessages.sumOf { msg ->
- val rect = Rect()
- textPaint.getTextBounds(msg, 0, msg.length, rect)
- rect.height().also {
- if (it > maxTextHeight) maxTextHeight = it
- }.plus(separatorHeight)
- }
- ffItem.layoutParams = ffItem.layoutParams.apply {
- height = feedEntryHeight + previewContainerHeight + separatorHeight
- }
+ fetchMessages(conversationId) {
+ var maxTextHeight = 0
+ val previewContainerHeight = messageCache[conversationId]?.sumOf { msg ->
+ val rect = Rect()
+ textPaint.getTextBounds(msg, 0, msg.length, rect)
+ rect.height().also {
+ if (it > maxTextHeight) maxTextHeight = it
+ }.plus(separatorHeight)
+ } ?: run {
+ ffItem.layoutParams = ffItem.layoutParams.apply {
+ height = ViewGroup.LayoutParams.MATCH_PARENT
+ }
+ return@fetchMessages
+ }
- frameLayout.addForegroundDrawable("ffItem", ShapeDrawable(object: Shape() {
- override fun draw(canvas: Canvas, paint: Paint) {
- val offsetY = canvas.height.toFloat() - previewContainerHeight
-
- stringMessages.forEachIndexed { index, messageString ->
- paint.textSize = secondaryTextSize
- paint.color = sigColorTextPrimary
- canvas.drawText(messageString,
- feedEntryHeight + ffSdlPrimaryTextStartMargin,
- offsetY + index * maxTextHeight,
- paint
- )
+ ffItem.layoutParams = ffItem.layoutParams.apply {
+ height = feedEntryHeight + previewContainerHeight + separatorHeight
}
+
+ cachedLayouts[conversationId] = frameLayout
+
+ frameLayout.addForegroundDrawable("ffItem", ShapeDrawable(object: Shape() {
+ override fun draw(canvas: Canvas, paint: Paint) {
+ val offsetY = canvas.height.toFloat() - previewContainerHeight
+
+ messageCache[conversationId]?.forEachIndexed { index, messageString ->
+ paint.textSize = secondaryTextSize
+ paint.color = sigColorTextPrimary
+ canvas.drawText(messageString,
+ feedEntryHeight + ffSdlPrimaryTextStartMargin,
+ offsetY + index * maxTextHeight,
+ paint
+ )
+ }
+ }
+ }))
}
- }))
+ }
}
}
}