feat: cof experiments
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
if (cofExperiments.contains(configId.lowercase())) {
|
||||
setBooleanResult(true)
|
||||
}
|
||||
|
||||
if (configId == "ANDROID_ACTION_MENU_V2" && result == true) {
|
||||
hasActionMenuV2 = true
|
||||
}
|
||||
}
|
||||
|
||||
setProperty("ANDROID_ACTION_MENU_V2", context.config.experimental.newChatActionMenu.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -36,6 +36,7 @@ class ClassMapper(
|
||||
OperaViewerParamsMapper(),
|
||||
MemoriesPresenterMapper(),
|
||||
StreaksExpirationMapper(),
|
||||
COFObservableMapper(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user