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 componentsHolder = context.resources.getIdentifier("components_holder", "id")
val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id") val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id")
val contextMenuButtonIconView = context.resources.getIdentifier("context_menu_button_icon_view", "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 -> context.event.subscribe(AddViewEvent::class) { event ->
val originalAddView: (View) -> Unit = { val originalAddView: (View) -> Unit = {
@ -75,6 +76,16 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
return@subscribe 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()) { if (childView.javaClass.name.endsWith("ChatActionMenuComponent") && context.config.experimental.newChatActionMenu.get()) {
(menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).handle(event) (menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).handle(event)
return@subscribe return@subscribe

View File

@ -1,56 +1,28 @@
package me.rhunk.snapenhance.core.ui.menu.impl 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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import android.widget.Button import android.widget.Button
import android.widget.LinearLayout 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.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.experiments.ConvertMessageLocally
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger 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.ViewTagState
import me.rhunk.snapenhance.core.ui.applyTheme 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.menu.AbstractMenu
import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.ktx.getDimens import me.rhunk.snapenhance.core.util.ktx.getDimens
import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress 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() { class ChatActionMenu : AbstractMenu() {
private val viewTagState = ViewTagState() private val viewTagState = ViewTagState()
private val defaultGap by lazy { context.resources.getDimens("default_gap") } 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 chatActionMenuItemMargin by lazy { context.resources.getDimens("chat_action_menu_item_margin") }
private val actionMenuItemHeight by lazy { context.resources.getDimens("action_menu_item_height") } private val actionMenuItemHeight by lazy { context.resources.getDimens("action_menu_item_height") }
private fun createContainer(viewGroup: ViewGroup): LinearLayout { 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() { override fun init() {
runCatching { runCatching {
if (!context.config.downloader.downloadContextMenu.get() && context.config.messaging.messageLogger.globalState != true && !context.isDeveloper) return 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") context.androidContext.classLoader.loadClass("com.snap.messaging.chat.features.actionmenu.ActionMenuChatItemContainer")
.hook("onMeasure", HookStage.BEFORE) { param -> .hook("onMeasure", HookStage.BEFORE) { param ->
param.setArg(1, 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 { }.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) { override fun inject(parent: ViewGroup, view: View, viewConsumer: (View) -> Unit) {
val viewGroup = parent.parent.parent as? ViewGroup ?: return val viewGroup = parent.parent.parent as? ViewGroup ?: return
if (viewTagState[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) viewGroup.addView(buttonContainer)
} }

View File

@ -1,5 +1,7 @@
package me.rhunk.snapenhance.core.ui.menu.impl package me.rhunk.snapenhance.core.ui.menu.impl
import android.content.Context
import android.text.format.Formatter
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ScrollView 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.filled.RemoveRedEye
import androidx.compose.material.icons.outlined.Image import androidx.compose.material.icons.outlined.Image
import androidx.compose.material.icons.rounded.BookmarkRemove 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.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text 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.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp 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.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.event.events.impl.AddViewEvent
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader 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.experiments.ConvertMessageLocally
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger 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.iterateParent
import me.rhunk.snapenhance.core.ui.menu.AbstractMenu import me.rhunk.snapenhance.core.ui.menu.AbstractMenu
import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent
import me.rhunk.snapenhance.core.util.ktx.isDarkTheme import me.rhunk.snapenhance.core.util.ktx.isDarkTheme
import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField
import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress 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() { 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) { fun handle(event: AddViewEvent) {
if (event.parent is LinearLayout) return if (event.parent is LinearLayout) return
val closeActionMenu = { event.parent.iterateParent { val closeActionMenu = { event.parent.iterateParent {
@ -149,9 +292,10 @@ class NewChatActionMenu : AbstractMenu() {
addView(composeView) addView(composeView)
composeView.post { composeView.post {
(event.parent.layoutParams as ViewGroup.MarginLayoutParams).apply { (event.parent.layoutParams as ViewGroup.MarginLayoutParams).apply {
setObjectField("a", null) // remove drag callback
if (height < composeView.measuredHeight) { if (height < composeView.measuredHeight) {
height += composeView.measuredHeight height += composeView.measuredHeight
} else {
setObjectField("a", null) // remove drag callback
} }
} }
event.parent.requestLayout() event.parent.requestLayout()