From 1ac1542384f2d597acd84fc3560d4e0c375ea265 Mon Sep 17 00:00:00 2001 From: RSR/ <52901789+RSR1337@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:31:42 +0400 Subject: [PATCH] fix MediaFilePicker on snap13.79 and added mapper --- .../impl/experiments/MediaFilePicker.kt | 76 ++++++++++++------- gradle.properties | 3 +- .../purrfectsnap/mapper/ClassMapper.kt | 1 + .../mapper/impl/ChatMediaDrawerMapper.kt | 48 ++++++++++++ 4 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/impl/ChatMediaDrawerMapper.kt diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt index ba6fed6c..0168f483 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt @@ -48,6 +48,7 @@ import me.eternal.purrfectsnap.core.ui.ViewAppearanceHelper import me.eternal.purrfectsnap.core.util.dataBuilder import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.hook +import me.eternal.purrfectsnap.mapper.impl.ChatMediaDrawerMapper import java.io.InputStream import java.lang.reflect.Method import kotlin.random.Random @@ -62,14 +63,27 @@ class MediaFilePicker : Feature("Media File Picker") { onNextActivityCreate(defer = true) { lateinit var chatMediaDrawerActionHandler: Any - lateinit var sendItemsMethod: Method + var sendItemsMethod: Method? = null + var drawerViewClass: Class<*>? = null + var sendItemsListItemClassFallback: Class<*>? = null - findClass("com.snap.composer.memories.ChatMediaDrawer").genericSuperclass?.getTypeArguments()?.getOrNull(1)?.apply { - methods.first { - it.parameterTypes.size == 1 && it.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler") - }.also { method -> - sendItemsMethod = method.parameterTypes[0].methods.first { it.name == "sendItems" } - }.hook(HookStage.AFTER) { + context.mappings.useMapper(ChatMediaDrawerMapper::class) { + val drawerCls = chatMediaDrawerClass.getAsClass() ?: return@useMapper + val actionHandlerCls = actionHandlerClass.getAsClass() ?: return@useMapper + val sendItemsName = sendItemsMethodName.getAsString() ?: "sendItems" + drawerViewClass = drawerCls + sendItemsListItemClassFallback = sendItemsListItemClass.getAsClass() + + val contextType = drawerCls.genericSuperclass?.getTypeArguments()?.getOrNull(1) ?: return@useMapper + val handlerParamMethod = contextType.methods.firstOrNull { method -> + method.parameterTypes.size == 1 && ( + method.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler") || + actionHandlerCls.isAssignableFrom(method.parameterTypes[0]) + ) + } ?: return@useMapper + val sendItems = handlerParamMethod.parameterTypes[0].methods.firstOrNull { it.name == sendItemsName } ?: return@useMapper + sendItemsMethod = sendItems + handlerParamMethod.hook(HookStage.AFTER) { chatMediaDrawerActionHandler = it.arg(0) } } @@ -102,7 +116,7 @@ class MediaFilePicker : Feature("Media File Picker") { } context.event.subscribe(ActivityResultEvent::class) { event -> - if (event.requestCode != requestCode || event.resultCode != Activity.RESULT_OK) return@subscribe + if (sendItemsMethod == null || event.requestCode != requestCode || event.resultCode != Activity.RESULT_OK) return@subscribe requestCode = null firstVideoId = context.androidContext.contentResolver.query( @@ -128,25 +142,35 @@ class MediaFilePicker : Feature("Media File Picker") { } fun sendMedia() { - sendItemsMethod.invoke(chatMediaDrawerActionHandler, listOf(), listOf( - sendItemsMethod.genericParameterTypes[1].getTypeArguments().first().dataBuilder { - from("_item") { - set("_cameraRollSource", "Snapchat") - set("_contentUri", "") - set("_durationMs", 0.0) - set("_disabled", false) - set("_imageRotation", 0.0) - set("_width", 1080.0) - set("_height", 1920.0) - set("_timestampMs", System.currentTimeMillis().toDouble()) - from("_itemId") { - set("_itemId", firstVideoId.toString()) - set("_type", "VIDEO") - } + val method = sendItemsMethod ?: return + val itemClass = method.genericParameterTypes.getOrNull(1)?.getTypeArguments()?.firstOrNull() + ?: sendItemsListItemClassFallback + if (itemClass == null) { + context.log.warn("MediaFilePicker: sendItems second parameter type has no generic info (type erasure). genericParameterTypes[1]=${method.genericParameterTypes.getOrNull(1)}") + context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to send media (incompatible version).") + return + } + val item = itemClass.dataBuilder { + from("_item") { + set("_cameraRollSource", "Snapchat") + set("_contentUri", "") + set("_durationMs", 0.0) + set("_disabled", false) + set("_imageRotation", 0.0) + set("_width", 1080.0) + set("_height", 1920.0) + set("_timestampMs", System.currentTimeMillis().toDouble()) + from("_itemId") { + set("_itemId", firstVideoId.toString()) + set("_type", "VIDEO") } - set("_order", 0.0) } - )) + set("_order", 0.0) + } ?: run { + context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to build media item.") + return + } + method.invoke(chatMediaDrawerActionHandler, listOf(), listOf(item)) } fun startConversion(audioOnly: Boolean) { @@ -204,7 +228,7 @@ class MediaFilePicker : Feature("Media File Picker") { val buttonTag = Random.nextInt(0, 65535) context.event.subscribe(AddViewEvent::class) { event -> - if (event.parent !is FrameLayout || !event.view::class.java.name.endsWith("ChatMediaDrawer")) return@subscribe + if (event.parent !is FrameLayout || drawerViewClass?.isInstance(event.view) != true) return@subscribe event.view.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { diff --git a/gradle.properties b/gradle.properties index 78756ceb..e32555bb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ org.gradle.jvmargs=-Xms2G -Xmx4G -Dfile.encoding=UTF-8 --enable-native-access=ALL-UNNAMED org.gradle.parallel=true -org.gradle.workers.max=8 +# Limit workers to avoid R8/minify file locking on Windows (classes.dex in use by another process) +org.gradle.workers.max=1 android.useAndroidX=true kotlin.code.style=official android.nonTransitiveRClass=true diff --git a/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt index 8f788d03..9f2eedc5 100644 --- a/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt +++ b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt @@ -40,6 +40,7 @@ class ClassMapper( COFObservableMapper(), FoldingLayoutMapper(), PlatformClientAttestationMapper(), + ChatMediaDrawerMapper(), ) } diff --git a/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/impl/ChatMediaDrawerMapper.kt b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/impl/ChatMediaDrawerMapper.kt new file mode 100644 index 00000000..dd1f2c5e --- /dev/null +++ b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/impl/ChatMediaDrawerMapper.kt @@ -0,0 +1,48 @@ +package me.eternal.purrfectsnap.mapper.impl + +import me.eternal.purrfectsnap.mapper.AbstractClassMapper +import me.eternal.purrfectsnap.mapper.ext.getClassName +import me.eternal.purrfectsnap.mapper.ext.getSuperClassName + +class ChatMediaDrawerMapper : AbstractClassMapper("ChatMediaDrawer") { + val chatMediaDrawerClass = classReference("chatMediaDrawerClass") + val actionHandlerClass = classReference("actionHandlerClass") + val sendItemsMethodName = string("sendItemsMethodName") + /** List element type for sendItems(List, List) second parameter — used when generic type is erased. */ + val sendItemsListItemClass = classReference("sendItemsListItemClass") + + init { + mapper { + val drawerClass = classes.firstOrNull { clazz -> + val name = clazz.getClassName() + val superName = clazz.getSuperClassName() ?: return@firstOrNull false + name.contains("ChatMediaDrawer") && + !name.contains("ActionHandler") && + superName.contains("ValdiGeneratedRootView") + } ?: return@mapper + + val actionHandlerClazz = classes.firstOrNull { clazz -> + clazz.getClassName().contains("ChatMediaDrawerActionHandler") + } ?: return@mapper + + val sendItemsMethod = actionHandlerClazz.methods.firstOrNull { method -> + method.name == "sendItems" && method.parameterTypes.size == 2 + } ?: actionHandlerClazz.methods.firstOrNull { method -> + method.parameterTypes.size == 2 && + method.parameterTypes[0].startsWith("Ljava/util/") && + method.parameterTypes[1].startsWith("Ljava/util/") + } ?: return@mapper + + chatMediaDrawerClass.set(drawerClass.getClassName().replace("/", ".")) + actionHandlerClass.set(actionHandlerClazz.getClassName().replace("/", ".")) + sendItemsMethodName.set(sendItemsMethod.name) + + // Find the list item type (e.g. Azc) by structure: has _order and _item (Valdi schema) + val listItemClass = classes.firstOrNull { clazz -> + val fieldNames = clazz.fields.map { it.name }.toSet() + fieldNames.contains("_order") && fieldNames.contains("_item") + } + listItemClass?.let { sendItemsListItemClass.set(it.getClassName().replace("/", ".")) } + } + } +}