commit 72cfd7a8bce2cd3c52764af99e8377be7cca77ec
parent 7619cc0b8eb752bbd5e51f9a70c184ee9bd1933b
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Mon,  4 Mar 2024 00:03:43 +0100

feat(ui/new_chat_action_menu): debug view

Diffstat:
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt | 11+++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt | 150+------------------------------------------------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt | 146++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 157 insertions(+), 150 deletions(-)

diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt @@ -40,6 +40,7 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar val componentsHolder = context.resources.getIdentifier("components_holder", "id") val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id") val contextMenuButtonIconView = context.resources.getIdentifier("context_menu_button_icon_view", "id") + val chatActionMenu = context.resources.getIdentifier("chat_action_menu", "id") context.event.subscribe(AddViewEvent::class) { event -> val originalAddView: (View) -> Unit = { @@ -75,6 +76,16 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar return@subscribe } + if (viewGroup !is LinearLayout && childView.id == chatActionMenu && context.config.experimental.newChatActionMenu.get() && context.isDeveloper) { + event.view = LinearLayout(childView.context).apply { + orientation = LinearLayout.VERTICAL + addView( + (menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).createDebugInfoView(childView.context) + ) + addView(event.view) + } + } + if (childView.javaClass.name.endsWith("ChatActionMenuComponent") && context.config.experimental.newChatActionMenu.get()) { (menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).handle(event) return@subscribe diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt @@ -1,56 +1,28 @@ package me.rhunk.snapenhance.core.ui.menu.impl -import android.annotation.SuppressLint -import android.content.Context -import android.text.format.Formatter import android.view.View import android.view.ViewGroup import android.view.ViewGroup.MarginLayoutParams import android.widget.Button import android.widget.LinearLayout -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import me.rhunk.snapenhance.common.data.ContentType -import me.rhunk.snapenhance.common.ui.createComposeView -import me.rhunk.snapenhance.common.util.ktx.copyToClipboard -import me.rhunk.snapenhance.common.util.protobuf.ProtoReader -import me.rhunk.snapenhance.common.util.snap.RemoteMediaResolver import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader -import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder import me.rhunk.snapenhance.core.features.impl.experiments.ConvertMessageLocally import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger -import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.ui.ViewTagState import me.rhunk.snapenhance.core.ui.applyTheme -import me.rhunk.snapenhance.core.ui.debugEditText import me.rhunk.snapenhance.core.ui.menu.AbstractMenu import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getDimens import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress -import java.text.SimpleDateFormat -import java.util.Date -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi -@SuppressLint("DiscouragedApi") class ChatActionMenu : AbstractMenu() { private val viewTagState = ViewTagState() - private val defaultGap by lazy { context.resources.getDimens("default_gap") } - private val chatActionMenuItemMargin by lazy { context.resources.getDimens("chat_action_menu_item_margin") } - private val actionMenuItemHeight by lazy { context.resources.getDimens("action_menu_item_height") } private fun createContainer(viewGroup: ViewGroup): LinearLayout { @@ -68,29 +40,13 @@ class ChatActionMenu : AbstractMenu() { } } - private fun debugAlertDialog(context: Context, title: String, text: String) { - this@ChatActionMenu.context.runOnUiThread { - ViewAppearanceHelper.newAlertDialogBuilder(context).apply { - setTitle(title) - setView(debugEditText(context, text)) - setPositiveButton("OK") { dialog, _ -> dialog.dismiss() } - setNegativeButton("Copy") { _, _ -> - context.copyToClipboard(text, title) - } - }.show() - } - } - - private val lastFocusedMessage - get() = context.database.getConversationMessageFromId(context.feature(Messaging::class).lastFocusedMessageId) - override fun init() { runCatching { if (!context.config.downloader.downloadContextMenu.get() && context.config.messaging.messageLogger.globalState != true && !context.isDeveloper) return context.androidContext.classLoader.loadClass("com.snap.messaging.chat.features.actionmenu.ActionMenuChatItemContainer") .hook("onMeasure", HookStage.BEFORE) { param -> param.setArg(1, - View.MeasureSpec.makeMeasureSpec((context.resources.displayMetrics.heightPixels * 0.35).toInt(), View.MeasureSpec.AT_MOST) + View.MeasureSpec.makeMeasureSpec((context.resources.displayMetrics.heightPixels * 0.25).toInt(), View.MeasureSpec.AT_MOST) ) } }.onFailure { @@ -98,8 +54,6 @@ class ChatActionMenu : AbstractMenu() { } } - @OptIn(ExperimentalLayoutApi::class, ExperimentalEncodingApi::class) - @SuppressLint("SetTextI18n", "DiscouragedApi", "ClickableViewAccessibility") override fun inject(parent: ViewGroup, view: View, viewConsumer: (View) -> Unit) { val viewGroup = parent.parent.parent as? ViewGroup ?: return if (viewTagState[viewGroup]) return @@ -202,108 +156,6 @@ class ChatActionMenu : AbstractMenu() { }) } - if (context.isDeveloper) { - val composeDebugView = createComposeView(viewGroup.context) { - FlowRow( - modifier = Modifier.fillMaxWidth().padding(5.dp), - horizontalArrangement = Arrangement.spacedBy(3.dp) - ) { - Button(onClick = { - val arroyoMessage = lastFocusedMessage ?: return@Button - debugAlertDialog(viewGroup.context, - "Message Info", - StringBuilder().apply { - runCatching { - append("conversation_id: ${arroyoMessage.clientConversationId}\n") - append("sender_id: ${arroyoMessage.senderId}\n") - append("client_id: ${arroyoMessage.clientMessageId}, server_id: ${arroyoMessage.serverMessageId}\n") - append("content_type: ${ContentType.fromId(arroyoMessage.contentType)} (${arroyoMessage.contentType})\n") - append("parsed_content_type: ${ - ContentType.fromMessageContainer( - ProtoReader(arroyoMessage.messageContent!!).followPath(4, 4) - ).let { "$it (${it?.id})" }}\n") - append("creation_timestamp: ${ - SimpleDateFormat.getDateTimeInstance().format( - Date(arroyoMessage.creationTimestamp) - )} (${arroyoMessage.creationTimestamp})\n") - append("read_timestamp: ${SimpleDateFormat.getDateTimeInstance().format( - Date(arroyoMessage.readTimestamp) - )} (${arroyoMessage.readTimestamp})\n") - append("ml_deleted: ${messageLogger.isMessageDeleted(arroyoMessage.clientConversationId!!, arroyoMessage.clientMessageId.toLong())}, ") - append("ml_stored: ${messageLogger.getMessageObject(arroyoMessage.clientConversationId!!, arroyoMessage.clientMessageId.toLong()) != null}\n") - } - }.toString() - ) - }) { - Text("Info") - } - Button(onClick = { - val arroyoMessage = lastFocusedMessage ?: return@Button - messaging.conversationManager?.fetchMessage(arroyoMessage.clientConversationId!!, arroyoMessage.clientMessageId.toLong(), onSuccess = { message -> - val decodedAttachments = MessageDecoder.decode(message.messageContent!!) - - debugAlertDialog( - viewGroup.context, - "Media References", - decodedAttachments.mapIndexed { index, attachment -> - StringBuilder().apply { - append("---- media $index ----\n") - append("resolveProto: ${attachment.mediaUrlKey}\n") - append("type: ${attachment.type}\n") - attachment.attachmentInfo?.apply { - encryption?.let { - append("encryption:\n - key: ${it.key}\n - iv: ${it.iv}\n") - } - resolution?.let { - append("resolution: ${it.first}x${it.second}\n") - } - duration?.let { - append("duration: $it\n") - } - } - runCatching { - val mediaHeaders = RemoteMediaResolver.getMediaHeaders(Base64.UrlSafe.decode(attachment.mediaUrlKey ?: return@runCatching)) - append("content-type: ${mediaHeaders["content-type"]}\n") - append("content-length: ${Formatter.formatShortFileSize(context.androidContext, mediaHeaders["content-length"]?.toLongOrNull() ?: 0)}\n") - append("creation-date: ${mediaHeaders["last-modified"]}\n") - } - }.toString() - }.joinToString("\n\n") - ) - }) - }) { - Text("Refs") - } - Button(onClick = { - val message = lastFocusedMessage ?: return@Button - debugAlertDialog( - viewGroup.context, - "Arroyo proto", - message.messageContent?.let { ProtoReader(it) }?.toString() ?: "empty" - ) - }) { - Text("Arroyo proto") - } - Button(onClick = { - val arroyoMessage = lastFocusedMessage ?: return@Button - messaging.conversationManager?.fetchMessage(arroyoMessage.clientConversationId!!, arroyoMessage.clientMessageId.toLong(), onSuccess = { message -> - debugAlertDialog( - viewGroup.context, - "Message proto", - message.messageContent?.content?.let { ProtoReader(it) }?.toString() ?: "empty" - ) - }, onError = { - this@ChatActionMenu.context.shortToast("Failed to fetch message: $it") - }) - }) { - Text("Message proto") - } - } - } - viewGroup.addView(createContainer(viewGroup).apply { - addView(composeDebugView) - }) - } viewGroup.addView(buttonContainer) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt @@ -1,5 +1,7 @@ package me.rhunk.snapenhance.core.ui.menu.impl +import android.content.Context +import android.text.format.Formatter import android.view.ViewGroup import android.widget.LinearLayout import android.widget.ScrollView @@ -12,6 +14,8 @@ import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.RemoveRedEye import androidx.compose.material.icons.outlined.Image import androidx.compose.material.icons.rounded.BookmarkRemove +import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -21,21 +25,160 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.unit.dp +import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.ui.createComposeView +import me.rhunk.snapenhance.common.util.ktx.copyToClipboard +import me.rhunk.snapenhance.common.util.protobuf.ProtoReader +import me.rhunk.snapenhance.common.util.snap.RemoteMediaResolver import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader +import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder import me.rhunk.snapenhance.core.features.impl.experiments.ConvertMessageLocally import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger +import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper +import me.rhunk.snapenhance.core.ui.debugEditText import me.rhunk.snapenhance.core.ui.iterateParent import me.rhunk.snapenhance.core.ui.menu.AbstractMenu import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent import me.rhunk.snapenhance.core.util.ktx.isDarkTheme import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress +import java.text.SimpleDateFormat +import java.util.Date +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi class NewChatActionMenu : AbstractMenu() { + private fun debugAlertDialog(context: Context, title: String, text: String) { + this@NewChatActionMenu.context.runOnUiThread { + ViewAppearanceHelper.newAlertDialogBuilder(context).apply { + setTitle(title) + setView(debugEditText(context, text)) + setPositiveButton("OK") { dialog, _ -> dialog.dismiss() } + setNegativeButton("Copy") { _, _ -> + context.copyToClipboard(text, title) + } + }.show() + } + } + + private val lastFocusedMessage + get() = context.database.getConversationMessageFromId(context.feature(Messaging::class).lastFocusedMessageId) + + @OptIn(ExperimentalLayoutApi::class, ExperimentalEncodingApi::class) + fun createDebugInfoView(context: Context): ComposeView { + val messageLogger = this@NewChatActionMenu.context.feature(MessageLogger::class) + val messaging = this@NewChatActionMenu.context.feature(Messaging::class) + + return createComposeView(context) { + Card( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 0.dp, bottom = 0.dp) + ) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(5.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Button(onClick = { + val arroyoMessage = lastFocusedMessage ?: return@Button + debugAlertDialog(context, + "Message Info", + StringBuilder().apply { + runCatching { + append("conversation_id: ${arroyoMessage.clientConversationId}\n") + append("sender_id: ${arroyoMessage.senderId}\n") + append("client_id: ${arroyoMessage.clientMessageId}, server_id: ${arroyoMessage.serverMessageId}\n") + append("content_type: ${ContentType.fromId(arroyoMessage.contentType)} (${arroyoMessage.contentType})\n") + append("parsed_content_type: ${ + ContentType.fromMessageContainer( + ProtoReader(arroyoMessage.messageContent!!).followPath(4, 4) + ).let { "$it (${it?.id})" }}\n") + append("creation_timestamp: ${ + SimpleDateFormat.getDateTimeInstance().format( + Date(arroyoMessage.creationTimestamp) + )} (${arroyoMessage.creationTimestamp})\n") + append("read_timestamp: ${ + SimpleDateFormat.getDateTimeInstance().format( + Date(arroyoMessage.readTimestamp) + )} (${arroyoMessage.readTimestamp})\n") + append("ml_deleted: ${messageLogger.isMessageDeleted(arroyoMessage.clientConversationId!!, arroyoMessage.clientMessageId.toLong())}, ") + append("ml_stored: ${messageLogger.getMessageObject(arroyoMessage.clientConversationId!!, arroyoMessage.clientMessageId.toLong()) != null}\n") + } + }.toString() + ) + }) { + Text("Info") + } + Button(onClick = { + val arroyoMessage = lastFocusedMessage ?: return@Button + messaging.conversationManager?.fetchMessage(arroyoMessage.clientConversationId!!, arroyoMessage.clientMessageId.toLong(), onSuccess = { message -> + val decodedAttachments = MessageDecoder.decode(message.messageContent!!) + debugAlertDialog( + context, + "Media References", + decodedAttachments.mapIndexed { index, attachment -> + StringBuilder().apply { + append("---- media $index ----\n") + append("resolveProto: ${attachment.mediaUrlKey}\n") + append("type: ${attachment.type}\n") + attachment.attachmentInfo?.apply { + encryption?.let { + append("encryption:\n - key: ${it.key}\n - iv: ${it.iv}\n") + } + resolution?.let { + append("resolution: ${it.first}x${it.second}\n") + } + duration?.let { + append("duration: $it\n") + } + } + runCatching { + val mediaHeaders = RemoteMediaResolver.getMediaHeaders( + Base64.UrlSafe.decode(attachment.mediaUrlKey ?: return@runCatching)) + append("content-type: ${mediaHeaders["content-type"]}\n") + append("content-length: ${Formatter.formatShortFileSize(context, mediaHeaders["content-length"]?.toLongOrNull() ?: 0)}\n") + append("creation-date: ${mediaHeaders["last-modified"]}\n") + } + }.toString() + }.joinToString("\n\n") + ) + }) + }) { + Text("Refs") + } + Button(onClick = { + val message = lastFocusedMessage ?: return@Button + debugAlertDialog( + context, + "Arroyo proto", + message.messageContent?.let { ProtoReader(it) }?.toString() ?: "empty" + ) + }) { + Text("Arroyo") + } + Button(onClick = { + val arroyoMessage = lastFocusedMessage ?: return@Button + messaging.conversationManager?.fetchMessage(arroyoMessage.clientConversationId!!, arroyoMessage.clientMessageId.toLong(), onSuccess = { message -> + debugAlertDialog( + context, + "Message proto", + message.messageContent?.content?.let { ProtoReader(it) }?.toString() ?: "empty" + ) + }, onError = { + this@NewChatActionMenu.context.shortToast("Failed to fetch message: $it") + }) + }) { + Text("Message") + } + } + } + } + } + fun handle(event: AddViewEvent) { if (event.parent is LinearLayout) return val closeActionMenu = { event.parent.iterateParent { @@ -149,9 +292,10 @@ class NewChatActionMenu : AbstractMenu() { addView(composeView) composeView.post { (event.parent.layoutParams as ViewGroup.MarginLayoutParams).apply { - setObjectField("a", null) // remove drag callback if (height < composeView.measuredHeight) { height += composeView.measuredHeight + } else { + setObjectField("a", null) // remove drag callback } } event.parent.requestLayout()