feat(ui/new_chat_action_menu): debug view

This commit is contained in:
rhunk 2024-03-04 00:03:43 +01:00
parent 7619cc0b8e
commit 72cfd7a8bc
3 changed files with 157 additions and 150 deletions

View File

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

View File

@ -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)
}

View File

@ -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()