mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-24 18:42:09 +02:00
perf(core): database access
This commit is contained in:
parent
da8561cddb
commit
96183921dc
@ -2,6 +2,7 @@ package me.rhunk.snapenhance.core.database
|
|||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import android.database.sqlite.SQLiteDatabase.OpenParams
|
||||||
import android.database.sqlite.SQLiteDatabaseCorruptException
|
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
|
||||||
@ -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.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.getIntOrNull
|
||||||
import me.rhunk.snapenhance.common.util.ktx.getInteger
|
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.manager.Manager
|
import me.rhunk.snapenhance.core.manager.Manager
|
||||||
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseAccess(
|
class DatabaseAccess(
|
||||||
@ -25,51 +25,32 @@ class DatabaseAccess(
|
|||||||
|
|
||||||
private inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? {
|
private inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
query()
|
synchronized(this) {
|
||||||
|
query()
|
||||||
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
context.log.error("Database operation failed", it)
|
context.log.error("Database operation failed", it)
|
||||||
}.getOrNull()
|
}.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? {
|
private fun SQLiteDatabase.safeRawQuery(query: String, args: Array<String>? = null): Cursor? {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
rawQuery(query, args)
|
rawQuery(query, args)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
if (it !is SQLiteDatabaseCorruptException) {
|
if (it !is SQLiteDatabaseCorruptException) {
|
||||||
context.log.error("Failed to execute query $query", it)
|
context.log.error("Failed to execute query $query", it)
|
||||||
showDatabaseError(this.path, it)
|
|
||||||
return@onFailure
|
return@onFailure
|
||||||
}
|
}
|
||||||
context.log.warn("Database ${this.path} is corrupted!")
|
context.longToast("Database ${this.path} is corrupted! Restarting ...")
|
||||||
context.androidContext.deleteDatabase(this.path)
|
context.androidContext.deleteDatabase(this.path)
|
||||||
showDatabaseError(this.path, it)
|
context.crash("Database ${this.path} is corrupted!", it)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val dmOtherParticipantCache by lazy {
|
private val dmOtherParticipantCache by lazy {
|
||||||
(arroyoDb?.performOperation {
|
(arroyoDb?.performOperation {
|
||||||
safeRawQuery(
|
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)
|
arrayOf(myUserId)
|
||||||
)?.use { query ->
|
)?.use { query ->
|
||||||
val participants = mutableMapOf<String, String?>()
|
val participants = mutableMapOf<String, String?>()
|
||||||
@ -77,7 +58,13 @@ class DatabaseAccess(
|
|||||||
return@performOperation null
|
return@performOperation null
|
||||||
}
|
}
|
||||||
do {
|
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())
|
} while (query.moveToNext())
|
||||||
participants
|
participants
|
||||||
}
|
}
|
||||||
@ -89,13 +76,16 @@ class DatabaseAccess(
|
|||||||
if (!dbPath.exists()) return null
|
if (!dbPath.exists()) return null
|
||||||
return runCatching {
|
return runCatching {
|
||||||
SQLiteDatabase.openDatabase(
|
SQLiteDatabase.openDatabase(
|
||||||
dbPath.absolutePath,
|
dbPath,
|
||||||
null,
|
OpenParams.Builder()
|
||||||
SQLiteDatabase.OPEN_READONLY or SQLiteDatabase.NO_LOCALIZED_COLLATORS
|
.setOpenFlags(SQLiteDatabase.OPEN_READONLY)
|
||||||
|
.setErrorHandler {
|
||||||
|
context.androidContext.deleteDatabase(dbPath.absolutePath)
|
||||||
|
context.softRestartApp()
|
||||||
|
}.build()
|
||||||
)
|
)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
context.log.error("Failed to open database $fileName!", it)
|
context.log.error("Failed to open database $fileName!", it)
|
||||||
showDatabaseError(dbPath.absolutePath, it)
|
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +127,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val myUserId by lazy {
|
val myUserId by lazy {
|
||||||
|
context.androidContext.getSharedPreferences("user_session_shared_pref", 0).getString("key_user_id", null) ?:
|
||||||
arroyoDb?.performOperation {
|
arroyoDb?.performOperation {
|
||||||
safeRawQuery(buildString {
|
safeRawQuery(buildString {
|
||||||
append("SELECT value FROM required_values WHERE key = 'USERID'")
|
append("SELECT value FROM required_values WHERE key = 'USERID'")
|
||||||
@ -146,7 +137,7 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
query.getStringOrNull("value")!!
|
query.getStringOrNull("value")!!
|
||||||
}
|
}
|
||||||
} ?: context.androidContext.getSharedPreferences("user_session_shared_pref", 0).getString("key_user_id", null)!!
|
}!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? {
|
fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? {
|
||||||
@ -241,8 +232,8 @@ class DatabaseAccess(
|
|||||||
participants.add(query.getStringOrNull("user_id")!!)
|
participants.add(query.getStringOrNull("user_id")!!)
|
||||||
} while (query.moveToNext())
|
} while (query.moveToNext())
|
||||||
participants.firstOrNull { it != myUserId }
|
participants.firstOrNull { it != myUserId }
|
||||||
}
|
}.also { dmOtherParticipantCache[conversationId] = it }
|
||||||
}.also { dmOtherParticipantCache[conversationId] = it }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -253,18 +244,28 @@ class DatabaseAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getConversationParticipants(conversationId: String): List<String>? {
|
fun getConversationParticipants(conversationId: String): List<String>? {
|
||||||
|
if (dmOtherParticipantCache[conversationId] != null) return dmOtherParticipantCache[conversationId]?.let { listOf(myUserId, it) }
|
||||||
return arroyoDb?.performOperation {
|
return arroyoDb?.performOperation {
|
||||||
safeRawQuery(
|
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)
|
arrayOf(conversationId)
|
||||||
)?.use {
|
)?.use { cursor ->
|
||||||
if (!it.moveToFirst()) {
|
if (!cursor.moveToFirst()) {
|
||||||
return@performOperation null
|
return@performOperation null
|
||||||
}
|
}
|
||||||
val participants = mutableListOf<String>()
|
val participants = mutableListOf<String>()
|
||||||
|
var conversationType = -1
|
||||||
do {
|
do {
|
||||||
participants.add(it.getStringOrNull("user_id")!!)
|
if (conversationType == -1) conversationType = cursor.getInteger("conversation_type")
|
||||||
} while (it.moveToNext())
|
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
|
participants
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,13 @@ class EventBus(
|
|||||||
private val subscribers = mutableMapOf<KClass<out Event>, MutableMap<Int, IListener<out Event>>>()
|
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) {
|
fun <T : Event> subscribe(event: KClass<T>, listener: IListener<T>, priority: Int? = null) {
|
||||||
if (!subscribers.containsKey(event)) {
|
synchronized(subscribers) {
|
||||||
subscribers[event] = sortedMapOf()
|
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)
|
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>) {
|
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? {
|
fun <T : Event> post(event: T, afterBlock: T.() -> Unit = {}): T? {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package me.rhunk.snapenhance.core.features.impl.ui
|
package me.rhunk.snapenhance.core.features.impl.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
@ -9,9 +8,14 @@ import android.graphics.drawable.shapes.Shape
|
|||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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.data.ContentType
|
||||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
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.BindViewEvent
|
||||||
|
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
|
||||||
import me.rhunk.snapenhance.core.features.Feature
|
import me.rhunk.snapenhance.core.features.Feature
|
||||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||||
import me.rhunk.snapenhance.core.features.impl.experiments.EndToEndEncryption
|
import me.rhunk.snapenhance.core.features.impl.experiments.EndToEndEncryption
|
||||||
@ -21,24 +25,64 @@ import me.rhunk.snapenhance.core.util.EvictingMap
|
|||||||
import me.rhunk.snapenhance.core.util.ktx.getDimens
|
import me.rhunk.snapenhance.core.util.ktx.getDimens
|
||||||
import me.rhunk.snapenhance.core.util.ktx.getId
|
import me.rhunk.snapenhance.core.util.ktx.getId
|
||||||
import me.rhunk.snapenhance.core.util.ktx.getIdentifier
|
import me.rhunk.snapenhance.core.util.ktx.getIdentifier
|
||||||
|
import java.util.WeakHashMap
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
@SuppressLint("DiscouragedApi")
|
|
||||||
class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
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 {
|
private val sigColorTextPrimary by lazy {
|
||||||
context.mainActivity!!.theme.obtainStyledAttributes(
|
context.mainActivity!!.theme.obtainStyledAttributes(
|
||||||
intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr"))
|
intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr"))
|
||||||
).getColor(0, 0)
|
).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 val friendNameCache = EvictingMap<String, String>(100)
|
||||||
|
|
||||||
override fun onActivityCreate() {
|
private suspend fun fetchMessages(conversationId: String, callback: suspend () -> Unit) {
|
||||||
val setting = context.config.userInterface.friendFeedMessagePreview
|
val messages = context.database.getMessagesFromConversationId(conversationId, setting.amount.get().absoluteValue)?.mapNotNull { message ->
|
||||||
if (setting.globalState != true) return
|
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 hasE2EE = context.config.experimental.e2eEncryption.globalState == true
|
val messageString = messageContainer.getString(2, 1)
|
||||||
val endToEndEncryption by lazy { context.feature(EndToEndEncryption::class) }
|
?: 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() {
|
||||||
|
if (setting.globalState != true) return
|
||||||
|
|
||||||
val ffItemId = context.resources.getId("ff_item")
|
val ffItemId = context.resources.getId("ff_item")
|
||||||
|
|
||||||
@ -54,71 +98,65 @@ class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview", loadParams
|
|||||||
textSize = secondaryTextSize
|
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 ->
|
context.event.subscribe(BindViewEvent::class) { param ->
|
||||||
param.friendFeedItem { conversationId ->
|
param.friendFeedItem { conversationId ->
|
||||||
val frameLayout = param.view as ViewGroup
|
val frameLayout = param.view as ViewGroup
|
||||||
val ffItem = frameLayout.findViewById<View>(ffItemId)
|
val ffItem = frameLayout.findViewById<View>(ffItemId)
|
||||||
|
|
||||||
ffItem.layoutParams = ffItem.layoutParams.apply {
|
context.coroutineScope.launch(coroutineDispatcher) {
|
||||||
height = ViewGroup.LayoutParams.MATCH_PARENT
|
withContext(Dispatchers.Main) {
|
||||||
}
|
cachedLayouts.remove(conversationId)
|
||||||
frameLayout.removeForegroundDrawable("ffItem")
|
frameLayout.removeForegroundDrawable("ffItem")
|
||||||
|
}
|
||||||
|
|
||||||
val stringMessages = context.database.getMessagesFromConversationId(conversationId, setting.amount.get().absoluteValue)?.mapNotNull { message ->
|
fetchMessages(conversationId) {
|
||||||
val messageContainer =
|
var maxTextHeight = 0
|
||||||
message.messageContent
|
val previewContainerHeight = messageCache[conversationId]?.sumOf { msg ->
|
||||||
?.let { ProtoReader(it) }
|
val rect = Rect()
|
||||||
?.followPath(4, 4)?.let { messageReader ->
|
textPaint.getTextBounds(msg, 0, msg.length, rect)
|
||||||
takeIf { hasE2EE }?.let takeIf@{
|
rect.height().also {
|
||||||
endToEndEncryption.tryDecryptMessage(
|
if (it > maxTextHeight) maxTextHeight = it
|
||||||
senderId = message.senderId ?: return@takeIf null,
|
}.plus(separatorHeight)
|
||||||
clientMessageId = message.clientMessageId.toLong(),
|
} ?: run {
|
||||||
conversationId = message.clientConversationId ?: return@takeIf null,
|
ffItem.layoutParams = ffItem.layoutParams.apply {
|
||||||
contentType = ContentType.fromId(message.contentType),
|
height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
messageBuffer = messageReader.getBuffer()
|
|
||||||
).second
|
|
||||||
}?.let { ProtoReader(it) } ?: messageReader
|
|
||||||
}
|
}
|
||||||
?: return@mapNotNull null
|
return@fetchMessages
|
||||||
|
|
||||||
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"
|
|
||||||
}?.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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user