From c606bd23aac4ab08b3a33c5453fda474965fbfd8 Mon Sep 17 00:00:00 2001 From: aiselp Date: Sun, 28 Jul 2024 22:39:51 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0engins=E6=A8=A1=E5=9D=97,?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E8=A7=A3=E6=9E=90=E5=99=A8=E4=BC=98=E5=8C=96?= =?UTF-8?q?,=E6=96=87=E6=A1=A3=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...5\273\272java\345\257\271\350\261\241.mjs" | 7 + ...00\201\344\272\213\344\273\266-nodejs.mjs" | 5 + ...\200\201\344\272\213\344\273\266-rhino.js" | 4 + ...223\216\344\272\213\344\273\266- rhino.js" | 6 + ...23\216\344\272\213\344\273\266-nodejs.mjs" | 11 ++ ...0\350\241\214\350\204\232\346\234\254.mjs" | 26 ++++ autojs/src/js-api/src/axios/index.ts | 2 +- autojs/src/js-api/src/clip_manager/index.ts | 2 + autojs/src/js-api/src/engines/engines.d.ts | 31 +++++ autojs/src/js-api/src/engines/index.ts | 120 ++++++++++++++++++ autojs/src/js-api/src/java/index.ts | 5 +- autojs/src/js-api/src/toast/index.ts | 2 +- autojs/src/js-api/src/vue-ui/README.md | 40 ++++++ autojs/src/js-api/src/vue-ui/index.ts | 8 ++ .../java/com/aiselp/autox/api/JsEngines.kt | 94 ++++++++++++++ .../aiselp/autox/engine/NativeApiManager.kt | 18 ++- .../aiselp/autox/engine/NodeScriptEngine.kt | 22 +++- .../aiselp/autox/module/NodeModuleResolver.kt | 62 ++++++--- .../autojs/engine/JavaScriptEngine.java | 8 +- .../stardust/autojs/engine/ScriptEngine.kt | 19 ++- .../autojs/execution/ExecutionConfig.kt | 17 +-- .../stardust/autojs/runtime/api/Engines.java | 3 +- gradle/libs.versions.toml | 2 +- 23 files changed, 461 insertions(+), 53 deletions(-) create mode 100644 "app/src/main/assets/sample/v7/java\344\272\244\344\272\222/\345\210\233\345\273\272java\345\257\271\350\261\241.mjs" create mode 100644 "app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\345\217\221\351\200\201\344\272\213\344\273\266-nodejs.mjs" create mode 100644 "app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\345\217\221\351\200\201\344\272\213\344\273\266-rhino.js" create mode 100644 "app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\347\233\221\345\220\254\345\274\225\346\223\216\344\272\213\344\273\266- rhino.js" create mode 100644 "app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\347\233\221\345\220\254\345\274\225\346\223\216\344\272\213\344\273\266-nodejs.mjs" create mode 100644 "app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\350\277\220\350\241\214\350\204\232\346\234\254.mjs" create mode 100644 autojs/src/js-api/src/engines/engines.d.ts create mode 100644 autojs/src/js-api/src/engines/index.ts create mode 100644 autojs/src/js-api/src/vue-ui/README.md create mode 100644 autojs/src/main/java/com/aiselp/autox/api/JsEngines.kt diff --git "a/app/src/main/assets/sample/v7/java\344\272\244\344\272\222/\345\210\233\345\273\272java\345\257\271\350\261\241.mjs" "b/app/src/main/assets/sample/v7/java\344\272\244\344\272\222/\345\210\233\345\273\272java\345\257\271\350\261\241.mjs" new file mode 100644 index 000000000..13d266d6f --- /dev/null +++ "b/app/src/main/assets/sample/v7/java\344\272\244\344\272\222/\345\210\233\345\273\272java\345\257\271\350\261\241.mjs" @@ -0,0 +1,7 @@ +import { loadClass } from 'java' + +const File = loadClass('java.io.File') + +const a = new File('/sdcard') + +console.log(a.isFile()); diff --git "a/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\345\217\221\351\200\201\344\272\213\344\273\266-nodejs.mjs" "b/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\345\217\221\351\200\201\344\272\213\344\273\266-nodejs.mjs" new file mode 100644 index 000000000..261828df5 --- /dev/null +++ "b/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\345\217\221\351\200\201\344\272\213\344\273\266-nodejs.mjs" @@ -0,0 +1,5 @@ +import { getRunningEngines } from 'engines' + +getRunningEngines().forEach(engine => { + engine.emit('test', 789012) +}) \ No newline at end of file diff --git "a/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\345\217\221\351\200\201\344\272\213\344\273\266-rhino.js" "b/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\345\217\221\351\200\201\344\272\213\344\273\266-rhino.js" new file mode 100644 index 000000000..ebe545b01 --- /dev/null +++ "b/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\345\217\221\351\200\201\344\272\213\344\273\266-rhino.js" @@ -0,0 +1,4 @@ + +engines.all().forEach(e => { + e.emit('test', 123456) +}); diff --git "a/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\347\233\221\345\220\254\345\274\225\346\223\216\344\272\213\344\273\266- rhino.js" "b/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\347\233\221\345\220\254\345\274\225\346\223\216\344\272\213\344\273\266- rhino.js" new file mode 100644 index 000000000..84028d879 --- /dev/null +++ "b/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\347\233\221\345\220\254\345\274\225\346\223\216\344\272\213\344\273\266- rhino.js" @@ -0,0 +1,6 @@ +events.on('test', (data) => { + toast(`收到test事件, data:${data}`) +}) + +//保持脚本运行 +setInterval(() => { }, 1000) \ No newline at end of file diff --git "a/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\347\233\221\345\220\254\345\274\225\346\223\216\344\272\213\344\273\266-nodejs.mjs" "b/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\347\233\221\345\220\254\345\274\225\346\223\216\344\272\213\344\273\266-nodejs.mjs" new file mode 100644 index 000000000..57e617c15 --- /dev/null +++ "b/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\347\233\221\345\220\254\345\274\225\346\223\216\344\272\213\344\273\266-nodejs.mjs" @@ -0,0 +1,11 @@ +import { selfEngine } from 'engines' +import { showToast } from 'toast' + +// console.log(myEngine()); + +selfEngine.on('test', (data) => { + showToast(`收到test事件, data:${data}`) +}) + +//保持脚本运行 +setInterval(() => { }, 1000) \ No newline at end of file diff --git "a/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\350\277\220\350\241\214\350\204\232\346\234\254.mjs" "b/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\350\277\220\350\241\214\350\204\232\346\234\254.mjs" new file mode 100644 index 000000000..bebeddc17 --- /dev/null +++ "b/app/src/main/assets/sample/v7/\350\204\232\346\234\254\345\274\225\346\223\216/\350\277\220\350\241\214\350\204\232\346\234\254.mjs" @@ -0,0 +1,26 @@ +import { execScriptFile, stopAll } from 'engines' +import { writeFile, rm } from 'fs/promises' +import { showToast } from 'toast' + + +const file = process.cwd() + '/test.js' +await writeFile(file, ` + setTimeout(() => { }, 2000) + `, "utf8") +console.log(process.cwd()); + +execScriptFile(file, { + onStart() { + showToast('开始执行') + }, + onSuccess() { + showToast('执行成功') + rm(file, { force: true }) + }, + onException() { + showToast('执行出错') + rm(file, { force: true }) + } +}) + +setTimeout(() => { }, 5000) \ No newline at end of file diff --git a/autojs/src/js-api/src/axios/index.ts b/autojs/src/js-api/src/axios/index.ts index 8cd674d98..0e126d536 100644 --- a/autojs/src/js-api/src/axios/index.ts +++ b/autojs/src/js-api/src/axios/index.ts @@ -3,7 +3,7 @@ * 作用于node.js和浏览器中。 * 它提供了http请求的方便封装。 * - * (官方中文文档)[https://axios-http.com/zh/docs/intro] + * [官方中文文档](https://axios-http.com/zh/docs/intro) * @packageDocumentation */ export { default } from 'axios' diff --git a/autojs/src/js-api/src/clip_manager/index.ts b/autojs/src/js-api/src/clip_manager/index.ts index 3ddf3f724..08e8218fe 100644 --- a/autojs/src/js-api/src/clip_manager/index.ts +++ b/autojs/src/js-api/src/clip_manager/index.ts @@ -8,9 +8,11 @@ const clipManager = Autox.clipManager /** * 此对象是一个EventEmitter,用于监听剪贴板变化 * @template + * ```js * clipboardManager.on('clip_changed',()=>{ * getClip() * }) + * ``` */ export const clipboardManager = new EventEmitter() clipManager.registerListener({ diff --git a/autojs/src/js-api/src/engines/engines.d.ts b/autojs/src/js-api/src/engines/engines.d.ts new file mode 100644 index 000000000..b2484f659 --- /dev/null +++ b/autojs/src/js-api/src/engines/engines.d.ts @@ -0,0 +1,31 @@ +declare namespace root.engines { + let selfEngine: any | undefined + function myEngine(): NodeScriptEngine + function allEngine(): Set + function stopAll() + function stopAllAndToast() + function createExecutionConfig(): ExecutionConfig + function execScriptFile(path: String, + config: ExecutionConfig?, + listener: ((a: number, ...args) => void)? + ): ScriptExecution + function setupJs(ops: { + emitCallback: (name: string, ...args: any[]) => void + }) +} + +interface ScriptEngine { + id: number + isDestroyed: boolean + forceStop() + cwd(): string + emit(name: String, ...args: any) +} +interface NodeScriptEngine extends ScriptEngine { + +} +type ExecutionConfig = { + workingDirectory: string + arguments: Map +} +type ScriptExecution = {} \ No newline at end of file diff --git a/autojs/src/js-api/src/engines/index.ts b/autojs/src/js-api/src/engines/index.ts new file mode 100644 index 000000000..d877c24a3 --- /dev/null +++ b/autojs/src/js-api/src/engines/index.ts @@ -0,0 +1,120 @@ +import { EventEmitter } from 'node:events' +const engines = Autox.engines + +export class ScriptEngineProxy extends EventEmitter { + private engine: ScriptEngine + get id(): number { + return this.engine.id + } + get isDestroyed(): boolean { + return this.engine.isDestroyed + } + + constructor(engine: ScriptEngine) { + super() + this.engine = engine + } + emit(eventName: string | symbol, ...args: any[]): boolean { + super.emit(eventName, ...args) + if (typeof eventName === 'string' && this.id !== selfEngine.id) { + this.engine.emit(eventName, ...args) + } + return true + } + forceStop() { + if (engines.selfEngine === this) { + process.exit(1) + return + } + this.engine.forceStop() + } + cwd(): string { + return this.engine.cwd() + } + +} +/** + * 当前运行的引擎 + */ +export const selfEngine = new ScriptEngineProxy(engines.myEngine()) +engines.setupJs({ + emitCallback(name, ...args) { + selfEngine.emit(name, ...args) + }, +}) +export interface ExecutionConfigOptions { + workingDirectory?: string + arguments?: Map + onStart?: (execution: ScriptExecution) => void + onSuccess?: (execution: ScriptExecution, result: any) => void + onException?: (execution: ScriptExecution, err: any) => void +} +/** + * 获取当前运行的引擎 + * @returns + */ +export function myEngine(): ScriptEngineProxy { + return selfEngine +} +/** + * 运行一个脚本文件 + * @param path 只能是绝对路径,不支持相对路径 + * @param ops + * @returns + */ +export function execScriptFile(path: string, ops: ExecutionConfigOptions) { + if (ops) { + const executionConfig = engines.createExecutionConfig() + if (ops.workingDirectory) { + executionConfig.workingDirectory = ops.workingDirectory + } + if (ops.arguments) { + ops.arguments.forEach((value, key) => { + executionConfig.arguments.set(key, value) + }) + } + return engines.execScriptFile(path, executionConfig, (a, ...args) => { + if (a == 0) { + ops.onStart?.(args[0] as ScriptExecution) + } else if (a == 1) { + ops.onSuccess?.(args[0] as ScriptExecution, args[1]) + } else if (a == 2) { + ops.onException?.(args[0] as ScriptExecution, args[1]) + } + }) + } + return engines.execScriptFile(path, null, null) +} +/** + * 停止所有运行中的脚本,包括自身 + * @returns + */ +export function stopAll() { + engines.stopAll() +} +/** + * 获取所有运行中的脚本 + * @returns + */ +export function getRunningEngines() { + const r: ScriptEngineProxy[] = [] + engines.allEngine().forEach((engine) => { + r.push(new ScriptEngineProxy(engine)) + }) + return r +} +/** + * 向所有运行中的脚本发送事件,相当于 + * ```js + * getRunningEngines().forEach((engine) => { + engine.emit(event, ...args) + }) + * ``` + * @param event + * @param args + */ +export function broadcast(event: string, ...args: any) { + getRunningEngines().forEach((engine) => { + engine.emit(event, ...args) + }) +} \ No newline at end of file diff --git a/autojs/src/js-api/src/java/index.ts b/autojs/src/js-api/src/java/index.ts index 1e8a058f3..53c0d4d9d 100644 --- a/autojs/src/js-api/src/java/index.ts +++ b/autojs/src/js-api/src/java/index.ts @@ -1,4 +1,7 @@ - +/** + * 该模块用于与java交互 + * @packageDocumentation + */ const java = Autox.java /** * 采用默认的计算线程池异步调用java方法,返回Promise接受结果 diff --git a/autojs/src/js-api/src/toast/index.ts b/autojs/src/js-api/src/toast/index.ts index db7d6bf5a..e18ec391b 100644 --- a/autojs/src/js-api/src/toast/index.ts +++ b/autojs/src/js-api/src/toast/index.ts @@ -9,7 +9,7 @@ interface ToastOptions { * import { showToast } from 'toast' * showToast('hello world') * @param message 要显示的消息 - * @param option 可以是"short" | "long",表示弹出时长 + * @param option 可以是`"short" | "long"`,表示弹出时长 */ export function showToast(message: any, option?: ToastOptions | string) { let duration: number diff --git a/autojs/src/js-api/src/vue-ui/README.md b/autojs/src/js-api/src/vue-ui/README.md new file mode 100644 index 000000000..a6a2dc2fb --- /dev/null +++ b/autojs/src/js-api/src/vue-ui/README.md @@ -0,0 +1,40 @@ +在第二代 api,ui 构建不再使用传统的 xml 和 e4x, +也不在是基于 view 的系统,而是在 js 端使用[vue3](https://cn.vuejs.org/guide/introduction.html)和[htm](https://github.com/developit/htm)作为模板引擎,在 java 端使用 Jetpack Compose, +要注意的是虽然是使用 vue 但并不代表 vue 所有特性都可用,也不代表就能够使用 web 中的各种 ui 框架,准确的来说是使用了 vue 的核心`@vue/runtime-core`,而实现了一套从 vue 渲染到安卓 Jetpack Compose 的渲染器。 + +## 入门 + +想要创建一个界面首先你需要从`vue-ui`模块导入各种函数,如 + +```js +import { createApp, xml, startActivity, ModifierExtension } from "vue-ui"; +``` + +使用`createApp`创建一个 app 实例,与 vue 的方法相同,不过其中必须包含`render`函数, +用于创建 Vnode。 + +```js +const app = createApp({ + render() { + return xml` + + UI内容 + + `; + }, +}); +``` + +`render`渲染函数会在数据变化时调用许多次,你不能在这个函数中做任何与创建 Vnode 无关的事。 +创建好后使用`startActivity`函数将打开一个界面并显示。 + +```js +startActivity(app); +``` + +`startActivity`会返回一个`Promise`,你可以通过`await`等待 Activity 启动完成并得到 +Activity 实例。 +与第 1 代 api 不同,你无需在脚本最前面加上`'ui';`,因为脚本线程始终与 ui 线程分离, +这样你还可以通过多次调用`startActivity`启动多个 Activity,但必须是不同的 app 实例。 + +## 组件 diff --git a/autojs/src/js-api/src/vue-ui/index.ts b/autojs/src/js-api/src/vue-ui/index.ts index 2e8376515..cf7890152 100644 --- a/autojs/src/js-api/src/vue-ui/index.ts +++ b/autojs/src/js-api/src/vue-ui/index.ts @@ -1,3 +1,8 @@ +/** + * 入门 + * @document README.md + * @packageDocumentation + */ import { App, Component, @@ -78,6 +83,9 @@ export const xml = htm.bind(h) export { setDebug } export * from '@vue/runtime-core' export * as Icons from './icons' +/** + * 这个对象导出用于设置`Modifier`的函数 + */ export { ModifierExtension } export { ActivityEventListener } export * as Theme from './theme' \ No newline at end of file diff --git a/autojs/src/main/java/com/aiselp/autox/api/JsEngines.kt b/autojs/src/main/java/com/aiselp/autox/api/JsEngines.kt new file mode 100644 index 000000000..add0ce834 --- /dev/null +++ b/autojs/src/main/java/com/aiselp/autox/api/JsEngines.kt @@ -0,0 +1,94 @@ +package com.aiselp.autox.api + +import com.aiselp.autox.engine.NodeScriptEngine +import com.caoccao.javet.annotations.V8Function +import com.caoccao.javet.interop.V8Runtime +import com.caoccao.javet.values.reference.V8ValueFunction +import com.caoccao.javet.values.reference.V8ValueObject +import com.stardust.autojs.ScriptEngineService +import com.stardust.autojs.engine.ScriptEngine +import com.stardust.autojs.execution.ExecutionConfig +import com.stardust.autojs.execution.ScriptExecution +import com.stardust.autojs.execution.ScriptExecutionListener +import com.stardust.autojs.script.ScriptFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class JsEngines(private val engine: NodeScriptEngine) : NativeApi { + override val moduleId: String = ID + private val engineService by lazy { ScriptEngineService.instance!! } + private var emitCallback: V8ValueFunction? = null + + override fun install(v8Runtime: V8Runtime, global: V8ValueObject): NativeApi.BindingMode { + return NativeApi.BindingMode.PROXY + } + + override fun recycle(v8Runtime: V8Runtime, global: V8ValueObject) { + } + + @V8Function + fun setupJs(ops: V8ValueObject) { + emitCallback = ops.get(EMIT_FUNCTION) + } + + @V8Function + fun execScriptFile( + path: String, + config: ExecutionConfig?, + listener: V8ValueFunction? + ): ScriptExecution { + if (listener != null) { + val callback = engine.eventLoopQueue.createV8Callback(listener) + return engineService.execute(ScriptFile(path).toSource(), object : + ScriptExecutionListener { + override fun onStart(execution: ScriptExecution?) { + callback.invoke(0, execution) + } + + override fun onSuccess(execution: ScriptExecution?, result: Any?) { + callback.invoke(1, execution, result) + callback.close() + } + + override fun onException(execution: ScriptExecution?, e: Throwable?) { + callback.invoke(2, execution, e) + callback.close() + } + + }, config) + } else + return engineService.execute(ScriptFile(path).toSource(), config) + } + + fun emitEngineEvent(name: String, args: Array) { + emitCallback?.callVoid(null, name, *args) + } + + @V8Function + fun allEngine(): Set> { + return engineService.engines + } + + @V8Function + fun myEngine() = engine + + @V8Function + fun createExecutionConfig() = ExecutionConfig() + + @V8Function + fun stopAll() { + engine.scope.launch(Dispatchers.Default) { + engineService.stopAll() + } + } + + @V8Function + fun stopAllAndToast() { + engineService.stopAllAndToast() + } + + companion object { + const val ID = "engines" + private const val EMIT_FUNCTION = "emitCallback" + } +} \ No newline at end of file diff --git a/autojs/src/main/java/com/aiselp/autox/engine/NativeApiManager.kt b/autojs/src/main/java/com/aiselp/autox/engine/NativeApiManager.kt index e71ed5e3c..e4a77fce0 100644 --- a/autojs/src/main/java/com/aiselp/autox/engine/NativeApiManager.kt +++ b/autojs/src/main/java/com/aiselp/autox/engine/NativeApiManager.kt @@ -7,6 +7,7 @@ import com.caoccao.javet.annotations.V8Property import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.reference.V8ValueError +import com.caoccao.javet.values.reference.V8ValueFunction import com.caoccao.javet.values.reference.V8ValueObject import com.stardust.autojs.runtime.exception.ScriptException import kotlinx.coroutines.cancel @@ -20,10 +21,16 @@ class NativeApiManager(engine: NodeScriptEngine) { apis[api.moduleId] = api } + fun getNativeApi(moduleId: String): NativeApi? { + return apis[moduleId] + } + fun initialize(v8Runtime: V8Runtime, global: V8ValueObject) { v8Runtime.createV8ValueObject().use { autoxObject -> autoxObject.bind(rootObject) - global.set(INSTANCE_NAME, autoxObject) + v8Runtime.getExecutor(DEFINE_PROPERTY).execute().use { + it.callVoid(null, global, INSTANCE_NAME, autoxObject) + } for (api in apis.values) { val bindingMode = api.install(v8Runtime, global) when (bindingMode) { @@ -87,5 +94,14 @@ class NativeApiManager(engine: NodeScriptEngine) { companion object { private const val INSTANCE_NAME = "Autox" + private val DEFINE_PROPERTY = """ + (obj, name, value) => { + Object.defineProperty(obj, name, { + value, + writable: false, + enumerable: false, + }) + } + """.trimIndent() } } \ No newline at end of file diff --git a/autojs/src/main/java/com/aiselp/autox/engine/NodeScriptEngine.kt b/autojs/src/main/java/com/aiselp/autox/engine/NodeScriptEngine.kt index 99a55af3a..cedcb6519 100644 --- a/autojs/src/main/java/com/aiselp/autox/engine/NodeScriptEngine.kt +++ b/autojs/src/main/java/com/aiselp/autox/engine/NodeScriptEngine.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Log import com.aiselp.autox.api.JavaInteractor import com.aiselp.autox.api.JsClipManager +import com.aiselp.autox.api.JsEngines import com.aiselp.autox.api.JsMedia import com.aiselp.autox.api.JsToast import com.aiselp.autox.api.JsUi @@ -34,7 +35,7 @@ import kotlinx.coroutines.withTimeout import java.io.File class NodeScriptEngine(val context: Context, val uiHandler: UiHandler) : - ScriptEngine.AbstractScriptEngine() { + ScriptEngine.AbstractScriptEngine(), ScriptEngine.EngineEvent { val runtime: NodeRuntime = V8Host.getNodeInstance().createV8Runtime() private val tags = mutableMapOf() @@ -81,6 +82,7 @@ class NodeScriptEngine(val context: Context, val uiHandler: UiHandler) : override fun init() { runtime.converter = converter runtime.allowEval(true) + runtime.isStopping = true runtime.getExecutor( """ (()=>{ @@ -109,6 +111,7 @@ class NodeScriptEngine(val context: Context, val uiHandler: UiHandler) : nativeApiManager.register(JavaInteractor(scope, converter, promiseFactory)) nativeApiManager.register(JsToast(context, scope)) nativeApiManager.register(JsMedia(context)) + nativeApiManager.register(JsEngines(this)) nativeApiManager.initialize(runtime, global) } @@ -124,7 +127,7 @@ class NodeScriptEngine(val context: Context, val uiHandler: UiHandler) : while (scope.isActive) { // Log.d(TAG,"loop ing...") if (runtime.await(V8AwaitMode.RunNoWait) or - eventLoopQueue.executeQueue() or + eventLoopQueue.executeQueue() || resultListener.result.isActive ) { Thread.sleep(1) @@ -146,6 +149,11 @@ class NodeScriptEngine(val context: Context, val uiHandler: UiHandler) : } } + override fun emit(name: String, vararg args: Any?) { + val jsEngines = nativeApiManager.getNativeApi(JsEngines.ID) as? JsEngines + jsEngines?.let { eventLoopQueue.addTask { it.emitEngineEvent(name, args) } } + } + private fun exceptionHandling(e: Any?) { when (e) { is Throwable -> run { @@ -165,19 +173,19 @@ class NodeScriptEngine(val context: Context, val uiHandler: UiHandler) : private fun initializeModule(file: File): V8Value { val parentFile = file.parentFile ?: File("/") runtime.getNodeModule(NodeModuleProcess::class.java).workingDirectory = parentFile - runtime.getNodeModule(NodeModuleModule::class.java).setRequireRootDirectory(parentFile) - val nodeModuleResolver = NodeModuleResolver(parentFile, moduleDirectory) + val nodeModuleResolver = NodeModuleResolver(runtime, parentFile, moduleDirectory) runtime.v8ModuleResolver = nodeModuleResolver + runtime.globalObject.delete(NodeModuleModule.PROPERTY_REQUIRE) return if (NodeModuleResolver.isEsModule(file)) { //es module - runtime.getExecutor(file).setResourceName(file.path).compileV8Module(true).run { + NodeModuleResolver.compileV8Module(runtime, file.readText(), file.path).run { nodeModuleResolver.addCacheModule(this) execute() } } else { //commonjs - runtime.globalObject.invoke( - NodeModuleModule.PROPERTY_REQUIRE, runtime.createV8ValueString(file.path) + nodeModuleResolver.require.call( + null, runtime.createV8ValueString(file.path) ) } } diff --git a/autojs/src/main/java/com/aiselp/autox/module/NodeModuleResolver.kt b/autojs/src/main/java/com/aiselp/autox/module/NodeModuleResolver.kt index 672bc7e5a..c59407e81 100644 --- a/autojs/src/main/java/com/aiselp/autox/module/NodeModuleResolver.kt +++ b/autojs/src/main/java/com/aiselp/autox/module/NodeModuleResolver.kt @@ -2,15 +2,28 @@ package com.aiselp.autox.module import android.net.Uri import android.util.Log +import com.caoccao.javet.interop.NodeRuntime import com.caoccao.javet.interop.V8Runtime -import com.caoccao.javet.interop.callback.JavetBuiltInModuleResolver +import com.caoccao.javet.interop.callback.IV8ModuleResolver +import com.caoccao.javet.node.modules.NodeModuleModule import com.caoccao.javet.values.reference.IV8Module +import com.caoccao.javet.values.reference.V8Module +import com.caoccao.javet.values.reference.V8ValueFunction +import com.caoccao.javet.values.reference.V8ValueObject import java.io.File import java.net.URI -class NodeModuleResolver(val workingDirectory: File, private val globalModuleDirectory: File) : - JavetBuiltInModuleResolver() { +class NodeModuleResolver( + val runtime: NodeRuntime, + val workingDirectory: File, + private val globalModuleDirectory: File +) : IV8ModuleResolver { private val esModuleCache = mutableMapOf() + val require = runtime.getNodeModule(NodeModuleModule::class.java).moduleObject + .invoke( + NodeModuleModule.FUNCTION_CREATE_REQUIRE, + workingDirectory.absolutePath + ) override fun resolve( v8Runtime: V8Runtime, resourceName: String, v8ModuleReferrer: IV8Module @@ -21,13 +34,25 @@ class NodeModuleResolver(val workingDirectory: File, private val globalModuleDir return parsingModule(v8Runtime, Uri.parse(s)) } val uri = Uri.parse(resourceName) - return if (uri.scheme == null) { - try { - super.resolve(v8Runtime, "node:$resourceName", v8ModuleReferrer) - }catch (e: Exception){ - null - } ?: parsingPackageModule(v8Runtime, workingDirectory, resourceName) - } else parsingModule(v8Runtime, uri) + return when (uri.scheme) { + "node" -> loadNodeModule(resourceName) + null -> run { + try { + loadNodeModule("node:$resourceName") + } catch (e: Exception) { + parsingPackageModule(v8Runtime, workingDirectory, resourceName) + } + } + + else -> parsingModule(v8Runtime, uri) + } + } + + private fun loadNodeModule(resourceName: String): V8Module? { + require.call(null, resourceName).let { module -> + module.set("default", module) + return runtime.createV8Module(resourceName, module) + } } private fun checkCacheModule(resourceName: String, load: () -> IV8Module?): IV8Module? { @@ -47,13 +72,12 @@ class NodeModuleResolver(val workingDirectory: File, private val globalModuleDir v8Runtime: V8Runtime, file: File, isModule: Boolean = isEsModule(file) ): IV8Module? = checkCacheModule(file.path) { return@checkCacheModule if (isModule) { - v8Runtime.getExecutor(file).setResourceName(file.path) - .compileV8Module() + compileV8Module(v8Runtime, file.readText(), file.path) } else { - v8Runtime.getExecutor( - "export default require(`${file.path}`)" - ).setResourceName(file.path) - .compileV8Module() + require.call(null, file.path).use { valueObject -> + valueObject.set("default", valueObject) + runtime.createV8Module(file.path, valueObject) + } } } @@ -90,6 +114,12 @@ class NodeModuleResolver(val workingDirectory: File, private val globalModuleDir companion object { private const val TAG = "NodeModuleResolver" + fun compileV8Module(runtime: V8Runtime, script: String, resourceName: String): IV8Module { + val executor = runtime.getExecutor(script) + executor.v8ScriptOrigin.resourceName = resourceName + return executor.setModule(true).compileV8Module() + } + fun isEsModule(file: File): Boolean { return if (file.isFile) { if (file.path.endsWith(".mjs")) return true diff --git a/autojs/src/main/java/com/stardust/autojs/engine/JavaScriptEngine.java b/autojs/src/main/java/com/stardust/autojs/engine/JavaScriptEngine.java index 586ade0d4..ddefddd17 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/JavaScriptEngine.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/JavaScriptEngine.java @@ -1,5 +1,7 @@ package com.stardust.autojs.engine; +import androidx.annotation.NonNull; + import com.stardust.autojs.runtime.ScriptRuntime; import com.stardust.autojs.script.JavaScriptSource; import com.stardust.autojs.script.ScriptSource; @@ -8,7 +10,8 @@ * Created by Stardust on 2017/8/3. */ -public abstract class JavaScriptEngine extends ScriptEngine.AbstractScriptEngine { +public abstract class JavaScriptEngine extends ScriptEngine.AbstractScriptEngine + implements ScriptEngine.EngineEvent { private ScriptRuntime mRuntime; private Object mExecArgv; @@ -35,7 +38,8 @@ public void setRuntime(ScriptRuntime runtime) { put("runtime", runtime); } - public void emit(String eventName, Object... args) { + @Override + public void emit(@NonNull String eventName, Object... args) { mRuntime.timers.getMainTimer().postDelayed(() -> mRuntime.events.emit(eventName, args), 0); } diff --git a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.kt b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.kt index 6bfefd283..323d73db8 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.kt +++ b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.kt @@ -20,31 +20,28 @@ import java.util.concurrent.atomic.AtomicInteger * If you want to stop the engine in other threads, you should call [ScriptEngine.forceStop]. */ interface ScriptEngine { + var id: Int + val isDestroyed: Boolean + val uncaughtException: Throwable? fun put(name: String, value: Any?) fun execute(scriptSource: S): Any? fun forceStop() fun destroy() - val isDestroyed: Boolean fun setTag(key: String, value: Any?) fun getTag(key: String): Any? fun cwd(): String? fun uncaughtException(throwable: Throwable?) - val uncaughtException: Throwable? - var id: Int - - /** - * @hide - */ fun setOnDestroyListener(listener: OnDestroyListener) - - /** - * @hide - */ fun init() + interface OnDestroyListener { fun onDestroy(engine: ScriptEngine<*>) } + interface EngineEvent { + fun emit(name: String, vararg args: Any?) + } + abstract class AbstractScriptEngine : ScriptEngine { private val mTags: MutableMap = ConcurrentHashMap() diff --git a/autojs/src/main/java/com/stardust/autojs/execution/ExecutionConfig.kt b/autojs/src/main/java/com/stardust/autojs/execution/ExecutionConfig.kt index d4b37f282..f15134b28 100644 --- a/autojs/src/main/java/com/stardust/autojs/execution/ExecutionConfig.kt +++ b/autojs/src/main/java/com/stardust/autojs/execution/ExecutionConfig.kt @@ -16,12 +16,7 @@ data class ExecutionConfig( var loopTimes: Int = 1, var scriptConfig: ScriptConfig = ScriptConfig() ) : Parcelable { - - - private val mArguments = HashMap() - - val arguments: Map - get() = mArguments + val arguments = mutableMapOf() constructor(parcel: Parcel) : this( parcel.readString().orEmpty(), @@ -32,12 +27,12 @@ data class ExecutionConfig( parcel.readInt() ) - fun setArgument(key: String, `object`: Any) { - mArguments[key] = `object` + fun setArgument(key: String, `object`: Any?) { + arguments[key] = `object` } fun getArgument(key: String): Any? { - return mArguments[key] + return arguments[key] } override fun equals(other: Any?): Boolean { @@ -52,7 +47,7 @@ data class ExecutionConfig( if (delay != other.delay) return false if (interval != other.interval) return false if (loopTimes != other.loopTimes) return false - if (mArguments != other.mArguments) return false + if (arguments != other.arguments) return false return true } @@ -64,7 +59,7 @@ data class ExecutionConfig( result = 31 * result + delay.hashCode() result = 31 * result + interval.hashCode() result = 31 * result + loopTimes - result = 31 * result + mArguments.hashCode() + result = 31 * result + arguments.hashCode() return result } diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/Engines.java b/autojs/src/main/java/com/stardust/autojs/runtime/api/Engines.java index 78fbaeab7..7f06f1b2e 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/api/Engines.java +++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/Engines.java @@ -7,6 +7,7 @@ import com.stardust.autojs.runtime.ScriptRuntime; import com.stardust.autojs.script.AutoFileSource; import com.stardust.autojs.script.JavaScriptFileSource; +import com.stardust.autojs.script.ScriptFile; import com.stardust.autojs.script.StringScriptSource; /** @@ -29,7 +30,7 @@ public ScriptExecution execScript(String name, String script, ExecutionConfig co } public ScriptExecution execScriptFile(String path, ExecutionConfig config) { - return mEngineService.execute(new JavaScriptFileSource(mScriptRuntime.files.path(path)), config); + return mEngineService.execute(new ScriptFile(mScriptRuntime.files.path(path)).toSource(), config); } public ScriptExecution execAutoFile(String path, ExecutionConfig config) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 13014e37f..6b9dc5802 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -94,7 +94,7 @@ rxjava2 = "io.reactivex.rxjava2:rxjava:2.2.21" rxjava2-rxandroid = "io.reactivex.rxjava2:rxandroid:2.1.1" rxjava3 = "io.reactivex.rxjava3:rxjava:3.1.5" rxjava3-rxandroid = "io.reactivex.rxjava3:rxandroid:3.0.2" -javet-android-node = "com.caoccao.javet:javet-android-node:3.1.3" +javet-android-node = "com.caoccao.javet:javet-android-node:3.1.4" #test libraries =================================== junit = { module = "junit:junit", version.ref = "junit" } From 5670a4b2b3dc13fd1fb2eafa3c62f944cb21b9d2 Mon Sep 17 00:00:00 2001 From: aiselp Date: Mon, 5 Aug 2024 14:07:05 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E7=BB=9D=E5=AF=B9=E8=B7=AF=E5=BE=84=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=EF=BC=8C=E4=B8=BAjava=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E7=B1=BB=E4=BC=BCrhino=E7=9A=84=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...5\273\272java\345\257\271\350\261\241.mjs" | 4 +- autojs/src/js-api/package-lock.json | 8 ++++ autojs/src/js-api/package.json | 1 + autojs/src/js-api/src/java/index.ts | 46 +++++++++++++++++++ .../aiselp/autox/module/NodeModuleResolver.kt | 7 ++- 5 files changed, 63 insertions(+), 3 deletions(-) diff --git "a/app/src/main/assets/sample/v7/java\344\272\244\344\272\222/\345\210\233\345\273\272java\345\257\271\350\261\241.mjs" "b/app/src/main/assets/sample/v7/java\344\272\244\344\272\222/\345\210\233\345\273\272java\345\257\271\350\261\241.mjs" index 13d266d6f..137b5f637 100644 --- "a/app/src/main/assets/sample/v7/java\344\272\244\344\272\222/\345\210\233\345\273\272java\345\257\271\350\261\241.mjs" +++ "b/app/src/main/assets/sample/v7/java\344\272\244\344\272\222/\345\210\233\345\273\272java\345\257\271\350\261\241.mjs" @@ -1,7 +1,9 @@ -import { loadClass } from 'java' +import { loadClass, java } from 'java' const File = loadClass('java.io.File') const a = new File('/sdcard') +const b = new java.io.File('/sdcard') console.log(a.isFile()); +console.log(b.isFile()); diff --git a/autojs/src/js-api/package-lock.json b/autojs/src/js-api/package-lock.json index 1776f9841..6d2e393fe 100644 --- a/autojs/src/js-api/package-lock.json +++ b/autojs/src/js-api/package-lock.json @@ -14,6 +14,7 @@ "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-typescript": "^11.1.6", + "@types/lodash": "^4.17.7", "@vue/runtime-core": "^3.4.31", "axios": "^1.7.2", "gulp": "^5.0.0", @@ -496,6 +497,13 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.14.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", diff --git a/autojs/src/js-api/package.json b/autojs/src/js-api/package.json index b8a20df44..77f1e80f1 100644 --- a/autojs/src/js-api/package.json +++ b/autojs/src/js-api/package.json @@ -16,6 +16,7 @@ "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-typescript": "^11.1.6", + "@types/lodash": "^4.17.7", "@vue/runtime-core": "^3.4.31", "axios": "^1.7.2", "gulp": "^5.0.0", diff --git a/autojs/src/js-api/src/java/index.ts b/autojs/src/js-api/src/java/index.ts index 53c0d4d9d..a0c453a0d 100644 --- a/autojs/src/js-api/src/java/index.ts +++ b/autojs/src/js-api/src/java/index.ts @@ -2,7 +2,26 @@ * 该模块用于与java交互 * @packageDocumentation */ +import { memoize } from 'lodash' const java = Autox.java + +const createPackage = memoize(function (name?: string): any { + if (name) { + try { + return loadClass(name) + } catch (e: any) { } + } + return new Proxy({ name }, { + get(target, propKey) { + if (typeof propKey !== 'string') return undefined + if (target.name) { + return createPackage(target.name + '.' + propKey) + } else { + return createPackage(propKey) + } + } + }) +}) /** * 采用默认的计算线程池异步调用java方法,返回Promise接受结果 * @alpha @@ -51,4 +70,31 @@ export function invokeUi(javaobj: any, methodName: string, args?: any[]): Promise { return java.invokeUi(javaobj, methodName, args || []) +} +/** + * 用于向rhino一样访问java类,如 + * `Packages.java`或`Packages.javax` + * 此外该模块直接导出了常用的包 + * ```js + * import { java, android, com } from 'java' + * + * new java.io.File(...) + * ``` + */ +export const Packages = createPackage() + +const javaPackage = Packages.java +const androidPackage = Packages.android +const javaxPackage = Packages.javax +const comPackage = Packages.com +const netPackage = Packages.net +const androidxPackage = Packages.androidx + +export { + javaPackage as java, + androidPackage as android, + javaxPackage as javax, + comPackage as com, + netPackage as net, + androidxPackage as androidx, } \ No newline at end of file diff --git a/autojs/src/main/java/com/aiselp/autox/module/NodeModuleResolver.kt b/autojs/src/main/java/com/aiselp/autox/module/NodeModuleResolver.kt index c59407e81..b2987c012 100644 --- a/autojs/src/main/java/com/aiselp/autox/module/NodeModuleResolver.kt +++ b/autojs/src/main/java/com/aiselp/autox/module/NodeModuleResolver.kt @@ -19,8 +19,8 @@ class NodeModuleResolver( private val globalModuleDirectory: File ) : IV8ModuleResolver { private val esModuleCache = mutableMapOf() - val require = runtime.getNodeModule(NodeModuleModule::class.java).moduleObject - .invoke( + val require: V8ValueFunction = runtime.getNodeModule(NodeModuleModule::class.java).moduleObject + .invoke( NodeModuleModule.FUNCTION_CREATE_REQUIRE, workingDirectory.absolutePath ) @@ -34,6 +34,9 @@ class NodeModuleResolver( return parsingModule(v8Runtime, Uri.parse(s)) } val uri = Uri.parse(resourceName) + if (resourceName.startsWith("/")) { + return parsingModule(v8Runtime, uri) + } return when (uri.scheme) { "node" -> loadNodeModule(resourceName) null -> run { From 3e665e013bdbbd2f5503308977053351c73ace69 Mon Sep 17 00:00:00 2001 From: aiselp Date: Mon, 5 Aug 2024 22:16:57 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E5=89=8D=E5=8F=B0=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=8C=E6=88=AA=E5=9B=BE=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 2 - .../foreground/ForegroundService.java | 88 ------------------- .../org/autojs/autojs/ui/main/MainActivity.kt | 3 - .../autojs/ui/main/drawer/DrawerPage.kt | 27 +++--- autojs/src/main/AndroidManifest.xml | 7 ++ .../autojs/IndependentScriptService.kt | 75 ++++++++++++++-- .../image/capture/CaptureForegroundService.kt | 20 +++-- .../image/capture/ScreenCaptureManager.kt | 20 ++--- .../capture/ScreenCaptureRequestActivity.kt | 49 ++++++----- .../core/image/capture/ScreenCapturer.kt | 3 +- .../com/stardust/autojs/core/pref/Pref.kt | 14 +++ .../com/stardust/autojs/runtime/api/Images.kt | 11 ++- autojs/src/main/res-i18n/values/strings.xml | 1 + 13 files changed, 159 insertions(+), 161 deletions(-) delete mode 100644 app/src/main/java/org/autojs/autojs/external/foreground/ForegroundService.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 27637415d..bc876e0d9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -202,8 +202,6 @@ android:name="org.autojs.autojs.ui.error.IssueReporterActivity" android:theme="@style/IssueReporterTheme" /> - - = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - startForeground(FOREGROUND_SERVICE_TYPE_SPECIAL_USE, buildNotification()); - } else { - startForeground(NOTIFICATION_ID, buildNotification()); - } - } - - private Notification buildNotification() { - createNotificationChannel(); - // PendingIntent contentIntent = PendingIntent.getActivity(this, 0, MainActivity_.intent(this).get(), 0); - PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), FLAG_IMMUTABLE); - return new NotificationCompat.Builder(this, CHANEL_ID) - .setContentTitle(getString(R.string.foreground_notification_title)) - .setContentText(getString(R.string.foreground_notification_text)) - .setSmallIcon(R.drawable.autojs_logo) - .setWhen(System.currentTimeMillis()) - .setContentIntent(contentIntent) - .setChannelId(CHANEL_ID) - .setVibrate(new long[0]) - .build(); - } - - private void createNotificationChannel() { - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - assert manager != null; - CharSequence name = getString(R.string.foreground_notification_channel_name); - String description = getString(R.string.foreground_notification_channel_name); - NotificationChannel channel = new NotificationChannel(CHANEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT); - channel.setDescription(description); - channel.enableLights(false); - manager.createNotificationChannel(channel); - } - - @Override - public void onDestroy() { - stopForeground(true); - super.onDestroy(); - } -} diff --git a/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt b/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt index eb70e9f45..0e4a38a7a 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/MainActivity.kt @@ -77,7 +77,6 @@ import com.stardust.autojs.servicecomponents.EngineController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.autojs.autojs.Pref -import org.autojs.autojs.external.foreground.ForegroundService import org.autojs.autojs.timing.TimedTaskScheduler import org.autojs.autojs.ui.build.ProjectConfigActivity import org.autojs.autojs.ui.common.ScriptOperations @@ -117,8 +116,6 @@ class MainActivity : FragmentActivity() { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) Log.i("MainActivity", "Pid: ${Process.myPid()}") - if (Pref.isForegroundServiceEnabled()) ForegroundService.start(this) - else ForegroundService.stop(this) if (Pref.isFloatingMenuShown()) { if (DrawOverlaysPermission.isCanDrawOverlays(this)) FloatyWindowManger.showCircularMenu() diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerPage.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerPage.kt index ea8ff10ec..652e8fb4c 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerPage.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerPage.kt @@ -60,6 +60,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog +import androidx.core.content.edit import androidx.lifecycle.viewmodel.compose.viewModel import androidx.preference.PreferenceManager import coil.compose.rememberAsyncImagePainter @@ -68,6 +69,8 @@ import com.stardust.app.isOpPermissionGranted import com.stardust.app.permission.DrawOverlaysPermission import com.stardust.app.permission.DrawOverlaysPermission.launchCanDrawOverlaysSettings import com.stardust.app.permission.PermissionsSettingsUtil +import com.stardust.autojs.IndependentScriptService +import com.stardust.autojs.core.pref.PrefKey import com.stardust.enhancedfloaty.FloatyService import com.stardust.notification.NotificationListenerService import com.stardust.toast @@ -84,7 +87,6 @@ import kotlinx.coroutines.withContext import org.autojs.autojs.Pref import org.autojs.autojs.autojs.AutoJs import org.autojs.autojs.devplugin.DevPlugin -import org.autojs.autojs.external.foreground.ForegroundService import org.autojs.autojs.tool.AccessibilityServiceTool import org.autojs.autojs.tool.WifiTool import org.autojs.autojs.ui.build.MyTextField @@ -341,7 +343,6 @@ private fun BottomButtons() { fun exitCompletely(context: Context) { if (context is Activity) context.finish() FloatyWindowManger.hideCircularMenu() - ForegroundService.stop(context) context.stopService(Intent(context, FloatyService::class.java)) AutoJs.getInstance().scriptEngineService.stopAll() } @@ -418,6 +419,7 @@ private fun ConnectComputerSwitch() { ).show() } } + QRResult.QRUserCanceled -> {} QRResult.QRMissingPermission -> {} is QRResult.QRError -> {} @@ -602,10 +604,10 @@ private fun FloatingWindowSwitch() { val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult(), onResult = { - if (DrawOverlaysPermission.isCanDrawOverlays(context)){ + if (DrawOverlaysPermission.isCanDrawOverlays(context)) { FloatyWindowManger.showCircularMenu() isFloatingWindowShowing = true - }else isFloatingWindowShowing = false + } else isFloatingWindowShowing = false } ) SwitchItem( @@ -693,8 +695,8 @@ private fun UsageStatsPermissionSwitch() { private fun ForegroundServiceSwitch() { val context = LocalContext.current var isOpenForegroundServices by remember { - val default = PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.key_foreground_servie), false) + val default = com.stardust.autojs.core.pref.Pref.getDefault(context) + .getBoolean(PrefKey.KEY_FOREGROUND_SERVIE, false) mutableStateOf(default) } SwitchItem( @@ -707,15 +709,12 @@ private fun ForegroundServiceSwitch() { text = { Text(text = stringResource(id = R.string.text_foreground_service)) }, checked = isOpenForegroundServices, onCheckedChange = { - if (it) { - ForegroundService.start(context) - } else { - ForegroundService.stop(context) + com.stardust.autojs.core.pref.Pref.getDefault(context).edit(true) { + putBoolean(PrefKey.KEY_FOREGROUND_SERVIE, it) } - PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putBoolean(context.getString(R.string.key_foreground_servie), it) - .apply() + if (it) { + IndependentScriptService.startForeground(context) + } else IndependentScriptService.stopForeground(context) isOpenForegroundServices = it } ) diff --git a/autojs/src/main/AndroidManifest.xml b/autojs/src/main/AndroidManifest.xml index e85f46d04..4aef7f260 100644 --- a/autojs/src/main/AndroidManifest.xml +++ b/autojs/src/main/AndroidManifest.xml @@ -12,6 +12,11 @@ + + + + + = Build.VERSION_CODES.R) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + } else { + 0 + }, + ) + } + + private fun buildNotification(): Notification { + val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val name: CharSequence = "AutoJS Service" + val description = "script foreground service" + val channel = NotificationChannel( + CHANEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT + ) + channel.description = description + channel.enableLights(false) + manager.createNotificationChannel(channel) + + return NotificationCompat.Builder(this, CHANEL_ID) + .setContentTitle(getString(R.string.foreground_notification_title)) + .setContentText("前台服务运行中") + .setSmallIcon(R.drawable.autojs_logo) + .setWhen(System.currentTimeMillis()) + .setChannelId(CHANEL_ID) + .setVibrate(LongArray(0)) + .setOngoing(true) + .build() + } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - return super.onStartCommand(intent, flags, startId) + val action = intent?.action + when (action) { + ACTION_START_FOREGROUND -> startForeground() + + ACTION_STOP_FOREGROUND -> { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + } + } + return START_STICKY } override fun onDestroy() { @@ -43,12 +99,21 @@ class IndependentScriptService : Service() { return ScriptBinder(this, scope) } - fun initAutojs() { - - } - companion object { private const val TAG = "ScriptService" + private val CHANEL_ID = IndependentScriptService::class.java.name + "_foreground" + const val ACTION_START_FOREGROUND = "action_start_foreground" + const val ACTION_STOP_FOREGROUND = "action_stop_foreground" + fun startForeground(context: Context) { + val intent = Intent(context, IndependentScriptService::class.java) + intent.action = ACTION_START_FOREGROUND + context.startService(intent) + } + fun stopForeground(context: Context) { + val intent = Intent(context, IndependentScriptService::class.java) + intent.action = ACTION_STOP_FOREGROUND + context.startService(intent) + } } } \ No newline at end of file diff --git a/autojs/src/main/java/com/stardust/autojs/core/image/capture/CaptureForegroundService.kt b/autojs/src/main/java/com/stardust/autojs/core/image/capture/CaptureForegroundService.kt index e28d6d4ae..1cdc3cbdd 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/image/capture/CaptureForegroundService.kt +++ b/autojs/src/main/java/com/stardust/autojs/core/image/capture/CaptureForegroundService.kt @@ -6,13 +6,14 @@ import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.Intent +import android.content.pm.ServiceInfo import android.media.projection.MediaProjection import android.os.Build import android.os.Handler import android.os.IBinder import android.util.Log -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat import com.stardust.autojs.R import com.stardust.autojs.core.image.capture.ScreenCaptureRequestActivity @@ -43,16 +44,18 @@ class CaptureForegroundService : Service() { override fun onCreate() { super.onCreate() - startForeground(NOTIFICATION_ID, buildNotification()) + ServiceCompat.startForeground( + this, NOTIFICATION_ID, buildNotification(), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION + } else 0 + ) } private fun buildNotification(): Notification { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannel() - } - val flags = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 + createNotificationChannel() + val flags = PendingIntent.FLAG_IMMUTABLE val contentIntent = PendingIntent.getActivity( this, 0, Intent(this, ScreenCaptureRequestActivity::class.java), flags @@ -68,7 +71,6 @@ class CaptureForegroundService : Service() { .build() } - @RequiresApi(api = Build.VERSION_CODES.O) private fun createNotificationChannel() { val manager = (getSystemService(NOTIFICATION_SERVICE) as NotificationManager) val channel = NotificationChannel( @@ -111,7 +113,7 @@ class CaptureForegroundService : Service() { var mediaProjection: MediaProjection? = null private const val TAG = "CaptureService" private const val STOP = "STOP_SERVICE" - private const val NOTIFICATION_ID = 2 + private const val NOTIFICATION_ID = 26 private val CHANNEL_ID = CaptureForegroundService::class.java.name + ".foreground" private const val NOTIFICATION_TITLE = "前台截图服务运行中" } diff --git a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt index 66b62cf43..8b8060a26 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt +++ b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureManager.kt @@ -7,7 +7,6 @@ import android.media.projection.MediaProjection import android.media.projection.MediaProjectionManager import com.stardust.app.OnActivityResultDelegate import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.coroutineScope import java.util.concurrent.CancellationException class ScreenCaptureManager : ScreenCaptureRequester { @@ -20,23 +19,19 @@ class ScreenCaptureManager : ScreenCaptureRequester { screenCapture?.setOrientation(orientation, context) return } + context.startService(Intent(context, CaptureForegroundService::class.java)) val result = if (context is OnActivityResultDelegate.DelegateHost && context is Activity) { ScreenCaptureRequester.ActivityScreenCaptureRequester( context.onActivityResultDelegateMediator, context ).request() } else { - coroutineScope { - val result = CompletableDeferred() - ScreenCaptureRequestActivity.request(context, - object : ScreenCaptureRequestActivity.Callback { - override fun onResult(data: Intent?) { - if (data != null) { - result.complete(data) - } else result.cancel(CancellationException("data is null")) - } - }) - result.await() + val result = CompletableDeferred() + ScreenCaptureRequestActivity.request(context) { data -> + if (data != null) { + result.complete(data) + } else result.cancel(CancellationException("data is null")) } + result.await() } mediaProjection = (context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager).getMediaProjection( @@ -44,7 +39,6 @@ class ScreenCaptureManager : ScreenCaptureRequester { result ) CaptureForegroundService.mediaProjection = mediaProjection - context.startService(Intent(context, CaptureForegroundService::class.java)) screenCapture = ScreenCapturer(mediaProjection!!, orientation) } diff --git a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequestActivity.kt b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequestActivity.kt index 6463872e5..a93c9e0c5 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequestActivity.kt +++ b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCaptureRequestActivity.kt @@ -1,24 +1,26 @@ package com.stardust.autojs.core.image.capture -import android.app.Activity import android.content.Context import android.content.Intent +import android.media.projection.MediaProjectionManager import android.os.Bundle -import com.stardust.app.OnActivityResultDelegate -import com.stardust.autojs.core.image.capture.ScreenCaptureRequester.ActivityScreenCaptureRequester +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContract +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalContext import com.stardust.util.IntentExtras -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.delay /** * Created by Stardust on 2017/5/22. */ -class ScreenCaptureRequestActivity : Activity() { - interface Callback { +class ScreenCaptureRequestActivity : AppCompatActivity() { + fun interface Callback { fun onResult(data: Intent?) } - private val mOnActivityResultDelegateMediator = OnActivityResultDelegate.Mediator() private var mCallback: Callback? = null private var extraId = 0 override fun onCreate(savedInstanceState: Bundle?) { @@ -35,17 +37,16 @@ class ScreenCaptureRequestActivity : Activity() { return } - MainScope().launch { - val screenCaptureRequester = ActivityScreenCaptureRequester( - mOnActivityResultDelegateMediator, - this@ScreenCaptureRequestActivity - ) - val intent = try { - screenCaptureRequester.request() - } catch (e: Exception) { - null + setContent { + val requester = rememberLauncherForActivityResult(ScreenCaptureRequester()) { + mCallback?.onResult(it) + finish() + } + val context = LocalContext.current + LaunchedEffect(key1 = Unit) { + delay(10) + requester.launch(context) } - mCallback?.onResult(intent) } } @@ -55,10 +56,14 @@ class ScreenCaptureRequestActivity : Activity() { mCallback = null } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - mOnActivityResultDelegateMediator.onActivityResult(requestCode, resultCode, data) - IntentExtras.fromIdAndRelease(extraId) - finish() + class ScreenCaptureRequester : ActivityResultContract() { + override fun createIntent(context: Context, input: Context): Intent { + return (input.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager).createScreenCaptureIntent() + } + + override fun parseResult(resultCode: Int, intent: Intent?): Intent? { + return intent + } } companion object { diff --git a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.kt b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.kt index 0e5fd2790..4954c6e96 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.kt +++ b/autojs/src/main/java/com/stardust/autojs/core/image/capture/ScreenCapturer.kt @@ -7,7 +7,6 @@ import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.media.Image import android.media.ImageReader -import android.media.MediaRecorder import android.media.projection.MediaProjection import android.os.Handler import android.os.Looper @@ -47,13 +46,13 @@ class ScreenCapturer( val screenHeight = ScreenMetrics.getOrientationAwareScreenHeight(orientation) val screenWidth = ScreenMetrics.getOrientationAwareScreenWidth(orientation) mImageReader = createImageReader(screenWidth, screenHeight) - mVirtualDisplay = createVirtualDisplay(screenWidth, screenHeight, screenDensity) mediaProjection.registerCallback(object : MediaProjection.Callback() { override fun onStop() { available = false release() } }, mHandler) + mVirtualDisplay = createVirtualDisplay(screenWidth, screenHeight, screenDensity) } private fun createImageReader(width: Int, height: Int): ImageReader { diff --git a/autojs/src/main/java/com/stardust/autojs/core/pref/Pref.kt b/autojs/src/main/java/com/stardust/autojs/core/pref/Pref.kt index 7790ac3f6..d3e8886c6 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/pref/Pref.kt +++ b/autojs/src/main/java/com/stardust/autojs/core/pref/Pref.kt @@ -1,5 +1,7 @@ package com.stardust.autojs.core.pref +import android.content.Context +import android.content.SharedPreferences import androidx.preference.PreferenceManager; import com.stardust.app.GlobalAppContext @@ -14,4 +16,16 @@ object Pref { get() { return preferences.getBoolean("key_gesture_observing", false) } + + fun getDefault(context: Context): SharedPreferences { + return PreferenceManager.getDefaultSharedPreferences(context) + } +} + +object PrefKey { + const val KEY_STABLE_MODE = "key_stable_mode" + const val KEY_GESTURE_OBSERVING = "key_gesture_observing" + const val KEY_AUTO_BACKUP = "key_auto_backup" + const val KEY_FOREGROUND_SERVIE = "key_foreground_servie" + const val KEY_USE_VOLUME_CONTROL_RECORD = "key_use_volume_control_record" } \ No newline at end of file diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt b/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt index 311077ffb..fc4dc1ca8 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt +++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/Images.kt @@ -51,14 +51,19 @@ class Images( val colorFinder: ColorFinder = ColorFinder(mScreenMetrics) fun requestScreenCapture(orientation: Int): Boolean = runBlocking { - return@runBlocking runCatching { + try { mScreenCaptureRequester.requestScreenCapture( mContext, orientation ) captureScreen() - }.isSuccess + true + } catch (e: Exception) { + e.printStackTrace() + false + } } - fun stopScreenCapturer(){ + + fun stopScreenCapturer() { mScreenCaptureRequester.recycle() } diff --git a/autojs/src/main/res-i18n/values/strings.xml b/autojs/src/main/res-i18n/values/strings.xml index a89877668..78401b210 100644 --- a/autojs/src/main/res-i18n/values/strings.xml +++ b/autojs/src/main/res-i18n/values/strings.xml @@ -21,6 +21,7 @@ 打开侧拉菜单 关闭侧拉菜单 没有读取设备信息权限 + 脚本服务 From e7538ea330f58cac6b5df562ead9778bbbcfec27 Mon Sep 17 00:00:00 2001 From: aiselp Date: Tue, 6 Aug 2024 22:42:38 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0dialogs=E6=A8=A1=E5=9D=97?= =?UTF-8?q?,eventLoop=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Alert.mjs" | 12 + .../Confirm.mjs" | 8 + .../InputDialog.mjs" | 6 + .../MultiChoiceDialog.mjs" | 7 + .../SelectDialog.mjs" | 7 + .../SingleChoiceDialog.mjs" | 7 + ...0\345\257\271\350\257\235\346\241\206.mjs" | 36 ++ autojs/src/js-api/gulpfile.mjs | 14 +- .../src/js-api/src/dialogs/DialogFactory.ts | 111 ++++++ autojs/src/js-api/src/dialogs/dialogs.d.ts | 14 + autojs/src/js-api/src/dialogs/index.ts | 329 ++++++++++++++++++ autojs/src/js-api/src/dialogs/options.ts | 116 ++++++ .../js-api/src/vue-ui/modifierExtension.ts | 2 +- autojs/src/js-api/src/vue-ui/nodeOps.ts | 4 +- autojs/src/js-api/src/vue-ui/patchProp.ts | 7 +- autojs/src/main/AndroidManifest.xml | 4 + .../autox/activity/AppDialogActivity.kt | 89 +++++ .../java/com/aiselp/autox/api/JsDialogs.kt | 60 ++++ .../main/java/com/aiselp/autox/api/JsToast.kt | 8 +- .../main/java/com/aiselp/autox/api/JsUi.kt | 7 +- .../com/aiselp/autox/engine/EventLoopQueue.kt | 87 +++-- .../aiselp/autox/engine/NodeScriptEngine.kt | 2 + autojs/src/main/res/values/styles.xml | 6 + .../aiselp/autox/ui/material3/theme/Theme.kt | 2 +- 24 files changed, 910 insertions(+), 35 deletions(-) create mode 100644 "app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/Alert.mjs" create mode 100644 "app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/Confirm.mjs" create mode 100644 "app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/InputDialog.mjs" create mode 100644 "app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/MultiChoiceDialog.mjs" create mode 100644 "app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/SelectDialog.mjs" create mode 100644 "app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/SingleChoiceDialog.mjs" create mode 100644 "app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/ui\344\270\255\344\275\277\347\224\250\345\257\271\350\257\235\346\241\206.mjs" create mode 100644 autojs/src/js-api/src/dialogs/DialogFactory.ts create mode 100644 autojs/src/js-api/src/dialogs/dialogs.d.ts create mode 100644 autojs/src/js-api/src/dialogs/index.ts create mode 100644 autojs/src/js-api/src/dialogs/options.ts create mode 100644 autojs/src/main/java/com/aiselp/autox/activity/AppDialogActivity.kt create mode 100644 autojs/src/main/java/com/aiselp/autox/api/JsDialogs.kt diff --git "a/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/Alert.mjs" "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/Alert.mjs" new file mode 100644 index 000000000..8e94ec3b2 --- /dev/null +++ "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/Alert.mjs" @@ -0,0 +1,12 @@ +import { showAlertDialog } from 'dialogs' + +await showAlertDialog("AlertDialog", { + content: "对话框内容" +}) + + +await showAlertDialog("提示", { + content: "不可通过点击返回键和点击外部取消的对话框", + dismissOnBackPress: false, + dismissOnClickOutside: false, +}) \ No newline at end of file diff --git "a/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/Confirm.mjs" "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/Confirm.mjs" new file mode 100644 index 000000000..a799853b2 --- /dev/null +++ "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/Confirm.mjs" @@ -0,0 +1,8 @@ +import { showConfirmDialog } from 'dialogs' +import { showToast } from 'toast' + +const cilik = await showConfirmDialog("提示", { + content: "对话框内容" +}) + +showToast('你点击了:' + (cilik ? '确认' : '取消')) \ No newline at end of file diff --git "a/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/InputDialog.mjs" "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/InputDialog.mjs" new file mode 100644 index 000000000..6c95b5ddc --- /dev/null +++ "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/InputDialog.mjs" @@ -0,0 +1,6 @@ +import { showInputDialog } from 'dialogs' +import { showToast } from 'toast' + +const input = await showInputDialog("请输入一些内容") + +showToast('你输入了:' + input) \ No newline at end of file diff --git "a/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/MultiChoiceDialog.mjs" "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/MultiChoiceDialog.mjs" new file mode 100644 index 000000000..de387e202 --- /dev/null +++ "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/MultiChoiceDialog.mjs" @@ -0,0 +1,7 @@ +import { showMultiChoiceDialog } from 'dialogs' +import { showToast } from 'toast' + + +const select = await showMultiChoiceDialog("标题", ['item1', 'item2', 'item3']) + +showToast('你选择了:' + select) \ No newline at end of file diff --git "a/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/SelectDialog.mjs" "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/SelectDialog.mjs" new file mode 100644 index 000000000..bbc255f80 --- /dev/null +++ "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/SelectDialog.mjs" @@ -0,0 +1,7 @@ +import { showSelectDialog } from 'dialogs' +import { showToast } from 'toast' + + +const select = await showSelectDialog("标题", ['item1', 'item2', 'item3']) + +showToast('你选择了:' + select) \ No newline at end of file diff --git "a/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/SingleChoiceDialog.mjs" "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/SingleChoiceDialog.mjs" new file mode 100644 index 000000000..1623131d8 --- /dev/null +++ "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/SingleChoiceDialog.mjs" @@ -0,0 +1,7 @@ +import { showSingleChoiceDialog } from 'dialogs' +import { showToast } from 'toast' + + +const select = await showSingleChoiceDialog("标题", ['item1', 'item2', 'item3']) + +showToast('你选择了:' + select) \ No newline at end of file diff --git "a/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/ui\344\270\255\344\275\277\347\224\250\345\257\271\350\257\235\346\241\206.mjs" "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/ui\344\270\255\344\275\277\347\224\250\345\257\271\350\257\235\346\241\206.mjs" new file mode 100644 index 000000000..559e6d327 --- /dev/null +++ "b/app/src/main/assets/sample/v7/\345\257\271\350\257\235\346\241\206/ui\344\270\255\344\275\277\347\224\250\345\257\271\350\257\235\346\241\206.mjs" @@ -0,0 +1,36 @@ +import { showAlertDialog, DialogFactory } from 'dialogs' +import { + createApp, xml, startActivity, Icons, defineComponent, ModifierExtension +} from "vue-ui"; +import { showToast } from 'toast' + +/** + * 如果有ui界面,建议使用以下方法创建对话框,拥有更好的交互效果 + */ +const factory = new DialogFactory() + +function alert() { + factory.showAlertDialog('提示', { + content: '这是ui中的对话框' + }) +} + +async function confirm() { + const b = await factory.showConfirmDialog('提示', { + content: '这是一个确认框' + }) + showToast(b) +} +const app = createApp({ + render() { + return xml` + + + + <${factory.Dialog/**对话框的挂载点 */} /> + + ` + } +}) + +startActivity(app) \ No newline at end of file diff --git a/autojs/src/js-api/gulpfile.mjs b/autojs/src/js-api/gulpfile.mjs index cf2a2612d..de584ba88 100644 --- a/autojs/src/js-api/gulpfile.mjs +++ b/autojs/src/js-api/gulpfile.mjs @@ -26,6 +26,17 @@ export async function createPackageFile(cb) { ) cb() } +async function createRootPackageFile(cb) { + const n = JSON.parse(await fs.readFile('./package.json', 'utf8')) + const packageFile = { + name: n.name, + version: n.version, + description: n.description, + type: "module", + license: n.license, + } + await fs.writeFile('./dist/package.json', JSON.stringify(packageFile, undefined, 2)) +} export const build = series( clear, @@ -37,5 +48,6 @@ export const build = series( await Promise.all(option.output.map(bundle.write)) } }, - createPackageFile + createPackageFile, + createRootPackageFile, ) \ No newline at end of file diff --git a/autojs/src/js-api/src/dialogs/DialogFactory.ts b/autojs/src/js-api/src/dialogs/DialogFactory.ts new file mode 100644 index 000000000..3338c5f7e --- /dev/null +++ b/autojs/src/js-api/src/dialogs/DialogFactory.ts @@ -0,0 +1,111 @@ +import { xml } from "@/vue-ui" +import { shallowReactive, Component, defineComponent, reactive, watch } from "@vue/runtime-core" +import { DialogEvent, DialogInterface } from "." +import * as main from '.' +import { DialogBuilderOptions, IDialogs, InputDialogOptions } from "./options" +import { EventEmitter } from 'node:events' + +type DialogStatus = DialogOps & { + isShow: boolean +} + +class MutxDialog extends EventEmitter> implements DialogInterface { + state: DialogStatus + constructor(state: DialogStatus) { + super() + this.state = state + } + dismiss(): void { + this.state.isShow = false + } +} + +export default class DialogFactory implements IDialogs { + showin = shallowReactive(new Set<[Component, DialogStatus]>()) + + get Dialog(): Component { + const showin = this.showin + return defineComponent({ + render() { + const nodes = Array.from(showin).map((t) => { + const [comp, ops] = t + if (!ops.isShow) { + showin.delete(t) + return + } + return xml` + { ops.isShow = false }}> + <${comp} /> + + ` + }) + return xml` + ${nodes} + ` + } + }) + } + + _mountUi(comp: Component, ops: DialogOps): DialogInterface { + const state: DialogStatus = reactive({ + ...ops, + isShow: true + }) + const p = watch(() => state.isShow, (isShow) => { + if (isShow === false) { + ops.onDismiss(); + p() + } + }) + this.showin.add([comp, state]) + return new MutxDialog(state) + } + showDialog(options: DialogBuilderOptions): DialogInterface { + options.type = this + return main.showDialog({ + ...options, + type: this, + }) + } + showAlertDialog(title: string, options: DialogBuilderOptions): Promise { + return main.showAlertDialog(title, { + ...options, + type: this, + }) + } + showConfirmDialog(title: string, options: DialogBuilderOptions): Promise { + return main.showConfirmDialog(title, { + ...options, + type: this, + }) + } + showInputDialog(title: string, prefill?: string, options?: InputDialogOptions): Promise { + return main.showInputDialog(title, prefill, { + ...options, + type: this, + }) + } + showSelectDialog(title: string, items: string[], options?: DialogBuilderOptions): Promise { + return main.showSelectDialog(title, items, { + ...options, + type: this, + }) + } + showMultiChoiceDialog(title: string, items: string[], initialSelectedIndices?: number[], options?: DialogBuilderOptions): Promise { + return main.showMultiChoiceDialog(title, items, initialSelectedIndices, { + ...options, + type: this, + }) + } + showSingleChoiceDialog(title: string, items: string[], initialSelectedIndex?: number, options?: DialogBuilderOptions): Promise { + return main.showSingleChoiceDialog(title, items, initialSelectedIndex, { + ...options, + type: this, + }) + } + +} \ No newline at end of file diff --git a/autojs/src/js-api/src/dialogs/dialogs.d.ts b/autojs/src/js-api/src/dialogs/dialogs.d.ts new file mode 100644 index 000000000..c26b3114e --- /dev/null +++ b/autojs/src/js-api/src/dialogs/dialogs.d.ts @@ -0,0 +1,14 @@ +declare type TSecureTpye = 'SecureOn' | 'SecureOff' | 'Inherit' + +declare type DialogOps = { + dismissOnBackPress: boolean + dismissOnClickOutside: boolean + securePolicy?: TSecureTpye + onDismiss: () => void +} +declare type AppDialogBuilder = { + dismiss(): void +} +declare namespace root.dialogs { + function showDialog(element: ComposeElement, ops: DialogOps?): AppDialogBuilder +} \ No newline at end of file diff --git a/autojs/src/js-api/src/dialogs/index.ts b/autojs/src/js-api/src/dialogs/index.ts new file mode 100644 index 000000000..ec0b6daa7 --- /dev/null +++ b/autojs/src/js-api/src/dialogs/index.ts @@ -0,0 +1,329 @@ + +import { EventEmitter } from 'node:events' +import { xml, Component, ModifierExtension, defineComponent, createApp, ref, reactive } from '@/vue-ui' +import { nodeOps } from '@/vue-ui/nodeOps' +import { createDialogContent, DialogBuilderOptions, DialogEventListener, InputDialogOptions } from './options' +import { padding, fillMaxWidth, height, heightIn, clickable } from '@/vue-ui/modifierExtension' +import DialogFactory from './DialogFactory' + +const dialogs = Autox.dialogs + +export type DialogType = 'app' | 'overlay' | DialogFactory +export const defaultDialogType: DialogType = 'app' + + +export function showAppDialog(comp: Component, ops?: DialogOps) { + const el = nodeOps.createElement('box') + const app = createApp(comp) + app.mount(el) + const s = setInterval(() => { }, 2000) + return dialogs.showDialog(el.__xel, Object.assign({ + dismissOnBackPress: true + }, ops, { + onDismiss() { + app.unmount() + clearInterval(s) + ops?.onDismiss() + } + })) +} + +export enum DialogEvent { + /**@event */ + ON_DISMISS = 'dismiss', + /**@event */ + ON_POSITIVE = 'positive', + /**@event */ + ON_NEGATIVE = 'negative', + /**@event */ + ON_NEUTRAL = 'neutral', + /**@event */ + ON_INPUT_CHANGE = 'input_change', +} +export interface DialogInterface extends EventEmitter> { + dismiss(): void +} +class Dialog extends EventEmitter> + implements DialogInterface { + _nv?: AppDialogBuilder + destroyed: boolean = false + constructor() { + super() + this.once(DialogEvent.ON_DISMISS, () => { this.destroyed = true; }) + } + dismiss() { + this._nv?.dismiss() + } +} +export function showDialog(options: DialogBuilderOptions): DialogInterface { + const { type = defaultDialogType, dismissOnBackPress, dismissOnClickOutside } = options + const Content = createDialogContent(options) + let dialog: DialogInterface + const dialogEventListener: DialogEventListener = { + onPositive() { + dialog.emit(DialogEvent.ON_POSITIVE, dialog) + }, + onNegative() { + dialog.emit(DialogEvent.ON_NEGATIVE, dialog) + }, + onNeutral() { + dialog.emit(DialogEvent.ON_NEUTRAL, dialog) + }, + } + const comp = () => { + return xml` + <${Content} events=${dialogEventListener}/> + ` + } + const ops: DialogOps = { + dismissOnBackPress: (typeof dismissOnBackPress === 'boolean') ? dismissOnBackPress : true, + dismissOnClickOutside: (typeof dismissOnClickOutside === 'boolean') ? dismissOnClickOutside : true, + onDismiss() { + dialog.emit(DialogEvent.ON_DISMISS) + }, + } + + if (type === 'app') { + dialog = new Dialog(); + (dialog as Dialog)._nv = showAppDialog(comp, ops) + } else if (type instanceof DialogFactory) { + dialog = type._mountUi(comp, ops) + } else { + dialog = new Dialog(); + console.warn('Unknown Dialog type: ' + type); + } + return dialog +} +/** + * 显示一个消息提示对话框,返回一个Promise + * @param title + * @param options + * @returns Promise将在对话框消失时完成 + */ +export async function showAlertDialog(title: string, options?: DialogBuilderOptions) { + const f: DialogBuilderOptions = { + title: title, + positive: '确认', + } + const dialog = showDialog(Object.assign(f, options)) + + return new Promise((resolve, reject) => { + dialog.once(DialogEvent.ON_DISMISS, resolve) + dialog.once(DialogEvent.ON_POSITIVE, () => { dialog.dismiss() }) + }) +} +/** + * 显示一个确认对话框 + * @param title + * @param options + * @returns 只在点击positive按钮时返回true,其他情况返回false + */ +export async function showConfirmDialog(title: string, options?: DialogBuilderOptions) { + const f: DialogBuilderOptions = { + title: title, + positive: '确认', + negative: '取消', + } + const dialog = showDialog(Object.assign(f, options)) + let r = false + return new Promise((resolve, reject) => { + dialog.once(DialogEvent.ON_DISMISS, () => resolve(r)) + dialog.once(DialogEvent.ON_POSITIVE, () => { + r = true + dialog.dismiss() + }) + dialog.once(DialogEvent.ON_NEGATIVE, () => { dialog.dismiss() }) + }) +} +/** + * 显示一个输入框,提示用户输入信息 + * @param title + * @param prefill 输入框的默认内容 + * @param options + * @returns 点击positive时返回字符串,即使输入为空,被取消时返回null + */ +export async function showInputDialog(title: string, prefill?: string, options?: InputDialogOptions) { + let input = prefill || "" + const DialogContent = defineComponent(function () { + function updateInput(value: string) { + input = value + dialog.emit(DialogEvent.ON_INPUT_CHANGE, value, dialog) + } + return function render() { + return xml` + + ` + } + }) + + const f: InputDialogOptions = { + title: title, + inputPrefill: prefill, + positive: '确认', + negative: '取消', + } + const dialog = showDialog(Object.assign(f, options, { content: DialogContent, })) + + return new Promise((resolve, reject) => { + dialog.once(DialogEvent.ON_DISMISS, () => resolve(null)) + dialog.once(DialogEvent.ON_POSITIVE, () => { + resolve(input) + dialog.dismiss() + }) + dialog.once(DialogEvent.ON_NEGATIVE, () => { dialog.dismiss() }) + }) +} +/** + * 显示一个选择对话框,选中任意项后消失 + * @param title + * @param items 选项数组 + * @param options + * @returns 返回选中的项目索引,被取消则返回-1 + */ +export async function showSelectDialog(title: string, items: string[], options?: DialogBuilderOptions) { + let select = -1 + const DialogContent = defineComponent(function () { + function click(i: number) { + select = i + dialog.dismiss() + } + const modifier = [fillMaxWidth(), heightIn(50)] + return function render() { + return items.map((item, i) => { + const onClick = click.bind(undefined, i) + return xml` + + ${item} + + ` + }) + } + }) + + const f: InputDialogOptions = { + title: title + } + const dialog = showDialog(Object.assign(f, options, { content: DialogContent, })) + + return new Promise((resolve, reject) => { + dialog.once(DialogEvent.ON_DISMISS, () => resolve(select)) + }) +} + +/** + * 显示一个多选对话框 + * @param title + * @param items 可多选的项目 + * @param initialSelectedIndices 初始选中的项目索引数组 + * @param options + * @returns 返回选中的项目索引数组,被取消则返回`null` + */ +export async function showMultiChoiceDialog(title: string, + items: string[], + initialSelectedIndices?: number[], + options?: DialogBuilderOptions) { + let select = new Set() + + const DialogContent = defineComponent(function () { + const state = reactive(items.map(() => false)) + if (initialSelectedIndices) { + for (let i of initialSelectedIndices) { + if (i >= items.length) continue; + select.add(i) + state[i] = true; + } + } + function click(i: number) { + const r = state[i] = !state[i] + if (r) { + select.add(i) + } else select.delete(i); + } + const modifier = [fillMaxWidth(), heightIn(50)] + return function render() { + return items.map((item, i) => { + const onCheckedChange = click.bind(undefined, i) + return xml` + + + ${item} + + ` + }) + } + }) + + const f: InputDialogOptions = { + title: title, + positive: '确认', + negative: '取消', + } + const dialog = showDialog(Object.assign(f, options, { content: DialogContent, })) + + return new Promise((resolve, reject) => { + dialog.once(DialogEvent.ON_DISMISS, () => resolve(null)) + dialog.once(DialogEvent.ON_POSITIVE, () => { + resolve(Array.from(select)) + dialog.dismiss() + }) + dialog.once(DialogEvent.ON_NEGATIVE, () => { dialog.dismiss() }) + }) +} +/** + * 显示一个单选对话框 + * @param title + * @param items + * @param initialSelectedIndex + * @param options + * @returns 返回选中的项目索引,被取消则返回-1 + */ +export async function showSingleChoiceDialog(title: string, + items: string[], + initialSelectedIndex?: number, + options?: DialogBuilderOptions) { + let select = ref(initialSelectedIndex || 0) + + const DialogContent = defineComponent(function () { + function click(i: number) { + select.value = i + } + const modifier = [fillMaxWidth(), heightIn(50)] + return function render() { + return items.map((item, i) => { + const onCheckedChange = click.bind(undefined, i) + return xml` + + + ${item} + + ` + }) + } + }) + + const f: InputDialogOptions = { + title: title, + positive: '确认', + negative: '取消', + } + const dialog = showDialog(Object.assign(f, options, { content: DialogContent, })) + + return new Promise((resolve, reject) => { + dialog.once(DialogEvent.ON_DISMISS, () => resolve(-1)) + dialog.once(DialogEvent.ON_POSITIVE, () => { + resolve(select.value) + dialog.dismiss() + }) + dialog.once(DialogEvent.ON_NEGATIVE, () => { dialog.dismiss() }) + }) +} + +export { DialogFactory } \ No newline at end of file diff --git a/autojs/src/js-api/src/dialogs/options.ts b/autojs/src/js-api/src/dialogs/options.ts new file mode 100644 index 000000000..5531a529d --- /dev/null +++ b/autojs/src/js-api/src/dialogs/options.ts @@ -0,0 +1,116 @@ +import { Color } from "@/vue-ui/theme" +import { DialogType, DialogInterface } from "." +import { Component, defineComponent, h, VNode, xml, PropType, shallowReactive } from "@/vue-ui" +import { fillMaxWidth, heightIn, padding, verticalScroll, widthIn } from "@/vue-ui/modifierExtension" + +export type SecureTpye = TSecureTpye +/** + * 创建对话框的一些通用选项 + */ +export interface DialogBuilderOptions { + /**对话框标题 */ + title?: string + /**对话框内容,可以是一个vue组件对象 */ + content?: string | Component + /**仅在content是字符串时有效 */ + contentColor?: Color + /**尚未支持 */ + icon?: string + /**positive的文本,不设置将不显示positive按钮 */ + positive?: string + positiveColor?: Color + /**negative的文本,不设置将不显示negative按钮 */ + negative?: string + negativeColor?: Color + neutralColor?: Color + /**neutral的文本,不设置将不显示neutral按钮 */ + neutral?: string + /**是否允许返回键取消对话框,默认为true */ + dismissOnBackPress?: boolean + /**是否允许点击对话框外部取消对话框,默认为true */ + dismissOnClickOutside?: boolean + /**设置对话框窗口的安全策略 */ + securePolicy?: SecureTpye + type?: DialogType +} +export interface DialogEventListener { + onPositive?: () => void + onNegative?: () => void + onNeutral?: () => void +} +export interface InputDialogOptions extends DialogBuilderOptions { + /**输入框的提示 */ + inputHint?: string, + /**输入框的默认文本 */ + inputPrefill?: string, + /**输入框的lable */ + inputLable?: string +} + +function dialogButton(text?: string, color?: Color, onClick?: () => void) { + if (!text) return + return xml` + + ` +} + +export function createDialogContent(options: DialogBuilderOptions) { + const { title, content, contentColor, positive, positiveColor, icon, + negative, negativeColor, neutralColor, neutral, + } = options + return defineComponent({ + props: { + events: Object as PropType + }, + render() { + let contentVnode: any; + if (typeof content === 'string') { + contentVnode = xml`${content}` + } else if (content) { + contentVnode = xml`<${content}/>` + } + const positiveVnode = dialogButton(positive, positiveColor, this.$props.events?.onPositive) + const negativeVnode = dialogButton(negative, negativeColor, this.$props.events?.onNegative) + const neutralVnode = dialogButton(neutral, neutralColor, this.$props.events?.onNeutral) + + return xml` + + + + ${title} + + + ${contentVnode} + + + ${neutralVnode} + + ${negativeVnode} + ${positiveVnode} + + + + + ` + } + }) +} + +export interface IDialogs { + showDialog(options: DialogBuilderOptions): DialogInterface + showAlertDialog(title: string, options?: DialogBuilderOptions): Promise + showConfirmDialog(title: string, options?: DialogBuilderOptions): Promise + showInputDialog(title: string, prefill?: string, options?: InputDialogOptions): Promise + showSelectDialog(title: string, items: string[], options?: DialogBuilderOptions): Promise + showMultiChoiceDialog(title: string, + items: string[], + initialSelectedIndices?: number[], + options?: DialogBuilderOptions): Promise + showSingleChoiceDialog(title: string, + items: string[], + initialSelectedIndex?: number, + options?: DialogBuilderOptions): Promise + +} diff --git a/autojs/src/js-api/src/vue-ui/modifierExtension.ts b/autojs/src/js-api/src/vue-ui/modifierExtension.ts index efc82ef40..dabba8998 100644 --- a/autojs/src/js-api/src/vue-ui/modifierExtension.ts +++ b/autojs/src/js-api/src/vue-ui/modifierExtension.ts @@ -98,7 +98,7 @@ export function padding(left: number, top?: number, right?: number, bottom?: num * @param clickable * @returns */ -export function clickable(clickable: () => {}) { +export function clickable(clickable: () => void) { return loadFactory('clickable').createModifierExt([clickable]) } /** diff --git a/autojs/src/js-api/src/vue-ui/nodeOps.ts b/autojs/src/js-api/src/vue-ui/nodeOps.ts index fd8005325..17b241d9b 100644 --- a/autojs/src/js-api/src/vue-ui/nodeOps.ts +++ b/autojs/src/js-api/src/vue-ui/nodeOps.ts @@ -3,7 +3,7 @@ import { RendererOptions } from '@vue/runtime-core' import { PxNode, PxNodeTypes } from './types' import { createElement, insertElement, removeElement, setText } from './nativeRender'; - +export type DomRendererOptions = RendererOptions let nodeId: number = 0 export enum NodeOpTypes { @@ -93,7 +93,7 @@ export function logNodeOp(op: NodeOp) { } } -export const nodeOps: Omit, 'patchProp'> = { +export const nodeOps: Omit = { createElement(tag: string): PxElement { const node: PxElement = new PxElement(tag) logNodeOp({ diff --git a/autojs/src/js-api/src/vue-ui/patchProp.ts b/autojs/src/js-api/src/vue-ui/patchProp.ts index abf1867c2..6bcd88434 100644 --- a/autojs/src/js-api/src/vue-ui/patchProp.ts +++ b/autojs/src/js-api/src/vue-ui/patchProp.ts @@ -1,14 +1,17 @@ -import { NodeOpTypes, logNodeOp } from './nodeOps' +import { NodeOpTypes, logNodeOp, DomRendererOptions } from './nodeOps' import { isOn } from '@vue/shared' import { PxElement } from './types'; import { patchElementProp } from './nativeRender'; import { parseModifier } from './modifierParse'; -export function patchProp( +export const patchProp: DomRendererOptions['patchProp'] = function ( el: PxElement, key: string, prevValue: any, nextValue: any, + namespace, + prevChildren, + parentComponent ) { logNodeOp({ type: NodeOpTypes.PATCH, diff --git a/autojs/src/main/AndroidManifest.xml b/autojs/src/main/AndroidManifest.xml index 4aef7f260..cfe06cd66 100644 --- a/autojs/src/main/AndroidManifest.xml +++ b/autojs/src/main/AndroidManifest.xml @@ -41,6 +41,10 @@ android:configChanges="fontScale|keyboard|keyboardHidden|locale|navigation|orientation|screenLayout|screenSize|uiMode" android:process="@string/text_script_process_name" /> + () + fun showDialog(context: Context, builder: AppDialogBuilder, scope: CoroutineScope) = + scope.launch(Dispatchers.Main) { + val eid = id++ + dialogs[eid] = builder + context.startActivity( + Intent(context, AppDialogActivity::class.java) + .putExtra(TAG, eid) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } + + private const val TAG = "AppDialogActivity" + } +} \ No newline at end of file diff --git a/autojs/src/main/java/com/aiselp/autox/api/JsDialogs.kt b/autojs/src/main/java/com/aiselp/autox/api/JsDialogs.kt new file mode 100644 index 000000000..ed43de7ef --- /dev/null +++ b/autojs/src/main/java/com/aiselp/autox/api/JsDialogs.kt @@ -0,0 +1,60 @@ +package com.aiselp.autox.api + +import android.content.Context +import androidx.compose.ui.window.SecureFlagPolicy +import com.aiselp.autox.activity.AppDialogActivity +import com.aiselp.autox.api.ui.ComposeElement +import com.aiselp.autox.engine.EventLoopQueue +import com.caoccao.javet.annotations.V8Function +import com.caoccao.javet.interop.V8Runtime +import com.caoccao.javet.values.reference.V8ValueFunction +import com.caoccao.javet.values.reference.V8ValueObject +import kotlinx.coroutines.CoroutineScope + +class JsDialogs( + val eventLoopQueue: EventLoopQueue, + val context: Context, + val scope: CoroutineScope +) : NativeApi { + override val moduleId: String = ID + override fun install(v8Runtime: V8Runtime, global: V8ValueObject): NativeApi.BindingMode { + return NativeApi.BindingMode.PROXY + } + + override fun recycle(v8Runtime: V8Runtime, global: V8ValueObject) { + } + + @V8Function + fun showDialog( + element: ComposeElement, + listener: V8ValueObject? + ): AppDialogActivity.AppDialogBuilder { + val securePolicy = listener?.getString("securePolicy") + val builder = object : AppDialogActivity.AppDialogBuilder(element, scope) { + val dismissListener = listener?.get("onDismiss")?.let { + eventLoopQueue.createV8Callback(it) + } + override val dismissOnBackPress: Boolean = + listener?.getBoolean("dismissOnBackPress") ?: super.dismissOnBackPress + override val dismissOnClickOutside: Boolean = + listener?.getBoolean("dismissOnClickOutside") ?: super.dismissOnClickOutside + override val securePolicy: SecureFlagPolicy = when (securePolicy) { + "SecureOn" -> SecureFlagPolicy.SecureOn + "SecureOff" -> SecureFlagPolicy.SecureOff + else -> super.securePolicy + } + + override fun onDismiss() { + dismissListener?.invoke() + dismissListener?.close() + } + } + + AppDialogActivity.showDialog(context, builder, scope) + return builder + } + + companion object { + const val ID = "dialogs" + } +} \ No newline at end of file diff --git a/autojs/src/main/java/com/aiselp/autox/api/JsToast.kt b/autojs/src/main/java/com/aiselp/autox/api/JsToast.kt index 6f39a3ee7..5be680db5 100644 --- a/autojs/src/main/java/com/aiselp/autox/api/JsToast.kt +++ b/autojs/src/main/java/com/aiselp/autox/api/JsToast.kt @@ -7,7 +7,9 @@ import com.caoccao.javet.annotations.V8Property import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.values.reference.V8ValueObject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch class JsToast(private val context: Context, private val scope: CoroutineScope) : NativeApi { @@ -15,12 +17,14 @@ class JsToast(private val context: Context, private val scope: CoroutineScope) : @get:V8Property(name = "SHORT") val SHORT = Toast.LENGTH_SHORT + @get:V8Property(name = "LONG") val LONG = Toast.LENGTH_LONG + + @OptIn(DelicateCoroutinesApi::class) @V8Function - @JvmOverloads fun showToast(msg: String, duration: Int = Toast.LENGTH_SHORT) = - scope.launch(Dispatchers.Main) { + GlobalScope.launch(Dispatchers.Main) { Toast.makeText(context, msg, duration).show() } diff --git a/autojs/src/main/java/com/aiselp/autox/api/JsUi.kt b/autojs/src/main/java/com/aiselp/autox/api/JsUi.kt index 6e9b6fac0..6cc33a2f7 100644 --- a/autojs/src/main/java/com/aiselp/autox/api/JsUi.kt +++ b/autojs/src/main/java/com/aiselp/autox/api/JsUi.kt @@ -96,11 +96,6 @@ class JsUi(nodeScriptEngine: NodeScriptEngine) : NativeApi { @V8Function fun patchProp(element: ComposeElement, key: String, value: V8Value?) { val value1 = converterValue(value) - element.props[key]?.let { - if (it is EventLoopQueue.V8Callback) { - it.close() - } - } element.props[key] = value1 } @@ -140,7 +135,7 @@ class JsUi(nodeScriptEngine: NodeScriptEngine) : NativeApi { private fun converterValue(value: V8Value?): Any? { return if (value is V8ValueFunction) { - eventLoopQueue.createV8Callback(value) + eventLoopQueue.createWeakV8Callback(value) } else { converter.toObject(value) } diff --git a/autojs/src/main/java/com/aiselp/autox/engine/EventLoopQueue.kt b/autojs/src/main/java/com/aiselp/autox/engine/EventLoopQueue.kt index 4e06e2d07..c878b49f9 100644 --- a/autojs/src/main/java/com/aiselp/autox/engine/EventLoopQueue.kt +++ b/autojs/src/main/java/com/aiselp/autox/engine/EventLoopQueue.kt @@ -31,7 +31,7 @@ class EventLoopQueue(val runtime: NodeRuntime) { return id; }, emit: function(id, ...args){ - callbacks.get(id)(...args); + return callbacks.get(id)?.(...args); }, removeCallback: function(id){ callbacks.delete(callbacks.get(id)); @@ -68,26 +68,28 @@ class EventLoopQueue(val runtime: NodeRuntime) { } fun createV8Callback(fn: V8ValueFunction): V8Callback { + val id = util.invoke("addCallback", fn) id.use { - return V8Callback(id.asLong()) + return LastingV8Callback(id.asLong()) } } - fun removeV8Callback(callback: V8Callback) { - addTask { - util.invokeVoid("removeCallback", callback.id) - } + fun createWeakV8Callback(fn: V8ValueFunction): V8Callback { + return WeakV8Callback(fn) } - fun executeQueue(): Boolean = synchronized(this) { - val executeQueue = currentQueue - if (executeQueue.isEmpty()) { - return false + fun executeQueue(): Boolean { + val executeQueue: ArrayDeque + synchronized(this) { + executeQueue = currentQueue + if (executeQueue.isEmpty()) { + return false + } + currentQueue = if (currentQueue === queue) { + queueX + } else queue } - currentQueue = if (currentQueue === queue) { - queueX - } else queue executeQueue.forEach { it.run() } executeQueue.clear() return true @@ -103,8 +105,11 @@ class EventLoopQueue(val runtime: NodeRuntime) { util.close() } - inner class V8Callback(val id: Long):AutoCloseable { - @Volatile + /** + * 此回调会在js上下文中创建一个持久引用,使用完毕应调用close, + * 此类实现了线程安全,可在任意线程中调用 + */ + inner class LastingV8Callback(val id: Long) : V8Callback { private var removerd = false private fun call(vararg args: Any?): Any? { @@ -115,7 +120,7 @@ class EventLoopQueue(val runtime: NodeRuntime) { } else return result } - fun invoke(vararg args: Any?): CompletableDeferred { + override fun invoke(vararg args: Any?): CompletableDeferred { if (removerd) { Log.w(TAG, "this callback[${id}] has been removed") return CompletableDeferred().also { it.complete(null) } @@ -125,22 +130,64 @@ class EventLoopQueue(val runtime: NodeRuntime) { return deferred } - fun invokeSync(vararg args: Any?): Any? = runBlocking { + override fun invokeSync(vararg args: Any?): Any? = runBlocking { invoke(*args).await() } - suspend fun invokeAsync(vararg args: Any?): Any? { + override suspend fun invokeAsync(vararg args: Any?): Any? { return invoke(*args).await() } override fun close() { removerd = true - this@EventLoopQueue.removeV8Callback(this) + addTask { + util.invokeVoid("removeCallback", id) + } } + } - fun cancel() { + /** + * 弱引用回调,当V8ValueFunction在js上下文失去引用并被回收后,此回调将失效 + */ + inner class WeakV8Callback(val fn: V8ValueFunction) : V8Callback { + init { + fn.setWeak() + } + private fun call(vararg args: Any?): Any? { + if (fn.isClosed) { + Log.d(TAG, "this callback[${fn}] has been closed") + return null + } + val result = runtime.converter.toObject(fn.call(null, *args)) + if (result is V8Value) { + result.close() + return null + } else return result } + + override fun invoke(vararg args: Any?): CompletableDeferred { + val deferred = CompletableDeferred(job) + this@EventLoopQueue.addTask { deferred.complete(call(*args)) } + return deferred + } + + override fun invokeSync(vararg args: Any?): Any? = runBlocking { + invoke(*args).await() + } + + override suspend fun invokeAsync(vararg args: Any?): Any? { + return invoke(*args).await() + } + + override fun close() { + } + } + + interface V8Callback : AutoCloseable { + fun invoke(vararg args: Any?): CompletableDeferred + fun invokeSync(vararg args: Any?): Any? + suspend fun invokeAsync(vararg args: Any?): Any? } companion object { diff --git a/autojs/src/main/java/com/aiselp/autox/engine/NodeScriptEngine.kt b/autojs/src/main/java/com/aiselp/autox/engine/NodeScriptEngine.kt index cedcb6519..92c4e0a6a 100644 --- a/autojs/src/main/java/com/aiselp/autox/engine/NodeScriptEngine.kt +++ b/autojs/src/main/java/com/aiselp/autox/engine/NodeScriptEngine.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Log import com.aiselp.autox.api.JavaInteractor import com.aiselp.autox.api.JsClipManager +import com.aiselp.autox.api.JsDialogs import com.aiselp.autox.api.JsEngines import com.aiselp.autox.api.JsMedia import com.aiselp.autox.api.JsToast @@ -111,6 +112,7 @@ class NodeScriptEngine(val context: Context, val uiHandler: UiHandler) : nativeApiManager.register(JavaInteractor(scope, converter, promiseFactory)) nativeApiManager.register(JsToast(context, scope)) nativeApiManager.register(JsMedia(context)) + nativeApiManager.register(JsDialogs(eventLoopQueue, context, scope)) nativeApiManager.register(JsEngines(this)) nativeApiManager.initialize(runtime, global) } diff --git a/autojs/src/main/res/values/styles.xml b/autojs/src/main/res/values/styles.xml index a2f2a0b41..8a31be359 100644 --- a/autojs/src/main/res/values/styles.xml +++ b/autojs/src/main/res/values/styles.xml @@ -27,6 +27,12 @@ true false + +