diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5f36906..db4b034 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -77,7 +77,6 @@ android { } dependencies { - implementation(project(":js")) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -88,6 +87,7 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.js) implementation(libs.androidx.navigation.compose) implementation(libs.kotlinx.coroutines.core) implementation(libs.mmkv) @@ -99,8 +99,6 @@ dependencies { implementation(libs.accompanist.permissions) implementation(libs.androidx.javascriptengine) implementation(libs.androidx.concurrent.futures.ktx) - implementation(libs.jsoup) - implementation(libs.gson) implementation(libs.mmkv) implementation(libs.commons.compress) implementation(libs.lottie.compose) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc7cb93..56c7e52 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,9 +6,8 @@ coilNetworkOkhttp = "3.0.2" commonsCompress = "1.27.1" concurrentFuturesKtx = "1.2.0" desugar_jdk_libs = "2.1.4" -gson = "2.11.0" javascriptengine = "1.0.0-beta01" -jsoup = "1.18.1" +js = "1.0.1" kotlin = "2.0.0" coreKtx = "1.15.0" junit = "4.13.2" @@ -28,13 +27,10 @@ lottieCompose = "6.6.0" materialIconsExtended = "1.7.6" mmkv = "2.0.0" navigationCompose = "2.8.5" -rhino = "1.8.0" roomRuntime = "2.6.1" webkit = "1.13.0-alpha02" zoomable = "0.14.0" material3 = "1.3.1" -appcompat = "1.7.0" -material = "1.12.0" [libraries] accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } @@ -55,8 +51,7 @@ coil-video = { module = "io.coil-kt.coil3:coil-video", version.ref = "coilNetwor commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commonsCompress" } desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } editor = { module = "io.github.Rosemoe.sora-editor:editor" } -gson = { module = "com.google.code.gson:gson", version.ref = "gson" } -jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } +js = { module = "com.paulcoding:js", version.ref = "js" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -80,10 +75,7 @@ ktor-serialization-gson = { module = "io.ktor:ktor-serialization-gson", version. language-textmate = { module = "io.github.Rosemoe.sora-editor:language-textmate" } lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottieCompose" } mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" } -rhino = { module = "org.mozilla:rhino", version.ref = "rhino" } zoomable = { module = "me.saket.telephoto:zoomable", version.ref = "zoomable" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/js/.gitignore b/js/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/js/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/js/README.md b/js/README.md deleted file mode 100644 index f093522..0000000 --- a/js/README.md +++ /dev/null @@ -1,142 +0,0 @@ -# Android JavaScript Library - -A Kotlin-based Android library that provides seamless JavaScript execution and integration capabilities using Mozilla's Rhino engine. - -## Features - -- Execute JavaScript code from strings or files -- Built-in functions for network requests (fetch, xhr) -- Base64 encoding/decoding support -- Console logging -- File importing capability -- JSON conversion utilities -- Coroutine support for asynchronous operations - -## Installation - -Add the following dependency to your app's `build.gradle.kts`: - -```kotlin -dependencies { - implementation("com.paulcoding:js:1.0.0") // Replace with actual version -} -``` - -## Initialization - -Initialize the library in your Application class: - -```kotlin -class MyApplication : Application() { - override fun onCreate() { - super.onCreate() - JS.initialize(applicationContext) - } -} -``` - -## Usage - -### Basic JavaScript Execution - -```kotlin -// Create JS instance -val js = JS() - -// Execute JavaScript string -suspend fun executeJs() { - js.evaluateString("'Hello, World!'") - .onSuccess { result -> - println(result) // Prints: Hello, World! - } - .onFailure { error -> - error.printStackTrace() - } -} - -// Call JavaScript function -suspend fun callJsFunction() { - js.callFunction("add", arrayOf(2, 3)) - .onSuccess { result -> - println(result) // Prints: 5 - } -} -``` - -### Loading JavaScript from File - -```kotlin -// Load from file in app's files directory -val js = JS("scripts", "main.js") - -// Or with full path -val js = JS(File("/path/to/script.js")) -``` - -### Built-in Functions - -The library provides several built-in JavaScript functions: - -#### fetch() -```javascript -const html = fetch('https://example.com') -``` - -#### xhr() -```javascript -const data = xhr('https://api.example.com/data') -``` - -#### import() -```javascript -import('utils.js') -``` - -#### console.log() -```javascript -console.log('Debug message') -``` - -#### atob() -```javascript -const decoded = atob('SGVsbG8gV29ybGQ=') -``` - -### Custom Properties - -You can pass custom properties to the JavaScript environment: - -```kotlin -val properties = mapOf( - "apiKey" to "your-api-key", - "baseUrl" to "https://api.example.com" -) -val js = JS(properties = properties) -``` - -## Dependencies - -- [Rhino](https://github.com/mozilla/rhino): JavaScript engine -- [Ktor](https://ktor.io/): HTTP client -- [Gson](https://github.com/google/gson): JSON parsing -- [JSoup](https://jsoup.org/): HTML parsing - -## Proguard - -``` --keepattributes Signature --dontwarn org.mozilla.javascript.** --keep class org.mozilla.javascript.** { *; } --keep class org.jsoup.** { *; } --dontwarn org.jspecify.annotations.NullMarked -``` - -## Requirements - -- Minimum SDK: 24 -- Kotlin: 1.8+ -- Java: 11 - -## License - -[License](LICIENSE) diff --git a/js/build.gradle.kts b/js/build.gradle.kts deleted file mode 100644 index e3d009d..0000000 --- a/js/build.gradle.kts +++ /dev/null @@ -1,92 +0,0 @@ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - `maven-publish` -} - -android { - namespace = "com.paulcoding.js" - compileSdk = 34 - - defaultConfig { - minSdk = 24 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" - } - buildFeatures { - buildConfig = true - } -} - -dependencies { - - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) - - implementation(libs.rhino) - implementation(libs.jsoup) - implementation(libs.gson) - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.android) - implementation(libs.ktor.client.content.negotiation) - implementation(libs.ktor.client.logging) - implementation(libs.ktor.serialization.gson) - - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) -} - -afterEvaluate { - publishing { - publications { - create("release") { - from(components["release"]) - - groupId = "com.github.paulcoding810" - artifactId = "android-js" - version = "1.0.0" - - pom { - name.set("android-js") - description.set("Rhino wrapper for android kotlin") - url.set("https://github.com/paulcoding810/h-viewer") - - licenses { - license { - name.set("The Apache License, Version 2.0") - url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") - } - } - - developers { - developer { - id.set("paulcoding810") - name.set("Paul Nguyen") - email.set("paulcoding810@gmail.com") - } - } - } - } - } - } -} diff --git a/js/consumer-rules.pro b/js/consumer-rules.pro deleted file mode 100644 index e69de29..0000000 diff --git a/js/proguard-rules.pro b/js/proguard-rules.pro deleted file mode 100644 index 481bb43..0000000 --- a/js/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/js/src/androidTest/java/com/paulcoding/js/ExampleInstrumentedTest.kt b/js/src/androidTest/java/com/paulcoding/js/ExampleInstrumentedTest.kt deleted file mode 100644 index 2b675a4..0000000 --- a/js/src/androidTest/java/com/paulcoding/js/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.paulcoding.js - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.paulcoding.js.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/js/src/main/AndroidManifest.xml b/js/src/main/AndroidManifest.xml deleted file mode 100644 index a5918e6..0000000 --- a/js/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/js/src/main/java/com/paulcoding/js/JS.kt b/js/src/main/java/com/paulcoding/js/JS.kt deleted file mode 100644 index c738444..0000000 --- a/js/src/main/java/com/paulcoding/js/JS.kt +++ /dev/null @@ -1,137 +0,0 @@ -package com.paulcoding.js - -import com.google.gson.Gson -import com.google.gson.JsonElement -import com.google.gson.reflect.TypeToken -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.mozilla.javascript.Context -import org.mozilla.javascript.Function -import org.mozilla.javascript.NativeArray -import org.mozilla.javascript.NativeObject -import org.mozilla.javascript.ScriptableObject -import java.io.File -import java.io.FileReader - -fun toJsonElement(jsObject: Any?, gson: Gson = Gson()): JsonElement { - return when (jsObject) { - is NativeObject -> { - val map = jsObject.entries.associate { it.key.toString() to it.value } - gson.toJsonTree(map) - } - - is NativeArray -> { - val list = jsObject.toArray().toList() - gson.toJsonTree(list) - } - - else -> gson.toJsonTree(jsObject) - } -} - -class JS(file: File, properties: Map? = null) { - var scope: ScriptableObject - val gson = Gson() - - constructor(rootName: String, fileName: String, properties: Map? = null) : this( - File( - appContext.filesDir.absolutePath + "/$rootName", fileName - ), properties - ) - - constructor(fileRelativePath: String, properties: Map? = null) : this(File(appContext.filesDir.absolutePath, fileRelativePath), properties) - - fun prepareContext(): Context { - val context: Context = Context.enter() - context.setInterpretedMode(true) - context.setGeneratingDebug(BuildConfig.DEBUG) - - return context - } - - init { - val context = prepareContext() - root = file.absolutePath - scope = context.initStandardObjects() - ScriptableObject.putProperty(scope, "import", importFunction) - ScriptableObject.putProperty(scope, "fetch", fetchFunction) - ScriptableObject.putProperty(scope, "xhr", xhrFunction) - ScriptableObject.putProperty(scope, "atob", atobFunction) - ScriptableObject.putProperty(scope, "console", NativeObject().apply { - put("log", this, logFunction) - }) - - try { - properties?.forEach { (key, value) -> - ScriptableObject.putProperty( - scope, - key, - Context.javaToJS(value, scope) - ) - } - - val reader = FileReader(file) - context.evaluateReader(scope, reader, file.name, 1, null) - Context.exit() - reader.close() - } catch (e: Exception) { - e.printStackTrace() - } - } - - // is required (https://github.com/google/gson/blob/main/Troubleshooting.md#-illegalargumentexception-typetoken-type-argument-must-not-contain-a-type-variable) - suspend inline fun evaluateString(script: String, sourceName: String): Result { - return withContext(Dispatchers.IO) { - return@withContext runCatching { - val context = prepareContext() - val result = context.evaluateString(scope, script, sourceName, 1, null) - transformResult(result) - }.closeContext() - } - } - - suspend inline fun callFunction( - functionName: String, - args: Array = arrayOf() - ): Result { - return withContext(Dispatchers.IO) { - return@withContext runCatching { - val context = prepareContext() - val fn = try { - scope.get(functionName, scope) as Function - } catch (e: Exception) { - throw Exception("fn $functionName not found.") - } - val result = fn.call(context, scope, scope, args) - transformResult(result) - }.closeContext() - } - } - - inline fun transformResult(result: Any): T { - val jsonElement = toJsonElement(result) - val type = object : TypeToken() {}.type - return gson.fromJson(jsonElement, type) as T - } - - - companion object { - lateinit var appContext: android.content.Context - lateinit var root: String - - fun initialize(context: android.content.Context) { - appContext = context - } - } -} - -fun Result.closeContext(): Result { - onSuccess { - Context.exit() - } - onFailure { - Context.exit() - } - - return this -} diff --git a/js/src/main/java/com/paulcoding/js/JsFunctions.kt b/js/src/main/java/com/paulcoding/js/JsFunctions.kt deleted file mode 100644 index b35d1fb..0000000 --- a/js/src/main/java/com/paulcoding/js/JsFunctions.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.paulcoding.js - -import io.ktor.client.call.body -import io.ktor.client.request.get -import kotlinx.coroutines.runBlocking -import org.jsoup.Jsoup -import org.mozilla.javascript.BaseFunction -import org.mozilla.javascript.Context -import org.mozilla.javascript.NativeJSON -import org.mozilla.javascript.Scriptable -import java.io.File -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi - -val fetchFunction = object : BaseFunction() { - override fun call( - cx: Context?, - scope: Scriptable?, - thisObj: Scriptable?, - args: Array - ): Any? { - val url = args.getOrNull(0) as? String - if (url.isNullOrEmpty()) - throw IllegalArgumentException("URL is required") - - return runCatching { - Jsoup.connect(url).followRedirects(true).get() - }.onSuccess { - return it - }.onFailure { - log(url, "Failed to fetch") - it.printStackTrace() - return null - } - } -} - -val xhrFunction = object : BaseFunction() { - override fun call( - cx: Context?, - scope: Scriptable?, - thisObj: Scriptable?, - args: Array - ): Any { - val url = args.getOrNull(0) as? String - if (url.isNullOrEmpty()) - throw IllegalArgumentException("URL is required") - - return runBlocking { - ktorClient.use { client -> - val res: String = client.get(url).body() - NativeJSON.parse( - cx, scope, res - ) { cx, scope, thisObj, args -> args[1] } - } - } - } -} - -val logFunction = object : BaseFunction() { - override fun call( - cx: Context?, - scope: Scriptable?, - thisObj: Scriptable?, - args: Array - ): Any? { - print("[JS LOG]: ") - args.forEach { arg -> - println(Context.toString(arg)) - } - return Context.getUndefinedValue() - } -} - -val importFunction = object : BaseFunction() { - override fun call( - cx: Context?, - scope: Scriptable?, - thisObj: Scriptable?, - args: Array - ) { - val fileName = args.getOrNull(0) as? String - ?: throw IllegalArgumentException("File path is required") - - val file = File(JS.root, fileName) - cx?.evaluateReader(scope, file.reader(), file.name, 1, null) - } -} - -@OptIn(ExperimentalEncodingApi::class) -val atobFunction = object : BaseFunction() { - override fun call( - cx: Context?, - scope: Scriptable?, - thisObj: Scriptable?, - args: Array - ): Any { - val encoded = args.getOrNull(0) as? String - ?: throw IllegalArgumentException("Encoded string is required") - val decoded = Base64.Default.decode(encoded) - return decoded.decodeToString() - } -} \ No newline at end of file diff --git a/js/src/main/java/com/paulcoding/js/Ktor.kt b/js/src/main/java/com/paulcoding/js/Ktor.kt deleted file mode 100644 index e8b0da8..0000000 --- a/js/src/main/java/com/paulcoding/js/Ktor.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.paulcoding.js - -import com.google.gson.Strictness -import io.ktor.client.HttpClient -import io.ktor.client.engine.android.Android -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logger -import io.ktor.client.plugins.logging.Logging -import io.ktor.serialization.gson.gson - -val ktorClient - get() = HttpClient(Android) { - install(ContentNegotiation) { - gson { - setStrictness(Strictness.LENIENT) - } - } - install(Logging) { - logger = object : Logger { - override fun log(message: String) { - log(message, "HTTP Client") - } - } - level = if (BuildConfig.DEBUG) LogLevel.HEADERS else LogLevel.INFO - } - } diff --git a/js/src/main/java/com/paulcoding/js/Logger.kt b/js/src/main/java/com/paulcoding/js/Logger.kt deleted file mode 100644 index 9d5e4b3..0000000 --- a/js/src/main/java/com/paulcoding/js/Logger.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.paulcoding.js - -fun log( - message: Any?, - tag: String? = "Logger", -) { - val border = "*".repeat(150) - println("\n") - println(border) - print("\t") - println("$tag:") - print("\t") - println(message) - println(border) - println("\n") -} - -fun T.alsoLog(tag: String? = "Logger"): T { - log(this, tag) - return this -} diff --git a/js/src/test/java/com/paulcoding/js/ExampleUnitTest.kt b/js/src/test/java/com/paulcoding/js/ExampleUnitTest.kt deleted file mode 100644 index 4d00669..0000000 --- a/js/src/test/java/com/paulcoding/js/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.paulcoding.js - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4287b59..05433b7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,4 +21,3 @@ dependencyResolutionManagement { rootProject.name = "H Viewer" include(":app") -include(":js")