commit d28733275b23495b0be3192b2c3f3ca9d99c9aa4
parent 5d5a067319df31d56d5e9b3d1612f47c3af160ab
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu, 30 May 2024 23:59:55 +0200

feat: cof experiments

Diffstat:
Mcommon/src/main/assets/lang/en_US.json | 8++++----
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt | 13+++++++++++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/COFOverride.kt | 69++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt | 7+++++--
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt | 1+
Amapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/COFObservableMapper.kt | 35+++++++++++++++++++++++++++++++++++
6 files changed, 86 insertions(+), 47 deletions(-)

diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -973,10 +973,6 @@ "name": "Convert Message Locally", "description": "Converts snaps to chat external media locally. This appears in chat download context menu" }, - "new_chat_action_menu": { - "name": "New Chat Action Menu", - "description": "Use the new chat action menu drawer" - }, "media_file_picker": { "name": "Media File Picker", "description": "Allows you to pick any video/audio file from the gallery" @@ -999,6 +995,10 @@ } } }, + "cof_experiments": { + "name": "COF Experiments", + "description": "Enables unreleased/beta Snapchat features" + }, "edit_message": { "name": "Edit Messages", "description": "Allows you to edit messages in conversations" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt @@ -8,6 +8,15 @@ import me.rhunk.snapenhance.common.config.ConfigFlag import me.rhunk.snapenhance.common.config.FeatureNotice class Experimental : ConfigContainer() { + companion object { + val cofExperimentList = listOf( + "android_action_menu_v2", + "android_action_menu_adjust_message_position", + "chat_emoji_reactions_sending_enabled", + "chat_text_message_plugin", + ) + } + class ComposerHooksConfig: ConfigContainer(hasGlobalState = true) { val showFirstCreatedUsername = boolean("show_first_created_username") val bypassCameraRollLimit = boolean("bypass_camera_roll_limit") @@ -42,12 +51,12 @@ class Experimental : ConfigContainer() { val nativeHooks = container("native_hooks", NativeHooks()) { icon = Icons.Default.Memory; requireRestart() } val spoof = container("spoof", Spoof()) { icon = Icons.Default.Fingerprint ; addNotices(FeatureNotice.BAN_RISK); requireRestart() } val convertMessageLocally = boolean("convert_message_locally") { requireRestart() } - val newChatActionMenu = boolean("new_chat_action_menu") { requireRestart() } val mediaFilePicker = boolean("media_file_picker") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) } val storyLogger = boolean("story_logger") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); } val callRecorder = boolean("call_recorder") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); } val accountSwitcher = container("account_switcher", AccountSwitcherConfig()) { requireRestart(); addNotices(FeatureNotice.UNSTABLE) } - val editMessage = boolean("edit_message") { requireRestart(); addNotices(FeatureNotice.BAN_RISK) } + val editMessage = boolean("edit_message") { requireRestart() } + val cofExperiments = multiple("cof_experiments", *cofExperimentList.toTypedArray()) { requireRestart(); addFlags(ConfigFlag.NO_TRANSLATE); addNotices(FeatureNotice.UNSTABLE) } val appLock = container("app_lock", AppLockConfig()) { requireRestart(); addNotices(FeatureNotice.UNSTABLE) } val infiniteStoryBoost = boolean("infinite_story_boost") val meoPasscodeBypass = boolean("meo_passcode_bypass") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/COFOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/COFOverride.kt @@ -1,49 +1,40 @@ package me.rhunk.snapenhance.core.features.impl -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteDatabase.OpenParams -import me.rhunk.snapenhance.common.util.ktx.getBlobOrNull -import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams +import me.rhunk.snapenhance.core.util.dataBuilder +import me.rhunk.snapenhance.core.util.hook.HookStage +import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.mapper.impl.COFObservableMapper +import java.lang.reflect.Method -class COFOverride : Feature("Cof Override", loadParams = FeatureLoadParams.INIT_ASYNC) { - override fun asyncInit() { - val coreDatabaseFile = context.androidContext.getDatabasePath("core.db") - if (!coreDatabaseFile.exists()) return - SQLiteDatabase.openDatabase(coreDatabaseFile, OpenParams.Builder().apply { - setOpenFlags(SQLiteDatabase.OPEN_READWRITE or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) - }.build()).use { db -> - fun setProperty(configId: String, value: Any) { - runCatching { - db.rawQuery("SELECT config_result FROM ConfigRule WHERE config_id = ?", arrayOf(configId)).use { cursor -> - if (!cursor.moveToFirst()) { - context.log.warn("Failed to find $configId in ConfigRule") - return - } - val configResult = cursor.getBlobOrNull("config_result")?.let { - ProtoEditor(it).apply { - edit(2) { - clear() - when (value) { - is Int -> addVarInt(1, value) - is Long -> addVarInt(2, value) - is Float -> addFixed32(3, value) - is Boolean -> addVarInt(4, if (value) 1 else 0) - is String -> addString(5, value) - is ByteArray -> addBuffer(6, value) - is Double -> addFixed64(7, value.toLong()) - else -> return@edit - } - } - }.toByteArray() - } ?: return - db.execSQL("UPDATE ConfigRule SET config_result = ? WHERE config_id = ?", arrayOf(configResult, configId)) - } +class COFOverride : Feature("COF Override", loadParams = FeatureLoadParams.INIT_SYNC) { + var hasActionMenuV2 = false + + override fun init() { + val cofExperiments by context.config.experimental.cofExperiments + + context.mappings.useMapper(COFObservableMapper::class) { + classReference.getAsClass()?.hook(getBooleanObservable.get() ?: return@useMapper, HookStage.AFTER) { param -> + val configId = param.arg<String>(0) + val result by lazy { param.getResult()?.getObjectField("b") } + + fun setBooleanResult(state: Boolean) { + param.setResult((param.method() as Method).returnType.dataBuilder { + set("a", 4) + set("b", state) + }) } - } - setProperty("ANDROID_ACTION_MENU_V2", context.config.experimental.newChatActionMenu.get()) + if (cofExperiments.contains(configId.lowercase())) { + setBooleanResult(true) + } + + if (configId == "ANDROID_ACTION_MENU_V2" && result == true) { + hasActionMenuV2 = true + } + } } } } \ No newline at end of file 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 @@ -10,6 +10,7 @@ import android.widget.ScrollView import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams +import me.rhunk.snapenhance.core.features.impl.COFOverride import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.ui.findParent import me.rhunk.snapenhance.core.ui.menu.impl.* @@ -52,6 +53,8 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar val contextMenuButtonIconView = context.resources.getIdentifier("context_menu_button_icon_view", "id") val chatActionMenu = context.resources.getIdentifier("chat_action_menu", "id") + val hasV2ActionMenu = { context.feature(COFOverride::class).hasActionMenuV2 } + context.event.subscribe(AddViewEvent::class) { event -> val originalAddView: (View) -> Unit = { event.adapter.invokeOriginal(arrayOf(it, -1, @@ -99,12 +102,12 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar } } - if (childView.javaClass.name.endsWith("ChatActionMenuComponent") && context.config.experimental.newChatActionMenu.get()) { + if (childView.javaClass.name.endsWith("ChatActionMenuComponent") && hasV2ActionMenu()) { (menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).handle(event) return@subscribe } - if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer") && !context.config.experimental.newChatActionMenu.get()) { + if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer") && !hasV2ActionMenu()) { if (viewGroup.parent == null || viewGroup.parent.parent == null) return@subscribe menuMap[ChatActionMenu::class]!!.inject(viewGroup, childView, originalAddView) return@subscribe diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt @@ -36,6 +36,7 @@ class ClassMapper( OperaViewerParamsMapper(), MemoriesPresenterMapper(), StreaksExpirationMapper(), + COFObservableMapper(), ) } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/COFObservableMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/COFObservableMapper.kt @@ -0,0 +1,34 @@ +package me.rhunk.snapenhance.mapper.impl + +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import me.rhunk.snapenhance.mapper.AbstractClassMapper +import me.rhunk.snapenhance.mapper.ext.getClassName + +class COFObservableMapper: AbstractClassMapper("COFObservable") { + val classReference = classReference("class") + val getBooleanObservable = string("getBooleanObservable") + + init { + mapper { + for (classDef in classes) { + if (classDef.interfaces.isEmpty()) continue + if (classDef.methods.none { it.name == "dispose" }) continue + + val getBooleanObservableDexMethod = classDef.methods.firstOrNull { method -> + method.parameterTypes.size == 2 && + method.parameterTypes[0] == "Ljava/lang/String;" && + getClass(method.returnType)?.methods?.any { it.name == "mergeFrom" } == true + } ?: continue + + if (getBooleanObservableDexMethod.implementation?.instructions?.any { instruction -> + instruction is Instruction35c && (instruction.reference as? MethodReference)?.name == "elapsedRealtime" + } == true) { + getBooleanObservable.set(getBooleanObservableDexMethod.name) + classReference.set(classDef.getClassName()) + return@mapper + } + } + } + } +}+ \ No newline at end of file