commit 6e0e6d33395d13bb006800f8e8e5ee16eedcdda3
parent 08614ef4542d9d02d53e2bec2572c4b04ab86d6e
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sat, 16 Sep 2023 23:34:28 +0200

feat(ui): script list
- add module state
- add onSnapActivity and onManagerActivity test functions

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mapp/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt | 57++++++++++++++++++++++++++++++++++++++++++---------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt | 4++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt | 4+++-
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/aidl/me/rhunk/snapenhance/bridge/scripting/IScripting.aidl | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt | 3++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/scripting/JSModule.kt | 77++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Acore/src/main/kotlin/me/rhunk/snapenhance/scripting/ScriptKtx.kt | 44++++++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/scripting/ScriptRuntime.kt | 9+++++++++
Mnative/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt | 2+-
11 files changed, 314 insertions(+), 60 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt @@ -11,6 +11,7 @@ import me.rhunk.snapenhance.core.util.SQLiteDatabaseHelper import me.rhunk.snapenhance.core.util.ktx.getInteger import me.rhunk.snapenhance.core.util.ktx.getLongOrNull import me.rhunk.snapenhance.core.util.ktx.getStringOrNull +import me.rhunk.snapenhance.scripting.type.ModuleInfo import java.util.concurrent.Executors @@ -60,17 +61,12 @@ class ModDatabase( "expirationTimestamp BIGINT", "length INTEGER" ), - "analytics_config" to listOf( - "userId VARCHAR PRIMARY KEY", - "modes VARCHAR" - ), - "analytics" to listOf( - "hash VARCHAR PRIMARY KEY", - "userId VARCHAR", - "conversationId VARCHAR", - "timestamp BIGINT", - "eventName VARCHAR", - "eventData VARCHAR" + "scripts" to listOf( + "name VARCHAR PRIMARY KEY", + "version VARCHAR NOT NULL", + "description VARCHAR", + "author VARCHAR NOT NULL", + "enabled BOOLEAN" ) )) } @@ -251,4 +247,64 @@ class ModDatabase( ruleIds } } + + fun getScripts(): List<ModuleInfo> { + return database.rawQuery("SELECT * FROM scripts", null).use { cursor -> + val scripts = mutableListOf<ModuleInfo>() + while (cursor.moveToNext()) { + scripts.add( + ModuleInfo( + name = cursor.getStringOrNull("name")!!, + version = cursor.getStringOrNull("version")!!, + description = cursor.getStringOrNull("description"), + author = cursor.getStringOrNull("author"), + grantPermissions = null + ) + ) + } + scripts + } + } + + fun setScriptEnabled(name: String, enabled: Boolean) { + executeAsync { + database.execSQL("UPDATE scripts SET enabled = ? WHERE name = ?", arrayOf( + if (enabled) 1 else 0, + name + )) + } + } + + fun isScriptEnabled(name: String): Boolean { + return database.rawQuery("SELECT enabled FROM scripts WHERE name = ?", arrayOf(name)).use { cursor -> + if (!cursor.moveToFirst()) return@use false + cursor.getInteger("enabled") == 1 + } + } + + fun syncScripts(availableScripts: List<ModuleInfo>) { + executeAsync { + val enabledScripts = getScripts() + val enabledScriptPaths = enabledScripts.map { it.name } + val availableScriptPaths = availableScripts.map { it.name } + + enabledScripts.forEach { script -> + if (!availableScriptPaths.contains(script.name)) { + database.execSQL("DELETE FROM scripts WHERE name = ?", arrayOf(script.name)) + } + } + + availableScripts.forEach { script -> + if (!enabledScriptPaths.contains(script.name)) { + database.execSQL("INSERT OR REPLACE INTO scripts (name, version, description, author, enabled) VALUES (?, ?, ?, ?, ?)", arrayOf( + script.name, + script.version, + script.description, + script.author, + 0 + )) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt @@ -5,36 +5,63 @@ import androidx.documentfile.provider.DocumentFile import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.bridge.scripting.ReloadListener +import me.rhunk.snapenhance.scripting.type.ModuleInfo +import java.io.InputStream class RemoteScriptManager( private val context: RemoteSideContext, ) : IScripting.Stub() { - private val scriptRuntime = ScriptRuntime(context.log) + val runtime = ScriptRuntime(context.log) private fun getScriptFolder() = DocumentFile.fromTreeUri(context.androidContext, Uri.parse(context.config.root.scripting.moduleFolder.get())) - private fun hasHotReload() = context.config.root.scripting.hotReload.get() - + //private fun hasHotReload() = context.config.root.scripting.hotReload.get() private val reloadListeners = mutableListOf<ReloadListener>() + private val cachedModuleInfo = mutableMapOf<String, ModuleInfo>() + + fun sync() { + getScriptFileNames().forEach { name -> + runCatching { + getScriptInputStream(name) { stream -> + runtime.getModuleInfo(stream!!).also { info -> + cachedModuleInfo[name] = info + } + } + }.onFailure { + context.log.error("Failed to load module info for $name", it) + } + } + + context.modDatabase.syncScripts(cachedModuleInfo.values.toList()) + } + fun init() { - enabledScriptPaths.forEach { path -> - val content = getScriptContent(path) - scriptRuntime.load(path, content) + sync() + + enabledScripts.forEach { path -> + val content = getScriptContent(path) ?: return@forEach + runtime.load(path, content) } } - override fun getEnabledScriptPaths(): List<String> { - val folder = getScriptFolder() ?: return emptyList() - return folder.listFiles().filter { it.name?.endsWith(".js") ?: false }.map { it.name!! } + private fun <R> getScriptInputStream(name: String, callback: (InputStream?) -> R): R { + val file = getScriptFolder()?.findFile(name) ?: return callback(null) + return context.androidContext.contentResolver.openInputStream(file.uri)?.use(callback) ?: callback(null) + } + + private fun getScriptFileNames(): List<String> { + return (getScriptFolder() ?: return emptyList()).listFiles().filter { it.name?.endsWith(".js") ?: false }.map { it.name!! } + } + + override fun getEnabledScripts(): List<String> { + return getScriptFileNames().filter { + context.modDatabase.isScriptEnabled(cachedModuleInfo[it]?.name ?: return@filter false) + } } - override fun getScriptContent(path: String): String { - val folder = getScriptFolder() ?: return "" - val file = folder.findFile(path) ?: return "" - return context.androidContext.contentResolver.openInputStream(file.uri)?.use { - it.readBytes().toString(Charsets.UTF_8) - } ?: "" + override fun getScriptContent(name: String): String? { + return getScriptInputStream(name) { it?.bufferedReader()?.readText() } } override fun registerReloadListener(listener: ReloadListener) { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt @@ -46,6 +46,10 @@ class MainActivity : ComponentActivity() { checkForRequirements() } + managerContext.scriptManager.runtime.eachModule { + callOnManagerLoad(this@MainActivity) + } + sections = EnumSection.values().toList().associateWith { it.section.constructors.first().call() }.onEach { (section, instance) -> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt @@ -17,6 +17,7 @@ import me.rhunk.snapenhance.ui.manager.sections.NotImplemented import me.rhunk.snapenhance.ui.manager.sections.downloads.DownloadsSection import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection import me.rhunk.snapenhance.ui.manager.sections.home.HomeSection +import me.rhunk.snapenhance.ui.manager.sections.scripting.ScriptsSection import me.rhunk.snapenhance.ui.manager.sections.social.SocialSection import kotlin.reflect.KClass @@ -47,7 +48,8 @@ enum class EnumSection( ), SCRIPTS( route = "scripts", - icon = Icons.Filled.DataObject + icon = Icons.Filled.DataObject, + section = ScriptsSection::class ); companion object { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt @@ -0,0 +1,91 @@ +package me.rhunk.snapenhance.ui.manager.sections.scripting + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.rhunk.snapenhance.scripting.type.ModuleInfo +import me.rhunk.snapenhance.ui.manager.Section + +class ScriptsSection : Section() { + @Composable + fun ModuleItem(script: ModuleInfo) { + var enabled by remember { + mutableStateOf(context.modDatabase.isScriptEnabled(script.name)) + } + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + elevation = CardDefaults.cardElevation() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 8.dp) + ) { + Text( + text = script.name, + fontSize = 20.sp, + ) + Text( + text = script.description ?: "No description", + fontSize = 14.sp, + ) + } + Switch( + checked = enabled, + onCheckedChange = { + context.modDatabase.setScriptEnabled(script.name, it) + enabled = it + } + ) + } + } + } + + + @Composable + override fun Content() { + val scriptModules = remember { + context.modDatabase.getScripts() + } + + LazyColumn { + item { + if (scriptModules.isEmpty()) { + Text( + text = "No scripts found", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(8.dp) + ) + } + } + items(scriptModules.size) { index -> + ModuleItem(scriptModules[index]) + } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/aidl/me/rhunk/snapenhance/bridge/scripting/IScripting.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/scripting/IScripting.aidl @@ -3,9 +3,9 @@ package me.rhunk.snapenhance.bridge.scripting; import me.rhunk.snapenhance.bridge.scripting.ReloadListener; interface IScripting { - List<String> getEnabledScriptPaths(); + List<String> getEnabledScripts(); - String getScriptContent(String path); + @nullable String getScriptContent(String path); void registerReloadListener(ReloadListener listener); } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -122,7 +122,7 @@ class SnapEnhance { } }) - enabledScriptPaths.forEach { path -> + enabledScripts.forEach { path -> runCatching { scriptRuntime.load(path, getScriptContent(path)) }.onFailure { @@ -143,6 +143,7 @@ class SnapEnhance { with(appContext) { features.onActivityCreate() actionManager.init() + scriptRuntime.eachModule { callOnSnapActivity(mainActivity!!) } } }.also { time -> appContext.log.verbose("onActivityCreate took $time") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/scripting/JSModule.kt b/core/src/main/kotlin/me/rhunk/snapenhance/scripting/JSModule.kt @@ -1,53 +1,72 @@ package me.rhunk.snapenhance.scripting +import android.app.Activity import me.rhunk.snapenhance.core.logger.AbstractLogger import me.rhunk.snapenhance.scripting.type.ModuleInfo -import org.mozilla.javascript.Context -import org.mozilla.javascript.FunctionObject import org.mozilla.javascript.ScriptableObject +import org.mozilla.javascript.Undefined class JSModule( val moduleInfo: ModuleInfo, val content: String, ) { lateinit var logger: AbstractLogger - private lateinit var scope: ScriptableObject - - companion object { - @JvmStatic - fun logDebug(message: String) { - println(message) - } - } + private lateinit var moduleObject: ScriptableObject fun load() { - val context = Context.enter() - context.optimizationLevel = -1 - scope = context.initSafeStandardObjects() - scope.putConst("module", scope, moduleInfo) + contextScope { + moduleObject = initSafeStandardObjects() + moduleObject.putConst("module", moduleObject, buildScriptableObject { + putConst("info", this, buildScriptableObject { + putConst("name", this, moduleInfo.name) + putConst("version", this, moduleInfo.version) + putConst("description", this, moduleInfo.description) + putConst("author", this, moduleInfo.author) + putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion) + putConst("minSEVersion", this, moduleInfo.minSEVersion) + putConst("grantPermissions", this, moduleInfo.grantPermissions) + }) + }) - scope.putConst("logDebug", scope, - FunctionObject("logDebug", JSModule::class.java.getDeclaredMethod("logDebug", String::class.java), scope) - ) + moduleObject.putFunction("logInfo") { args -> + logger.info(args?.getOrNull(0)?.toString() ?: "null") + Undefined.instance + } - context.evaluateString(scope, content, moduleInfo.name, 1, null) + evaluateString(moduleObject, content, moduleInfo.name, 1, null) + } } fun unload() { - val context = Context.enter() - context.evaluateString(scope, "if (typeof module.onUnload === 'function') module.onUnload();", "onUnload", 1, null) - Context.exit() + contextScope { + moduleObject.scriptable("module")?.function("onUnload")?.call( + this, + moduleObject, + moduleObject, + null + ) + } } - fun callOnCoreLoad() { - val context = Context.enter() - context.evaluateString(scope, "if (typeof module.onCoreLoad === 'function') module.onCoreLoad();", "onCoreLoad", 1, null) - Context.exit() + fun callOnSnapActivity(activity: Activity) { + contextScope { + moduleObject.scriptable("module")?.function("onSnapActivity")?.call( + this, + moduleObject, + moduleObject, + arrayOf(activity) + ) + } } - fun callOnManagerLoad() { - val context = Context.enter() - context.evaluateString(scope, "if (typeof module.onManagerLoad === 'function') module.onManagerLoad();", "onManagerLoad", 1, null) - Context.exit() + fun callOnManagerLoad(activity: Activity) { + contextScope { + moduleObject.scriptable("module")?.function("onManagerActivity")?.call( + this, + moduleObject, + moduleObject, + arrayOf(activity) + ) + } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/scripting/ScriptKtx.kt b/core/src/main/kotlin/me/rhunk/snapenhance/scripting/ScriptKtx.kt @@ -0,0 +1,43 @@ +package me.rhunk.snapenhance.scripting + +import org.mozilla.javascript.Context +import org.mozilla.javascript.Function +import org.mozilla.javascript.Scriptable +import org.mozilla.javascript.ScriptableObject + +fun contextScope(f: Context.() -> Unit) { + val context = Context.enter() + context.optimizationLevel = -1 + try { + context.f() + } finally { + Context.exit() + } +} + +fun Scriptable.scriptable(name: String): Scriptable? { + return this.get(name, this) as? Scriptable +} + +fun Scriptable.function(name: String): Function? { + return this.get(name, this) as? Function +} + +fun ScriptableObject.putFunction(name: String, proxy: Scriptable.(Array<out Any>?) -> Any) { + this.putConst(name, this, object: org.mozilla.javascript.BaseFunction() { + override fun call( + cx: Context?, + scope: Scriptable, + thisObj: Scriptable, + args: Array<out Any>? + ): Any { + return thisObj.proxy(args) + } + }) +} + +fun buildScriptableObject(name: String? = "ScriptableObject", f: ScriptableObject.() -> Unit): ScriptableObject { + return object: ScriptableObject() { + override fun getClassName() = name + }.apply(f) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/scripting/ScriptRuntime.kt b/core/src/main/kotlin/me/rhunk/snapenhance/scripting/ScriptRuntime.kt @@ -4,12 +4,17 @@ import me.rhunk.snapenhance.core.logger.AbstractLogger import me.rhunk.snapenhance.scripting.type.ModuleInfo import java.io.BufferedReader import java.io.ByteArrayInputStream +import java.io.InputStream class ScriptRuntime( private val logger: AbstractLogger, ) { private val modules = mutableMapOf<String, JSModule>() + fun eachModule(f: JSModule.() -> Unit) { + modules.values.forEach(f) + } + private fun readModuleInfo(reader: BufferedReader): ModuleInfo { val header = reader.readLine() if (!header.startsWith("// ==SE_module==")) { @@ -40,6 +45,10 @@ class ScriptRuntime( ) } + fun getModuleInfo(inputStream: InputStream): ModuleInfo { + return readModuleInfo(inputStream.bufferedReader()) + } + fun reload(path: String, content: String) { unload(path) load(path, content) diff --git a/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt b/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt @@ -15,7 +15,7 @@ class NativeLib { init(classloader) initialized = true }.onFailure { - Log.e("SnapEnhance", "NativeLib init failed", it) + Log.e("SnapEnhance", "NativeLib init failed") } }