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