refactor(composer_hooks): module export
This commit is contained in:
@@ -1,37 +1,22 @@
|
||||
const deviceBridge = require('composer_core/src/DeviceBridge');
|
||||
const callExport = require('composer_core/src/DeviceBridge')[EXPORTED_FUNCTION_NAME];
|
||||
|
||||
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 (!callExport) {
|
||||
return "No export function found";
|
||||
}
|
||||
|
||||
if (LOADER_CONFIG.bypassCameraRollLimit) {
|
||||
((module) => {
|
||||
const config = callExport("getConfig");
|
||||
|
||||
if (config.composerLogs) {
|
||||
["log", "error", "warn", "info", "debug"].forEach(method => {
|
||||
console[method] = (...args) => callExport("log", method, Array.from(args).join(" "));
|
||||
})
|
||||
|
||||
console.stacktrace = () => new Error().stack;
|
||||
console.info("loader.js loaded");
|
||||
}
|
||||
|
||||
if (config.bypassCameraRollLimit) {
|
||||
(module => {
|
||||
module.MultiSelectClickHandler = new Proxy(module.MultiSelectClickHandler, {
|
||||
construct: function(target, args, newTarget) {
|
||||
args[1].selectionLimit = 9999999;
|
||||
@@ -40,5 +25,3 @@ if (LOADER_CONFIG.bypassCameraRollLimit) {
|
||||
});
|
||||
})(require('memories_ui/src/clickhandlers/MultiSelectClickHandler'))
|
||||
}
|
||||
|
||||
console.info("loader.js loaded");
|
||||
@@ -1,7 +1,5 @@
|
||||
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
|
||||
@@ -12,7 +10,11 @@ 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.material3.Button
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -22,7 +24,6 @@ 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
|
||||
@@ -30,13 +31,17 @@ 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.Hooker
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.wrapper.impl.composer.ComposerMarshaller
|
||||
import me.rhunk.snapenhance.nativelib.NativeLib
|
||||
import java.lang.reflect.Proxy
|
||||
import kotlin.math.absoluteValue
|
||||
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 exportedFunctionName = Random.nextInt().absoluteValue.toString(16)
|
||||
|
||||
private val composerConsole by lazy {
|
||||
createComposeAlertDialog(context.mainActivity!!) {
|
||||
@@ -114,46 +119,75 @@ class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.ACT
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadHooks() {
|
||||
val loaderConfig = JsonObject()
|
||||
private fun newComposerFunction(block: (composerMarshaller: ComposerMarshaller) -> Boolean): Any {
|
||||
val composerFunctionClass = findClass("com.snap.composer.callable.ComposerFunction")
|
||||
return Proxy.newProxyInstance(
|
||||
composerFunctionClass.classLoader,
|
||||
arrayOf(composerFunctionClass)
|
||||
) { _, method, args ->
|
||||
if (method.name != "perform") return@newProxyInstance null
|
||||
block(ComposerMarshaller(args?.get(0) ?: return@newProxyInstance false))
|
||||
}
|
||||
}
|
||||
|
||||
if (config.composerLogs.get()) {
|
||||
val logPrefix = Random.nextInt(100000..999999).toString()
|
||||
val logTag = "ComposerLogs"
|
||||
private fun handleExportCall(composerMarshaller: ComposerMarshaller): Boolean {
|
||||
val argc = composerMarshaller.getSize()
|
||||
if (argc < 1) return false
|
||||
val action = composerMarshaller.getUntyped(0) as? String ?: return false
|
||||
|
||||
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)
|
||||
when (action) {
|
||||
"getConfig" -> {
|
||||
composerMarshaller.pushUntyped(
|
||||
HashMap<String, Any>().apply {
|
||||
put("bypassCameraRollLimit", config.bypassCameraRollLimit.get())
|
||||
put("composerConsole", config.composerConsole.get())
|
||||
put("composerLogs", config.composerLogs.get())
|
||||
}
|
||||
)
|
||||
}
|
||||
loaderConfig.addProperty("logPrefix", logPrefix)
|
||||
"showToast" -> {
|
||||
if (argc < 2) return false
|
||||
val message = composerMarshaller.getUntyped(1) as? String ?: return false
|
||||
context.shortToast(message)
|
||||
}
|
||||
"log" -> {
|
||||
if (argc < 3) return false
|
||||
val logLevel = composerMarshaller.getUntyped(1) as? String ?: return false
|
||||
val message = composerMarshaller.getUntyped(2) as? String ?: return false
|
||||
|
||||
val tag = "ComposerLogs"
|
||||
|
||||
when (logLevel) {
|
||||
"log" -> context.log.verbose(message, tag)
|
||||
"debug" -> context.log.debug(message, tag)
|
||||
"info" -> context.log.info(message, tag)
|
||||
"warn" -> context.log.warn(message, tag)
|
||||
"error" -> context.log.error(message, tag)
|
||||
}
|
||||
}
|
||||
"eval" -> {
|
||||
if (argc < 2) return false
|
||||
val code = composerMarshaller.getUntyped(1) as? String ?: return false
|
||||
runCatching {
|
||||
composerMarshaller.pushUntyped(context.native.composerEval(code))
|
||||
}.onFailure {
|
||||
composerMarshaller.pushUntyped(it.toString())
|
||||
}
|
||||
}
|
||||
else -> context.log.warn("Unknown action: $action", "Composer")
|
||||
}
|
||||
|
||||
if (config.bypassCameraRollLimit.get()) {
|
||||
loaderConfig.addProperty("bypassCameraRollLimit", true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun loadHooks() {
|
||||
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
|
||||
(() => { try { const EXPORTED_FUNCTION_NAME = "$exportedFunctionName"; $loaderScript
|
||||
} catch (e) {
|
||||
return e.toString() + "\n" + e.stack;
|
||||
}
|
||||
@@ -172,8 +206,21 @@ class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.ACT
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun onActivityCreate() {
|
||||
if (!NativeLib.initialized || config.globalState != true) return
|
||||
|
||||
findClass("com.snapchat.client.composer.NativeBridge").hook("registerNativeModuleFactory", HookStage.BEFORE) { param ->
|
||||
val moduleFactory = param.argNullable<Any>(1) ?: return@hook
|
||||
if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString() != "DeviceBridge") return@hook
|
||||
Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam ->
|
||||
val functions = methodParam.getResult() as? MutableMap<String, Any> ?: return@ephemeralHookObjectMethod
|
||||
functions[exportedFunctionName] = newComposerFunction {
|
||||
handleExportCall(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var composerThreadTask: (() -> Unit)? = null
|
||||
|
||||
findClass("com.snap.composer.callable.ComposerFunctionNative").hook("nativePerform", HookStage.BEFORE) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package me.rhunk.snapenhance.core.wrapper.impl.composer
|
||||
|
||||
import me.rhunk.snapenhance.core.wrapper.AbstractWrapper
|
||||
|
||||
class ComposerMarshaller(obj: Any): AbstractWrapper(obj) {
|
||||
private val getUntypedMethod by lazy { instanceNonNull().javaClass.methods.first { it.name == "getUntyped" } }
|
||||
private val getSizeMethod by lazy { instanceNonNull().javaClass.methods.first { it.name == "getSize" } }
|
||||
private val pushUntypedMethod by lazy { instanceNonNull().javaClass.methods.first { it.name == "pushUntyped" } }
|
||||
|
||||
fun getUntyped(index: Int): Any? = getUntypedMethod.invoke(instanceNonNull(), index)
|
||||
fun getSize() = getSizeMethod.invoke(instanceNonNull()) as Int
|
||||
fun pushUntyped(value: Any?): Any? = pushUntypedMethod.invoke(instanceNonNull(), value)
|
||||
}
|
||||
Reference in New Issue
Block a user