feat(experimental): composer hooks
This commit is contained in:
44
core/src/main/assets/composer/loader.js
Normal file
44
core/src/main/assets/composer/loader.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const deviceBridge = require('composer_core/src/DeviceBridge');
|
||||
|
||||
if (LOADER_CONFIG.logPrefix) {
|
||||
function internalLog(logLevel, args) {
|
||||
deviceBridge.copyToClipBoard(LOADER_CONFIG.logPrefix + "|" + logLevel + "|" + Array.from(args).join(" "));
|
||||
}
|
||||
|
||||
console.log = function() {
|
||||
internalLog("info", arguments);
|
||||
}
|
||||
|
||||
console.error = function() {
|
||||
internalLog("error", arguments);
|
||||
}
|
||||
|
||||
console.warn = function() {
|
||||
internalLog("warn", arguments);
|
||||
}
|
||||
|
||||
console.info = function() {
|
||||
internalLog("info", arguments);
|
||||
}
|
||||
|
||||
console.debug = function() {
|
||||
internalLog("debug", arguments);
|
||||
}
|
||||
|
||||
console.stacktrace = function() {
|
||||
return new Error().stack;
|
||||
}
|
||||
}
|
||||
|
||||
if (LOADER_CONFIG.bypassCameraRollLimit) {
|
||||
((module) => {
|
||||
module.MultiSelectClickHandler = new Proxy(module.MultiSelectClickHandler, {
|
||||
construct: function(target, args) {
|
||||
args[1].selectionLimit = 9999999;
|
||||
return new target(...args);
|
||||
},
|
||||
});
|
||||
})(require('memories_ui/src/clickhandlers/MultiSelectClickHandler'))
|
||||
}
|
||||
|
||||
console.info("loader.js loaded");
|
||||
@@ -156,6 +156,7 @@ class ModContext(
|
||||
disableBitmoji = config.experimental.nativeHooks.disableBitmoji.get(),
|
||||
disableMetrics = config.global.disableMetrics.get(),
|
||||
hookAssetOpen = config.experimental.disableComposerModules.get().isNotEmpty(),
|
||||
composerHooks = config.experimental.nativeHooks.composerHooks.globalState == true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,11 +4,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import me.rhunk.snapenhance.core.ModContext
|
||||
import me.rhunk.snapenhance.core.features.impl.COFOverride
|
||||
import me.rhunk.snapenhance.core.features.impl.ConfigurationOverride
|
||||
import me.rhunk.snapenhance.core.features.impl.MixerStories
|
||||
import me.rhunk.snapenhance.core.features.impl.OperaViewerParamsOverride
|
||||
import me.rhunk.snapenhance.core.features.impl.ScopeSync
|
||||
import me.rhunk.snapenhance.core.features.impl.*
|
||||
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
|
||||
import me.rhunk.snapenhance.core.features.impl.downloader.ProfilePictureDownloader
|
||||
import me.rhunk.snapenhance.core.features.impl.experiments.*
|
||||
@@ -16,7 +12,6 @@ import me.rhunk.snapenhance.core.features.impl.global.*
|
||||
import me.rhunk.snapenhance.core.features.impl.messaging.*
|
||||
import me.rhunk.snapenhance.core.features.impl.spying.HalfSwipeNotifier
|
||||
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
|
||||
import me.rhunk.snapenhance.core.features.impl.FriendMutationObserver
|
||||
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
|
||||
import me.rhunk.snapenhance.core.features.impl.tweaks.*
|
||||
import me.rhunk.snapenhance.core.features.impl.ui.*
|
||||
@@ -131,6 +126,7 @@ class FeatureManager(
|
||||
HideActiveMusic(),
|
||||
AutoOpenSnaps(),
|
||||
CustomStreaksExpirationFormat(),
|
||||
ComposerHooks(),
|
||||
)
|
||||
|
||||
initializeFeatures()
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.experiments
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.gson.JsonObject
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.common.ui.AppMaterialTheme
|
||||
import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
|
||||
import me.rhunk.snapenhance.common.ui.createComposeView
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.nativelib.NativeLib
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
|
||||
class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
private val config by lazy { context.config.experimental.nativeHooks.composerHooks }
|
||||
|
||||
private val composerConsole by lazy {
|
||||
createComposeAlertDialog(context.mainActivity!!) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
var result by remember { mutableStateOf("") }
|
||||
var codeContent by remember { mutableStateOf("") }
|
||||
|
||||
Text("Composer Console", fontSize = 18.sp, fontWeight = FontWeight.Bold)
|
||||
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textStyle = TextStyle.Default.copy(fontSize = 12.sp),
|
||||
value = codeContent,
|
||||
placeholder = { Text("Enter your JS code here:") },
|
||||
onValueChange = {
|
||||
codeContent = it
|
||||
}
|
||||
)
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
context.log.verbose("input: $codeContent", "ComposerConsole")
|
||||
result = "Running..."
|
||||
context.coroutineScope.launch {
|
||||
result = (context.native.composerEval("""
|
||||
(() => {
|
||||
try {
|
||||
$codeContent
|
||||
} catch (e) {
|
||||
return e.toString()
|
||||
}
|
||||
})()
|
||||
""".trimIndent()) ?: "(no result)").also {
|
||||
context.log.verbose("result: $it", "ComposerConsole")
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Run")
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun injectConsole() {
|
||||
val root = context.mainActivity!!.findViewById<FrameLayout>(android.R.id.content)
|
||||
root.post {
|
||||
root.addView(createComposeView(root.context) {
|
||||
AppMaterialTheme {
|
||||
FilledIconButton(
|
||||
onClick = {
|
||||
composerConsole.show()
|
||||
},
|
||||
modifier = Modifier.padding(top = 100.dp, end = 16.dp)
|
||||
) {
|
||||
Icon(Icons.Default.BugReport, contentDescription = "Debug Console")
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||
gravity = android.view.Gravity.TOP or android.view.Gravity.END
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadHooks() {
|
||||
val loaderConfig = JsonObject()
|
||||
|
||||
if (config.composerLogs.get()) {
|
||||
val logPrefix = Random.nextInt(100000..999999).toString()
|
||||
val logTag = "ComposerLogs"
|
||||
|
||||
ClipboardManager::class.java.hook("setPrimaryClip", HookStage.BEFORE) { param ->
|
||||
val clipData = param.arg<ClipData>(0).takeIf { it.itemCount == 1 } ?: return@hook
|
||||
val logText = clipData.getItemAt(0).text ?: return@hook
|
||||
if (!logText.startsWith("$logPrefix|")) return@hook
|
||||
|
||||
val logContainer = logText.removePrefix("$logPrefix|").toString()
|
||||
val logType = logContainer.substringBefore("|")
|
||||
val content = logContainer.substringAfter("|")
|
||||
|
||||
when (logType) {
|
||||
"verbose" -> context.log.verbose(content, logTag)
|
||||
"info" -> context.log.info(content, logTag)
|
||||
"debug" -> context.log.debug(content, logTag)
|
||||
"warn" -> context.log.warn(content, logTag)
|
||||
"error" -> context.log.error(content, logTag)
|
||||
else -> context.log.info(logContainer, logTag)
|
||||
}
|
||||
param.setResult(null)
|
||||
}
|
||||
loaderConfig.addProperty("logPrefix", logPrefix)
|
||||
}
|
||||
|
||||
if (config.bypassCameraRollLimit.get()) {
|
||||
loaderConfig.addProperty("bypassCameraRollLimit", true)
|
||||
}
|
||||
|
||||
val loaderScript = context.scriptRuntime.scripting.getScriptContent("composer/loader.js") ?: run {
|
||||
context.log.error("Failed to load composer loader script")
|
||||
return
|
||||
}
|
||||
|
||||
val hookResult = context.native.composerEval("""
|
||||
(() => { try { const LOADER_CONFIG = $loaderConfig; $loaderScript
|
||||
} catch (e) {
|
||||
return e.toString() + "\n" + e.stack;
|
||||
}
|
||||
return "success";
|
||||
})()
|
||||
""".trimIndent().trim())
|
||||
|
||||
if (hookResult != "success") {
|
||||
context.shortToast(("Composer loader failed : $hookResult").also {
|
||||
context.log.error(it)
|
||||
})
|
||||
}
|
||||
|
||||
if (config.composerConsole.get()) {
|
||||
injectConsole()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
if (!NativeLib.initialized || config.globalState != true) return
|
||||
var composerThreadTask: (() -> Unit)? = null
|
||||
|
||||
findClass("com.snap.composer.callable.ComposerFunctionNative").hook("nativePerform", HookStage.BEFORE) {
|
||||
composerThreadTask?.invoke()
|
||||
composerThreadTask = null
|
||||
}
|
||||
|
||||
context.coroutineScope.launch {
|
||||
context.native.waitForComposer()
|
||||
composerThreadTask = ::loadHooks
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user