feat(ui): script list

- add module state
- add onSnapActivity and onManagerActivity test functions
This commit is contained in:
rhunk
2023-09-16 23:34:28 +02:00
parent 08614ef454
commit 6e0e6d3339
11 changed files with 314 additions and 62 deletions

View File

@@ -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
))
}
}
}
}
}

View File

@@ -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)
}
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)
} ?: ""
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(name: String): String? {
return getScriptInputStream(name) { it?.bufferedReader()?.readText() }
}
override fun registerReloadListener(listener: ReloadListener) {

View File

@@ -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) ->

View File

@@ -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 {

View File

@@ -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])
}
}
}
}

View File

@@ -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);
}

View File

@@ -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")

View File

@@ -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
private lateinit var moduleObject: ScriptableObject
companion object {
@JvmStatic
fun logDebug(message: String) {
println(message)
fun load() {
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)
})
})
moduleObject.putFunction("logInfo") { args ->
logger.info(args?.getOrNull(0)?.toString() ?: "null")
Undefined.instance
}
evaluateString(moduleObject, content, moduleInfo.name, 1, null)
}
}
fun load() {
val context = Context.enter()
context.optimizationLevel = -1
scope = context.initSafeStandardObjects()
scope.putConst("module", scope, moduleInfo)
scope.putConst("logDebug", scope,
FunctionObject("logDebug", JSModule::class.java.getDeclaredMethod("logDebug", String::class.java), scope)
)
context.evaluateString(scope, 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)
)
}
}
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -15,7 +15,7 @@ class NativeLib {
init(classloader)
initialized = true
}.onFailure {
Log.e("SnapEnhance", "NativeLib init failed", it)
Log.e("SnapEnhance", "NativeLib init failed")
}
}