diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json
index 55f25342..5d4f3f55 100644
--- a/.changelog/lang_zh-Hans.json
+++ b/.changelog/lang_zh-Hans.json
@@ -11,7 +11,8 @@
"images.compressToBytes 方法, 用于压缩图像并生成字节数组",
"images.downsample 方法, 用于像素降采样并生成新的 ImageWrapper",
"ui.keepScreenOn 方法, 用于 UI 页面获取焦点时保持设备屏幕常亮",
- "ui.root 属性 (getter), 用于获取 UI 页面布局的 \"窗口内容根容器\" 节点"
+ "ui.root 属性 (getter), 用于获取 UI 页面布局的 \"窗口内容根容器\" 节点",
+ "webview 元素支持基于 JsBridge 的 Web 页面布局 (Ref to [Auto.js Pro](https://g.pro.autojs.org/)) [参阅 示例代码 > 布局 > 可交互 HTML / Vue2 + Vant (SFC)] _[`issue #281`](http://issues.autojs6.com/281)_"
],
"fix": [
"主页文档标签显示在线文档时部分内容被系统导航栏遮挡的问题",
@@ -19,6 +20,7 @@
"主题色设置页面调色盘对话框可能无限叠加的问题",
"无障碍服务关闭时音量加键停止所有脚本功能失效的问题",
"定时任务页面编辑自定义广播内容时出现的输入法遮挡问题",
+ "webview 元素中的控件无法正常激活输入法软键盘的问题",
"APK 文件类型信息对话框可能无法获取应用名称及 SDK 信息的问题",
"文件管理器示例代码进入项目目录时可能无法自动加载子目录文件内容的问题",
"Android 15 UI 模式顶部内容被状态栏覆盖的问题",
@@ -45,6 +47,7 @@
"代码编辑器格式化代码支持 `??`, `?.`, `??=` 等运算符",
"打包单文件时自动读取并勾选已安装应用的声明权限 _[`issue #362`](http://issues.autojs6.com/362)_",
"意图相关操作 (编辑/查看/安装/发送/播放等) 增加操作异常提示",
+ "webview 元素的 url 属性支持相对路径",
"ImageWrapper#saveTo 方法的路径参数支持相对路径",
"images.save 方法使用 quality 参数时支持 png 格式的文件体积压缩 _[`issue #367`](http://issues.autojs6.com/367)_",
"已忽略更新记录及客户端模式连接地址记录支持清空操作",
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 9e5e7252..38fd91bf 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -67,6 +67,11 @@
+
+
+
+
+
diff --git a/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/main.js b/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/main.js
new file mode 100644
index 00000000..48beabf3
--- /dev/null
+++ b/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/main.js
@@ -0,0 +1,93 @@
+'ui';
+
+ui.layout(
+
+
+ ,
+);
+
+ui.statusBarColor('#ffffff');
+
+let FILE_REQ_CODE = 11525;
+let fileChooserResolve = null;
+let web = ui['web'];
+
+// 监听 WebView 的控制台消息, 打印到控制台
+web.events.on('console_message', (event, msg) => {
+ console.log(`${files.getName(msg.sourceId())}:${msg.lineNumber()}: ${msg.message()}`);
+});
+
+// 处理来自 Web 的请求
+web.jsBridge
+ // 处理读取本地文件的请求
+ .handle('fetch', (event, args) => {
+ return files.read(files.join(files.path('web'), args.path));
+ })
+ // 处理显示日志界面的请求
+ .handle('show-log', (event) => {
+ app.startActivity('console');
+ })
+ // 处理 toastLog 消息
+ .handle('toast-log', (event, msg) => {
+ toastLog(msg);
+ })
+ // 处理设置无障碍服务的请求
+ .handle('set-accessibility-enabled', (event, enabled) => {
+ enabled ? auto.enable() : auto.disable();
+ })
+ // 处理获取无障碍服务状态的请求
+ .handle('get-accessibility-enabled', (event) => {
+ return auto.isRunning();
+ })
+ // 处理获取应用版本名称的请求
+ .handle('get-app-version-name', (event) => {
+ return app.autojs.versionName;
+ })
+ // 处理文件选择的请求
+ .handle('select-file', (event, mimeType) => {
+ return new Promise((resolve, reject) => {
+ if (fileChooserResolve) {
+ toastLog('File chooser is already open.');
+ return;
+ }
+ fileChooserResolve = resolve;
+
+ let intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType(mimeType);
+ intent.addCategory(android.content.Intent.CATEGORY_OPENABLE);
+ activity.startActivityForResult(intent, FILE_REQ_CODE);
+ });
+
+ })
+ // 处理显示设备信息的请求
+ .handle('show-device-info-dialog', (event) => {
+ new MaterialDialog.Builder(activity)
+ .title(R.strings.text_app_and_device_info)
+ .content(DeviceUtils.getDeviceSummaryWithSimpleAppInfo(activity))
+ .neutralText(R.strings.dialog_button_copy)
+ .onNeutral((dialog, which) => {
+ setClip(dialog.contentView.text);
+ toast(activity.getString(R.strings.text_already_copied_to_clip));
+ })
+ .neutralColorRes(R.color.dialog_button_hint)
+ .negativeText(R.strings.dialog_button_dismiss)
+ .build()
+ .show();
+ })
+ // 处理打开链接的请求. 这里用广播方式, 也可以使用 handle 的 "请求-响应" 方式
+ .on('open-url', (event, url) => {
+ app.openUrl(url);
+ });
+
+ui.emitter.on('activity_result', (requestCode, resultCode, data) => {
+ if (requestCode === FILE_REQ_CODE && fileChooserResolve) {
+ if (resultCode === android.app.Activity.RESULT_OK && data) {
+ let uri = data.getData();
+ let path = IntentUtils.getFileName(activity, uri);
+ fileChooserResolve(path);
+ } else {
+ fileChooserResolve(null);
+ }
+ fileChooserResolve = null;
+ }
+});
\ No newline at end of file
diff --git a/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/project.json b/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/project.json
new file mode 100644
index 00000000..6594fd48
--- /dev/null
+++ b/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/project.json
@@ -0,0 +1,16 @@
+{
+ "assets": [],
+ "useFeatures": [],
+ "launchConfig": {
+ "displaySplash": true,
+ "hideLogs": true,
+ "splashText": "Powered by AutoJs6",
+ "stableMode": false
+ },
+ "main": "main.js",
+ "name": "Vue2 + Vant (SFC)",
+ "packageName": "org.example.vue2_vant",
+ "scripts": {},
+ "versionCode": 1,
+ "versionName": "1.0.0"
+}
\ No newline at end of file
diff --git a/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/web/index.html b/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/web/index.html
new file mode 100644
index 00000000..efe24472
--- /dev/null
+++ b/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/web/index.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/web/main.vue b/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/web/main.vue
new file mode 100644
index 00000000..e4758fac
--- /dev/null
+++ b/app/src/main/assets-app/sample/布局/Vue2 + Vant (SFC)/web/main.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 运行
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets-app/sample/布局/可交互 HTML/index.html b/app/src/main/assets-app/sample/布局/可交互 HTML/index.html
new file mode 100644
index 00000000..9567f131
--- /dev/null
+++ b/app/src/main/assets-app/sample/布局/可交互 HTML/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ Interactive HTML
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets-app/sample/布局/可交互 HTML/index.js b/app/src/main/assets-app/sample/布局/可交互 HTML/index.js
new file mode 100644
index 00000000..f29cb833
--- /dev/null
+++ b/app/src/main/assets-app/sample/布局/可交互 HTML/index.js
@@ -0,0 +1,10 @@
+const winOnloadBak = window.onload ? window.onload.bind(window) : null;
+
+window.onload = function () {
+ if (typeof winOnloadBak === 'function') {
+ winOnloadBak();
+ }
+ document.getElementById('testButton').addEventListener('click', () => {
+ $autojs.invoke('toast-log', '按钮已被点击');
+ });
+};
\ No newline at end of file
diff --git a/app/src/main/assets-app/sample/布局/可交互 HTML/main.js b/app/src/main/assets-app/sample/布局/可交互 HTML/main.js
new file mode 100644
index 00000000..93971144
--- /dev/null
+++ b/app/src/main/assets-app/sample/布局/可交互 HTML/main.js
@@ -0,0 +1,13 @@
+'ui';
+
+ui.layout(
+
+
+ ,
+);
+
+let web = ui['web'];
+
+web.jsBridge.handle('toast-log', (event, msg) => {
+ toastLog(msg);
+});
\ No newline at end of file
diff --git a/app/src/main/assets-app/sample/布局/可交互 HTML/project.json b/app/src/main/assets-app/sample/布局/可交互 HTML/project.json
new file mode 100644
index 00000000..8a1dd34b
--- /dev/null
+++ b/app/src/main/assets-app/sample/布局/可交互 HTML/project.json
@@ -0,0 +1,16 @@
+{
+ "assets": [],
+ "useFeatures": [],
+ "launchConfig": {
+ "displaySplash": true,
+ "hideLogs": true,
+ "splashText": "Powered by AutoJs6",
+ "stableMode": false
+ },
+ "main": "main.js",
+ "name": "可交互 HTML",
+ "packageName": "org.example.interactive_html",
+ "scripts": {},
+ "versionCode": 1,
+ "versionName": "1.0.0"
+}
\ No newline at end of file
diff --git a/app/src/main/assets-app/sample/布局/可交互 HTML/style.css b/app/src/main/assets-app/sample/布局/可交互 HTML/style.css
new file mode 100644
index 00000000..5147679e
--- /dev/null
+++ b/app/src/main/assets-app/sample/布局/可交互 HTML/style.css
@@ -0,0 +1,25 @@
+html, body {
+ height: 100%;
+ margin: 0;
+}
+
+body {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: #f5f5f5;
+}
+
+#testButton {
+ padding: 12px 24px;
+ font-size: 16px;
+ color: #fff;
+ background-color: #006400;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+#testButton:hover {
+ background-color: #008000;
+}
diff --git a/app/src/main/assets/modules/ui-ext.js b/app/src/main/assets/modules/ui-ext.js
new file mode 100644
index 00000000..9046963b
--- /dev/null
+++ b/app/src/main/assets/modules/ui-ext.js
@@ -0,0 +1,163 @@
+( /* @ModuleIIFE */ () => {
+
+ // @Reference to module __ui__.js from Auto.js Pro 9.3.11 on May 24, 2025.
+
+ let JsBridge = (function () {
+ let ResultAdapter = require('result-adapter');
+
+ let EVENT_REQUEST = '$autojs:internal:request';
+ let EVENT_RESPONSE = '$autojs:internal:response';
+
+ function JavaScriptBridgeImpl(webview) {
+ let self = this;
+ events.__asEmitter__(self);
+ self.nextId = 1;
+ self.requestHandlers = new Map();
+ self.webview = webview;
+ webview.setJavascriptEventCallback({
+ onWebJavaScriptEvent(event, args) {
+ let obj = unwrapJson(args) || [];
+ self.emit.apply(self, [ event, { name: event } ].concat(obj));
+ },
+ });
+ self.on(EVENT_REQUEST, function (e, request) {
+ let handler = self.requestHandlers.get(request.channel) ?? self.requestHandlers.get('');
+ if (!handler) {
+ self.sendResponseError(request, new Error('no handler for action: ' + request.channel));
+ return;
+ }
+ let event = {
+ channel: request.channel,
+ arguments: request.args,
+ };
+ let result;
+ try {
+ result = handler.apply(void 0, [ event ].concat(event.arguments));
+ } catch (e) {
+ self.sendResponseError(request, e);
+ return;
+ }
+ if (isPromise(result)) {
+ result.then(function (r) {
+ self.sendResponse(request, r);
+ }).catch(function (err) {
+ self.sendResponseError(request, err);
+ });
+ } else {
+ self.sendResponse(request, result);
+ }
+ });
+ return self;
+ }
+
+ JavaScriptBridgeImpl.prototype.sendResponse = function (request, result, error) {
+ this.send(EVENT_RESPONSE + ':' + request.id, {
+ result: result,
+ error: error,
+ });
+ };
+ JavaScriptBridgeImpl.prototype.sendResponseError = function (request, error) {
+ this.sendResponse(request, undefined, error.toString());
+ };
+ JavaScriptBridgeImpl.prototype.invoke = function (channel) {
+ let self = this;
+ let args = [];
+ for (let _i = 1; _i < arguments.length; _i++) {
+ args[_i - 1] = arguments[_i];
+ }
+ let id = this.nextId++;
+ return new Promise(function (resolve, reject) {
+ self.once(EVENT_RESPONSE + ':' + id, function (event, result) {
+ if (result.error) {
+ reject(new Error('Error occurred while handling invoke: channel = ' + channel + ', error = ' + result.error));
+ } else {
+ resolve(result.result);
+ }
+ });
+ self.send(EVENT_REQUEST, {
+ id: id,
+ channel: channel,
+ args: args,
+ });
+ });
+ };
+ JavaScriptBridgeImpl.prototype.send = function (event) {
+ let args = [];
+ for (let i = 1; i < arguments.length; i++) {
+ args[i - 1] = arguments[i];
+ }
+ this.webview.sendEventToWebJavaScript(event, wrapJson(args));
+ };
+ JavaScriptBridgeImpl.prototype.handle = function (channel, handler) {
+ this.requestHandlers.set(channel !== null && channel !== void 0 ? channel : '', handler);
+ return this;
+ };
+ JavaScriptBridgeImpl.prototype.eval = function (code) {
+ let self = this;
+ return new Promise(function (resolve, reject) {
+ ResultAdapter.promise(self.webview.__eval(code))
+ .then(result => resolve(JSON.parse(String(result))))
+ .catch(err => reject(err));
+ });
+ };
+
+ function unwrapJson(maybeJson) {
+ if (!maybeJson) {
+ return undefined;
+ }
+ return JSON.parse(maybeJson);
+ }
+
+ function wrapJson(obj) {
+ if (typeof obj === 'undefined') {
+ return undefined;
+ }
+ return JSON.stringify(obj);
+ }
+
+ function isPromise(obj) {
+ return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
+ }
+
+ return JavaScriptBridgeImpl;
+ })();
+
+ /**
+ * @param {org.autojs.autojs.core.ui.widget.JsWebView} webview
+ */
+ function initWebView(webview) {
+ webview.jsBridge = new JsBridge(webview);
+ let emitter = events.emitter();
+ webview.events = emitter;
+ webview.setSyncWebViewEventCallback({
+ onSyncWebViewEvent(event) {
+ dispatchJavaEvent(event, emitter);
+ },
+ });
+ webview.setSyncEventEnabled('', true);
+
+ function dispatchJavaEvent(event, emitter) {
+ let eventName = event.getName();
+ let args = Array.from(event.getArguments());
+ let _returnValue;
+ let returnValueSet = false;
+ let e = {
+ name: eventName,
+ arguments: args,
+ consumed: false,
+ };
+ Object.defineProperty(e, 'returnValue', {
+ get: function () {
+ return _returnValue;
+ },
+ set: function (value) {
+ _returnValue = value;
+ returnValueSet = true;
+ },
+ });
+ emitter.emit.apply(emitter, [ eventName, e ].concat(args));
+ }
+ }
+
+ module.exports = initWebView;
+})();
\ No newline at end of file
diff --git a/app/src/main/assets/web/dist/autojs.sdk.v1.js b/app/src/main/assets/web/dist/autojs.sdk.v1.js
new file mode 100644
index 00000000..5832905e
--- /dev/null
+++ b/app/src/main/assets/web/dist/autojs.sdk.v1.js
@@ -0,0 +1,90 @@
+'use strict';
+
+( /* @IIFE */ () => {
+
+ // @Reference to .../asset/web/dist/autojs.sdk.v1.js from Auto.js Pro 9.3.11 on May 24, 2025.
+
+ const EVENT_RESPONSE = '$autojs:internal:response';
+ const EVENT_REQUEST = '$autojs:internal:request';
+
+ // noinspection JSValidateTypes
+ /** @type {import('events')} */
+ let nodejsEvents = events;
+
+ let nextId = 1;
+
+ let helper = {
+ isPromise: o => o && typeof o.then === 'function',
+ unwrapJson: o => o ? JSON.parse(o) : undefined,
+ wrapJson: o => o === undefined ? o : JSON.stringify(o),
+ };
+
+ Object.setPrototypeOf($autojs, new nodejsEvents.EventEmitter());
+
+ /** @type {Internal.AutoJsBridge} */
+ let AutoJsBridge = {
+ requestHandlers: {},
+ onEventInternal(event, args) {
+ this.emit(event, ...helper.unwrapJson(args));
+ },
+ send(event, ...args) {
+ this.sendEventInternal(event, helper.wrapJson(args));
+ },
+ invoke(channel, ...args) {
+ let id = nextId++;
+ return new Promise((resolve, reject) => {
+ this.once(EVENT_RESPONSE + ':' + id, (result) => {
+ if (result.error) {
+ reject(new Error('Error occurred while handling invoke: channel = ' + channel + ', error = ' + result.error));
+ } else {
+ resolve(result.result);
+ }
+ });
+ this.send(EVENT_REQUEST, {
+ id, channel, args,
+ });
+ });
+ },
+ /** @this {Internal.AutoJsBridge} */
+ handle(channel, handler) {
+ this.requestHandlers[channel || ''] = handler;
+ return this;
+ },
+ removeHandler(channel) {
+ delete this.requestHandlers[channel];
+ },
+ handleRequest(request) {
+ let handler = this.requestHandlers[request.channel] || this.requestHandlers[''];
+ if (!handler) {
+ return;
+ }
+ let event = {
+ channel: request.channel,
+ arguments: request.args,
+ };
+ let result;
+ try {
+ result = handler(event, ...event.arguments);
+ } catch (e) {
+ this.sendResponse(request, undefined, e);
+ return;
+ }
+ if (helper.isPromise(request)) {
+ result.then((r) => {
+ this.sendResponse(request, r);
+ }).catch((err) => {
+ this.sendResponse(request, undefined, err);
+ });
+ } else {
+ this.sendResponse(request, result);
+ }
+ },
+ sendResponse(request, result, error) {
+ this.send(EVENT_RESPONSE + ':' + request.id, { result, error });
+ },
+ };
+
+ $autojs = Object.assign($autojs, AutoJsBridge);
+ $autojs.on(EVENT_REQUEST, AutoJsBridge.handleRequest.bind($autojs));
+
+})();
diff --git a/app/src/main/assets/web/dist/events.min@3.3.0.js b/app/src/main/assets/web/dist/events.min@3.3.0.js
new file mode 100644
index 00000000..6978fe30
--- /dev/null
+++ b/app/src/main/assets/web/dist/events.min@3.3.0.js
@@ -0,0 +1,23 @@
+// @Reference to .../asset/web/dist/events.min@3.3.0.js from Auto.js Pro 9.3.11 on May 24, 2025.
+
+((function (root, factory) {
+ var name = 'events';
+ if (typeof define === 'function' && define.amd) {
+ define(name, [], function () {
+ return factory({});
+ });
+ } else if (typeof module === 'object' && module.exports) {
+ factory(module);
+ } else {
+ root[name] = factory({});
+ }
+ })(this, function (module) {
+ /**
+ * Minified by jsDelivr using Terser v5.3.5.
+ * Original file: /npm/events@3.3.0/events.js
+ *
+ * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
+ */
+ "use strict";var ReflectOwnKeys,R="object"==typeof Reflect?Reflect:null,ReflectApply=R&&"function"==typeof R.apply?R.apply:function(e,t,n){return Function.prototype.apply.call(e,t,n)};function ProcessEmitWarning(e){console&&console.warn&&console.warn(e)}ReflectOwnKeys=R&&"function"==typeof R.ownKeys?R.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var NumberIsNaN=Number.isNaN||function(e){return e!=e};function EventEmitter(){EventEmitter.init.call(this)}module.exports=EventEmitter,module.exports.once=once,EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._eventsCount=0,EventEmitter.prototype._maxListeners=void 0;var defaultMaxListeners=10;function checkListener(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function _getMaxListeners(e){return void 0===e._maxListeners?EventEmitter.defaultMaxListeners:e._maxListeners}function _addListener(e,t,n,r){var i,o,s;if(checkListener(n),void 0===(o=e._events)?(o=e._events=Object.create(null),e._eventsCount=0):(void 0!==o.newListener&&(e.emit("newListener",t,n.listener?n.listener:n),o=e._events),s=o[t]),void 0===s)s=o[t]=n,++e._eventsCount;else if("function"==typeof s?s=o[t]=r?[n,s]:[s,n]:r?s.unshift(n):s.push(n),(i=_getMaxListeners(e))>0&&s.length>i&&!s.warned){s.warned=!0;var u=new Error("Possible EventEmitter memory leak detected. "+s.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");u.name="MaxListenersExceededWarning",u.emitter=e,u.type=t,u.count=s.length,ProcessEmitWarning(u)}return e}function onceWrapper(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function _onceWrap(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},i=onceWrapper.bind(r);return i.listener=n,r.wrapFn=i,i}function _listeners(e,t,n){var r=e._events;if(void 0===r)return[];var i=r[t];return void 0===i?[]:"function"==typeof i?n?[i.listener||i]:[i]:n?unwrapListeners(i):arrayClone(i,i.length)}function listenerCount(e){var t=this._events;if(void 0!==t){var n=t[e];if("function"==typeof n)return 1;if(void 0!==n)return n.length}return 0}function arrayClone(e,t){for(var n=new Array(t),r=0;r0&&(o=t[0]),o instanceof Error)throw o;var s=new Error("Unhandled error."+(o?" ("+o.message+")":""));throw s.context=o,s}var u=i[e];if(void 0===u)return!1;if("function"==typeof u)ReflectApply(u,this,t);else{var f=u.length,v=arrayClone(u,f);for(n=0;n=0;o--)if(n[o]===t||n[o].listener===t){s=n[o].listener,i=o;break}if(i<0)return this;0===i?n.shift():spliceOne(n,i),1===n.length&&(r[e]=n[0]),void 0!==r.removeListener&&this.emit("removeListener",e,s||t)}return this},EventEmitter.prototype.off=EventEmitter.prototype.removeListener,EventEmitter.prototype.removeAllListeners=function(e){var t,n,r;if(void 0===(n=this._events))return this;if(void 0===n.removeListener)return 0===arguments.length?(this._events=Object.create(null),this._eventsCount=0):void 0!==n[e]&&(0==--this._eventsCount?this._events=Object.create(null):delete n[e]),this;if(0===arguments.length){var i,o=Object.keys(n);for(r=0;r=0;r--)this.removeListener(e,t[r]);return this},EventEmitter.prototype.listeners=function(e){return _listeners(this,e,!0)},EventEmitter.prototype.rawListeners=function(e){return _listeners(this,e,!1)},EventEmitter.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):listenerCount.call(e,t)},EventEmitter.prototype.listenerCount=listenerCount,EventEmitter.prototype.eventNames=function(){return this._eventsCount>0?ReflectOwnKeys(this._events):[]};
+ return EventEmitter;
+}));
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/core/ui/attribute/WebViewAttributes.kt b/app/src/main/java/org/autojs/autojs/core/ui/attribute/WebViewAttributes.kt
index f01e06df..da51ff5e 100644
--- a/app/src/main/java/org/autojs/autojs/core/ui/attribute/WebViewAttributes.kt
+++ b/app/src/main/java/org/autojs/autojs/core/ui/attribute/WebViewAttributes.kt
@@ -4,6 +4,7 @@ import android.view.View
import android.webkit.WebView
import org.autojs.autojs.core.ui.inflater.ResourceParser
import org.autojs.autojs.core.ui.inflater.util.Strings
+import org.autojs.autojs.extension.StringExtensions.isUri
import org.autojs.autojs.runtime.ScriptRuntime
open class WebViewAttributes(scriptRuntime: ScriptRuntime, resourceParser: ResourceParser, view: View) : ViewGroupAttributes(scriptRuntime, resourceParser, view) {
@@ -13,7 +14,7 @@ open class WebViewAttributes(scriptRuntime: ScriptRuntime, resourceParser: Resou
override fun onRegisterAttrs(scriptRuntime: ScriptRuntime) {
super.onRegisterAttrs(scriptRuntime)
- registerAttr("url") { view.loadUrl(it) }
+ registerAttr("url") { view.loadUrl(if (it.isUri()) it else scriptRuntime.files.nonNullPath(it)) }
registerAttrs(arrayOf("scale", "initialScale")) { view.setInitialScale(it.toInt()) }
registerAttrs(arrayOf("enableNetwork", "networkAvailable")) { view.setNetworkAvailable(it.toBoolean()) }
registerAttr("blockNetworkImage") { view.settings.blockNetworkImage = it.toBoolean() }
diff --git a/app/src/main/java/org/autojs/autojs/core/ui/widget/EventWebView.kt b/app/src/main/java/org/autojs/autojs/core/ui/widget/EventWebView.kt
new file mode 100644
index 00000000..3e455d46
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/core/ui/widget/EventWebView.kt
@@ -0,0 +1,599 @@
+package org.autojs.autojs.core.ui.widget
+
+import android.annotation.SuppressLint
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.net.http.SslError
+import android.os.Message
+import android.util.AttributeSet
+import android.view.KeyEvent
+import android.view.View
+import android.webkit.ClientCertRequest
+import android.webkit.ConsoleMessage
+import android.webkit.GeolocationPermissions
+import android.webkit.HttpAuthHandler
+import android.webkit.JavascriptInterface
+import android.webkit.JsPromptResult
+import android.webkit.JsResult
+import android.webkit.PermissionRequest
+import android.webkit.RenderProcessGoneDetail
+import android.webkit.SafeBrowsingResponse
+import android.webkit.SslErrorHandler
+import android.webkit.ValueCallback
+import android.webkit.WebChromeClient
+import android.webkit.WebChromeClient.FileChooserParams
+import android.webkit.WebResourceError
+import android.webkit.WebResourceRequest
+import android.webkit.WebResourceResponse
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.annotation.AnyThread
+import androidx.annotation.Keep
+import androidx.core.net.toUri
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import org.autojs.autojs.annotation.ScriptInterface
+import org.autojs.autojs.app.GlobalAppContext
+import org.autojs.autojs.app.OnActivityResultDelegate
+import org.autojs.autojs.event.CoroutineSyncEventHost
+import org.autojs.autojs.event.CoroutineSyncEventHost.Companion.Event
+import org.autojs.autojs.event.EventResult
+import org.autojs.autojs.event.IEventEmitter
+import org.autojs.autojs.runtime.api.Mime
+import org.autojs.autojs.runtime.api.Resolvable
+import org.autojs.autojs.util.ContextUtils.findActivity
+import org.autojs.autojs.util.RhinoUtils
+import org.autojs.autojs.util.RhinoUtils.coerceBoolean
+import java.io.ByteArrayInputStream
+import java.nio.charset.StandardCharsets
+import java.util.*
+
+/**
+ * Created by SuperMonster003 on May 23, 2025.
+ */
+// @Reference to Auto.js Pro 9.3.11 by SuperMonster003 on May 23, 2025.
+@Keep
+abstract class EventWebView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0,
+) : WebView(context, attrs, defStyleAttr, defStyleRes), IEventEmitter {
+
+ private val bridge = AutoJs(this)
+
+ private val pendingEvents = Collections.synchronizedList(mutableListOf())
+
+ private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
+
+ private val syncEventHost = CoroutineSyncEventHost(coroutineScope) { event ->
+ when (event.sync) {
+ true -> syncWebViewEventCallback?.onSyncWebViewEvent(event)
+ else -> webViewEventCallback?.onWebViewEvent(event)
+ } != null
+ }.apply { onError = ::onError }
+
+ @Volatile
+ var webViewEventCallback: WebViewEventCallback? = null
+
+ @Volatile
+ var syncWebViewEventCallback: SyncViewEventCallback? = null
+
+ @Volatile
+ var javascriptEventCallback: JavaScriptEventCallback? = null
+ private set
+
+ init {
+ initSettings()
+ setWebViewClient(InternalClient())
+ setWebChromeClient(InternalChromeClient())
+ addJavascriptInterface(bridge, "\$autojs")
+ }
+
+ private fun initSettings() = settings.run {
+ @SuppressLint("SetJavaScriptEnabled")
+ javaScriptEnabled = true
+
+ javaScriptCanOpenWindowsAutomatically = true
+ domStorageEnabled = true
+ builtInZoomControls = false
+ allowFileAccess = true
+
+ @Suppress("DEPRECATION")
+ allowFileAccessFromFileURLs = true
+
+ @Suppress("DEPRECATION")
+ allowUniversalAccessFromFileURLs = true
+ }
+
+ @AnyThread
+ @ScriptInterface
+ fun evalInternal(code: String): Resolvable {
+ val promise = newPromise()
+ RhinoUtils.dispatchToMainThread {
+ evaluateJavascript(code) { promise.resolve(it) }
+ }
+ return promise
+ }
+
+ override fun emitEvent(event: String, vararg args: Any?): EventResult {
+ return emitInScope(event, coroutineScope, *args)
+ }
+
+ fun emitInScope(eventName: String, scope: CoroutineScope, vararg args: Any?): EventResult {
+ return syncEventHost.emitInScope(eventName, scope, *args)
+ }
+
+ private inner class InternalClient : WebViewClient() {
+
+ override fun doUpdateVisitedHistory(view: WebView, url: String?, isReload: Boolean) {
+ if (emitEvent("update_visited_history", view, url, isReload).callSuper) {
+ super.doUpdateVisitedHistory(view, url, isReload)
+ }
+ }
+
+ override fun onFormResubmission(view: WebView, dontResend: Message?, resend: Message?) {
+ if (emitEvent("form_resubmission", view, dontResend, resend).callSuper) {
+ super.onFormResubmission(view, dontResend, resend)
+ }
+ }
+
+ override fun onLoadResource(view: WebView, url: String?) {
+ if (emitEvent("load_resource", view, url).callSuper) {
+ super.onLoadResource(view, url)
+ }
+ }
+
+ override fun onPageCommitVisible(view: WebView, url: String?) {
+ if (emitEvent("page_commit_visible", view, url).callSuper) {
+ super.onPageCommitVisible(view, url)
+ }
+ }
+
+ override fun onPageFinished(view: WebView, url: String?) {
+ if (emitEvent("page_finished", view, url).callSuper) {
+ super.onPageFinished(view, url)
+ }
+ }
+
+ override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
+ if (emitEvent("page_started", view, url, favicon).callSuper) {
+ super.onPageStarted(view, url, favicon)
+ }
+ }
+
+ override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest?) {
+ if (emitEvent("received_client_cert_request", view, request).callSuper) {
+ super.onReceivedClientCertRequest(view, request)
+ }
+ }
+
+ override fun onReceivedError(view: WebView, request: WebResourceRequest?, error: WebResourceError?) {
+ if (emitEvent("received_error", view, request, error).callSuper) {
+ super.onReceivedError(view, request, error)
+ }
+ }
+
+ override fun onReceivedHttpAuthRequest(view: WebView, handler: HttpAuthHandler?, host: String?, realm: String?) {
+ if (emitEvent("received_http_auth_request", view, handler, host, realm).callSuper) {
+ super.onReceivedHttpAuthRequest(view, handler, host, realm)
+ }
+ }
+
+ override fun onReceivedHttpError(view: WebView, request: WebResourceRequest?, errorResponse: WebResourceResponse?) {
+ if (emitEvent("received_http_error", view, request, errorResponse).callSuper) {
+ super.onReceivedHttpError(view, request, errorResponse)
+ }
+ }
+
+ override fun onReceivedLoginRequest(view: WebView, realm: String?, account: String?, args: String?) {
+ if (emitEvent("received_login_request", view, realm, account, args).callSuper) {
+ super.onReceivedLoginRequest(view, realm, account, args)
+ }
+ }
+
+ override fun onReceivedSslError(view: WebView, handler: SslErrorHandler?, error: SslError?) {
+ if (emitEvent("received_ssl_error", view, handler, error).callSuper) {
+ super.onReceivedSslError(view, handler, error)
+ }
+ }
+
+ override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail?): Boolean {
+ val eventResult = emitEvent("render_process_gone", view, detail)
+ val superResult = super.onRenderProcessGone(view, detail)
+ return when {
+ eventResult.callSuper -> superResult
+ else -> coerceBoolean(eventResult.result, superResult)
+ }
+ }
+
+ override fun onSafeBrowsingHit(view: WebView, request: WebResourceRequest?, threatType: Int, callback: SafeBrowsingResponse?) {
+ if (emitEvent("safe_browsing_hit", view, request, threatType, callback).callSuper) {
+ super.onSafeBrowsingHit(view, request, threatType, callback)
+ }
+ }
+
+ override fun onScaleChanged(view: WebView, oldScale: Float, newScale: Float) {
+ if (emitEvent("scale_changed", view, oldScale, newScale).callSuper) {
+ super.onScaleChanged(view, oldScale, newScale)
+ }
+ }
+
+ @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
+ override fun onTooManyRedirects(view: WebView, cancelMsg: Message?, continueMsg: Message?) {
+ if (emitEvent("too_many_redirects", view, cancelMsg, continueMsg).callSuper) {
+ super.onTooManyRedirects(view, cancelMsg, continueMsg)
+ }
+ }
+
+ override fun onUnhandledKeyEvent(view: WebView, event: KeyEvent?) {
+ if (emitEvent("unhandled_key_event", view, event).callSuper) {
+ super.onUnhandledKeyEvent(view, event)
+ }
+ }
+
+ @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
+ override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? {
+ // 本地注入 AutoJs SDK
+ if (url?.toUri() == AUTOJS_SDK_URI) {
+ return WebResourceResponse(
+ /* mimeType = */ "application/javascript",
+ /* encoding = */ "UTF-8",
+ /* statusCode = */ 200,
+ /* reasonPhrase = */ "OK",
+ /* responseHeaders = */ emptyMap(),
+ /* data = */ ByteArrayInputStream(AUTOJS_SDK_JS)
+ )
+ }
+ val eventResult = emitEvent("should_intercept_request", view, url)
+ val superResult = super.shouldInterceptRequest(view, url)
+ return when {
+ eventResult.callSuper -> superResult
+ else -> eventResult.result as? WebResourceResponse? ?: superResult
+ }
+ }
+
+ override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
+ // 本地注入 AutoJs SDK
+ if (request.url == AUTOJS_SDK_URI) {
+ return WebResourceResponse(
+ /* mimeType = */ "application/javascript",
+ /* encoding = */ "UTF-8",
+ /* statusCode = */ 200,
+ /* reasonPhrase = */ "OK",
+ /* responseHeaders = */ emptyMap(),
+ /* data = */ ByteArrayInputStream(AUTOJS_SDK_JS)
+ )
+ }
+ val eventResult = emitEvent("should_intercept_request", view, request)
+ val superResult = super.shouldInterceptRequest(view, request)
+ return when {
+ eventResult.callSuper -> superResult
+ else -> eventResult.result as? WebResourceResponse? ?: superResult
+ }
+ }
+
+ override fun shouldOverrideKeyEvent(view: WebView?, event: KeyEvent?): Boolean {
+ val eventResult = emitEvent("should_override_key_event", view, event)
+ val superResult = super.shouldOverrideKeyEvent(view, event)
+ return when {
+ eventResult.callSuper -> superResult
+ else -> coerceBoolean(eventResult.result, superResult)
+ }
+ }
+
+ override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
+ val eventResult = emitEvent("should_override_url_loading", view, request)
+ val superResult = super.shouldOverrideUrlLoading(view, request)
+ return when {
+ eventResult.callSuper -> superResult
+ else -> coerceBoolean(eventResult.result, superResult)
+ }
+ }
+
+ }
+
+ private inner class InternalChromeClient : WebChromeClient() {
+
+ override fun getDefaultVideoPoster(): Bitmap? {
+ val eventResult = emitEvent("get_default_video_poster")
+ val superResult = super.getDefaultVideoPoster()
+ return when {
+ eventResult.callSuper -> superResult
+ else -> eventResult.result as? Bitmap? ?: superResult
+ }
+ }
+
+ override fun getVideoLoadingProgressView(): View? {
+ val eventResult = emitEvent("get_video_loading_progress_view")
+ val superResult = super.getVideoLoadingProgressView()
+ return when {
+ eventResult.callSuper -> superResult
+ else -> eventResult.result as? View? ?: superResult
+ }
+ }
+
+ override fun getVisitedHistory(callback: ValueCallback?>?) {
+ if (emitEvent("get_visited_history", callback).callSuper) {
+ super.getVisitedHistory(callback)
+ }
+ }
+
+ override fun onCloseWindow(window: WebView?) {
+ if (emitEvent("close_window", window).callSuper) {
+ super.onCloseWindow(window)
+ }
+ }
+
+ override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
+ val eventResult = emitEvent("console_message", consoleMessage)
+ val superResult = super.onConsoleMessage(consoleMessage)
+ return when {
+ eventResult.callSuper -> superResult
+ else -> coerceBoolean(eventResult.result, superResult)
+ }
+ }
+
+ override fun onCreateWindow(view: WebView?, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message?): Boolean {
+ val eventResult = emitEvent("create_window", view, isDialog, isUserGesture, resultMsg)
+ val superResult = super.onCreateWindow(view, isDialog, isUserGesture, resultMsg)
+ return when {
+ eventResult.callSuper -> superResult
+ else -> coerceBoolean(eventResult.result, superResult)
+ }
+ }
+
+ override fun onGeolocationPermissionsHidePrompt() {
+ if (emitEvent("geolocation_permissions_hide_prompt").callSuper) {
+ super.onGeolocationPermissionsHidePrompt()
+ }
+ }
+
+ override fun onGeolocationPermissionsShowPrompt(origin: String?, callback: GeolocationPermissions.Callback?) {
+ if (emitEvent("geolocation_permissions_show_prompt", origin, callback).callSuper) {
+ super.onGeolocationPermissionsShowPrompt(origin, callback)
+ }
+ }
+
+ override fun onHideCustomView() {
+ if (emitEvent("hide_custom_view").callSuper) {
+ super.onHideCustomView()
+ }
+ }
+
+ override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
+ val eventResult = emitEvent("js_alert", view, url, message, result)
+ val superResult = super.onJsAlert(view, url, message, result)
+ return when {
+ eventResult.callSuper -> superResult
+ else -> coerceBoolean(eventResult.result, superResult)
+ }
+ }
+
+ override fun onJsBeforeUnload(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
+ val eventResult = emitEvent("js_before_unload", view, url, message, result)
+ val superResult = super.onJsBeforeUnload(view, url, message, result)
+ return when {
+ eventResult.callSuper -> superResult
+ else -> coerceBoolean(eventResult.result, superResult)
+ }
+ }
+
+ override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
+ val eventResult = emitEvent("js_confirm", view, url, message, result)
+ val superResult = super.onJsConfirm(view, url, message, result)
+ return when {
+ eventResult.callSuper -> superResult
+ else -> coerceBoolean(eventResult.result, superResult)
+ }
+ }
+
+ override fun onJsPrompt(view: WebView?, url: String?, message: String?, defaultValue: String?, result: JsPromptResult?): Boolean {
+ val eventResult = emitEvent("js_prompt", view, url, message, defaultValue, result)
+ val superResult = super.onJsPrompt(view, url, message, defaultValue, result)
+ return when {
+ eventResult.callSuper -> superResult
+ else -> coerceBoolean(eventResult.result, superResult)
+ }
+ }
+
+ @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
+ override fun onJsTimeout(): Boolean {
+ val eventResult = emitEvent("js_timeout")
+ val superResult = super.onJsTimeout()
+ return when {
+ eventResult.callSuper -> superResult
+ else -> coerceBoolean(eventResult.result, superResult)
+ }
+ }
+
+ override fun onPermissionRequest(request: PermissionRequest?) {
+ if (emitEvent("permission_request", request).callSuper) {
+ super.onPermissionRequest(request)
+ }
+ }
+
+ override fun onPermissionRequestCanceled(request: PermissionRequest?) {
+ if (emitEvent("permission_request_canceled", request).callSuper) {
+ super.onPermissionRequestCanceled(request)
+ }
+ }
+
+ override fun onProgressChanged(view: WebView?, newProgress: Int) {
+ if (emitEvent("progress_changed", view, newProgress).callSuper) {
+ super.onProgressChanged(view, newProgress)
+ }
+ }
+
+ override fun onReceivedIcon(view: WebView?, icon: Bitmap?) {
+ if (emitEvent("received_icon", view, icon).callSuper) {
+ super.onReceivedIcon(view, icon)
+ }
+ }
+
+ override fun onReceivedTitle(view: WebView?, title: String?) {
+ if (emitEvent("received_title", view, title).callSuper) {
+ super.onReceivedTitle(view, title)
+ }
+ }
+
+ override fun onReceivedTouchIconUrl(view: WebView?, url: String?, precomposed: Boolean) {
+ if (emitEvent("received_touch_icon_url", view, url, precomposed).callSuper) {
+ super.onReceivedTouchIconUrl(view, url, precomposed)
+ }
+ }
+
+ override fun onRequestFocus(view: WebView?) {
+ if (emitEvent("request_focus", view).callSuper) {
+ super.onRequestFocus(view)
+ }
+ }
+
+ override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
+ if (emitEvent("show_custom_view", view, callback).callSuper) {
+ super.onShowCustomView(view, callback)
+ }
+ }
+
+ override fun onShowFileChooser(webView: WebView?, filePathCallback: ValueCallback?>?, fileChooserParams: FileChooserParams?): Boolean {
+ val eventResult = emitEvent("show_file_chooser", webView, filePathCallback, fileChooserParams)
+ val superResult = super.onShowFileChooser(webView, filePathCallback, fileChooserParams)
+ if (!eventResult.callSuper) {
+ return coerceBoolean(eventResult.result, superResult)
+ }
+ var handled = false
+ try {
+ val activity = webView?.context?.findActivity()
+ if (activity is OnActivityResultDelegate.DelegateHost) {
+ val mediator = activity.getOnActivityResultDelegateMediator()
+ val intent = fileChooserParams?.createIntent()
+ val type = intent?.type
+ if (type != null && type.startsWith(".")) {
+ intent.type = Mime.fromFile(type)
+ }
+ try {
+ activity.startActivityForResult(intent, FILE_CHOOSER_REQUEST_CODE)
+ mediator.addDelegate(FileChooserCallback(filePathCallback, mediator))
+ handled = true
+ } catch (e: ActivityNotFoundException) {
+ e.printStackTrace()
+ filePathCallback?.onReceiveValue(null)
+ }
+ }
+ } catch (t: Throwable) {
+ t.printStackTrace()
+ }
+ return handled || superResult
+ }
+
+ }
+
+ private inner class FileChooserCallback(
+ private val filePathCallback: ValueCallback?>?,
+ private val mediator: OnActivityResultDelegate.Mediator,
+ ) : OnActivityResultDelegate {
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == FILE_CHOOSER_REQUEST_CODE) {
+ filePathCallback?.onReceiveValue(FileChooserParams.parseResult(resultCode, data))
+ emitEvent("file_chooser_result", requestCode, resultCode, data)
+ mediator.removeDelegate(this)
+ }
+ }
+
+ }
+
+ open fun onError(t: Throwable) {
+ org.autojs.autojs.AutoJs.instance.globalConsole.error(t)
+ t.printStackTrace()
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ coroutineScope.cancel()
+ }
+
+ interface JavaScriptEventCallback {
+ fun onWebJavaScriptEvent(eventName: String, args: String?)
+ }
+
+ interface WebViewEventCallback {
+ fun onWebViewEvent(event: Event)
+ }
+
+ interface SyncViewEventCallback {
+ fun onSyncWebViewEvent(event: Event)
+ }
+
+ @ScriptInterface
+ fun sendEventToWebJavaScript(event: String, jsonArgs: String?) {
+ val eventName = escapeToStr(event)
+ val args = escapeToStr(jsonArgs ?: "")
+ val js = "\$autojs?.onEventInternal?.($eventName, $args)"
+ RhinoUtils.dispatchToMainThread {
+ evaluateJavascript(js, null)
+ }
+ }
+
+ @ScriptInterface
+ fun setJavascriptEventCallback(callback: JavaScriptEventCallback) {
+ javascriptEventCallback = callback
+ synchronized(pendingEvents) {
+ for (pendingEvent in pendingEvents) {
+ callback.onWebJavaScriptEvent(pendingEvent.event, pendingEvent.jsonArgs)
+ }
+ pendingEvents.clear()
+ }
+ }
+
+ @ScriptInterface
+ fun setSyncEventEnabled(event: String, alwaysSync: Boolean) {
+ val coroutineSyncEventHost = syncEventHost
+ if (event.isEmpty()) {
+ coroutineSyncEventHost.alwaysSync = alwaysSync
+ } else {
+ coroutineSyncEventHost.syncEventTable.put(event, alwaysSync)
+ }
+ }
+
+ abstract fun escapeToStr(src: String): String
+
+ abstract fun newPromise(): Resolvable
+
+ companion object {
+
+ const val FILE_CHOOSER_REQUEST_CODE = 24009
+
+ val AUTOJS_SDK_URI: Uri = "autojs://sdk/v1.js".toUri()
+
+ val AUTOJS_SDK_JS: ByteArray by lazy {
+ val assets = GlobalAppContext.get().applicationContext.assets
+ val eventsJs = assets.open("web/dist/events.min@3.3.0.js").bufferedReader().readText()
+ val autoJsJs = assets.open("web/dist/autojs.sdk.v1.js").bufferedReader().readText()
+ (eventsJs + "\n" + autoJsJs).toByteArray(StandardCharsets.UTF_8)
+ }
+
+ data class PendingJsEvent(val event: String, val jsonArgs: String)
+
+ @Keep
+ class AutoJs(private val webView: EventWebView) {
+
+ @JavascriptInterface
+ fun sendEventInternal(event: String, jsonArgs: String) {
+ val callback = webView.javascriptEventCallback
+ if (callback == null) {
+ webView.pendingEvents.add(PendingJsEvent(event, jsonArgs))
+ } else {
+ callback.onWebJavaScriptEvent(event, jsonArgs)
+ }
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/core/ui/widget/JsToolbar.kt b/app/src/main/java/org/autojs/autojs/core/ui/widget/JsToolbar.kt
index de50dd47..77e24251 100644
--- a/app/src/main/java/org/autojs/autojs/core/ui/widget/JsToolbar.kt
+++ b/app/src/main/java/org/autojs/autojs/core/ui/widget/JsToolbar.kt
@@ -2,11 +2,11 @@ package org.autojs.autojs.core.ui.widget
import android.app.Activity
import android.content.Context
-import android.content.ContextWrapper
import android.util.AttributeSet
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.widget.Toolbar
import androidx.drawerlayout.widget.DrawerLayout
+import org.autojs.autojs.util.ContextUtils.requireActivity
import org.autojs.autojs6.R
class JsToolbar : Toolbar {
@@ -19,11 +19,7 @@ class JsToolbar : Toolbar {
private val activity: Activity
get() {
- var context = context
- while (context !is Activity) {
- context = (context as? ContextWrapper)?.baseContext
- }
- return context
+ return context.requireActivity()
}
fun setupWithDrawer(drawerLayout: DrawerLayout) {
diff --git a/app/src/main/java/org/autojs/autojs/core/ui/widget/JsWebView.kt b/app/src/main/java/org/autojs/autojs/core/ui/widget/JsWebView.kt
index 8d647529..956f3579 100644
--- a/app/src/main/java/org/autojs/autojs/core/ui/widget/JsWebView.kt
+++ b/app/src/main/java/org/autojs/autojs/core/ui/widget/JsWebView.kt
@@ -1,16 +1,26 @@
package org.autojs.autojs.core.ui.widget
-import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
-import android.webkit.WebChromeClient
-import android.webkit.WebView
-import android.webkit.WebViewClient
+import org.autojs.autojs.AutoJs
+import org.autojs.autojs.core.eventloop.EventEmitter
+import org.autojs.autojs.runtime.api.Resolvable
+import org.autojs.autojs.runtime.api.ScriptPromiseAdapter
+import org.mozilla.javascript.ScriptRuntime
+import org.mozilla.javascript.Scriptable
/**
* Created by Stardust on Nov 29, 2017.
+ * Modified by SuperMonster003 as of Jan 21, 2023.
+ * Transformed by SuperMonster003 on May 26, 2023.
*/
-class JsWebView : WebView {
+class JsWebView : EventWebView {
+
+ @JvmField
+ var events: EventEmitter? = null
+
+ @JvmField
+ var jsBridge: Scriptable? = null
constructor(context: Context) : super(context)
@@ -21,17 +31,18 @@ class JsWebView : WebView {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
init {
- settings.apply {
- useWideViewPort = true
- builtInZoomControls = true
- loadWithOverviewMode = true
- @SuppressLint("SetJavaScriptEnabled")
- javaScriptEnabled = true
- javaScriptCanOpenWindowsAutomatically = true
- domStorageEnabled = true
- displayZoomControls = false
- }
- webViewClient = WebViewClient()
- webChromeClient = WebChromeClient()
+ isFocusable = true
+ isFocusableInTouchMode = true
}
+
+ /**
+ * Escape string according to Rhino's rules and wrap with single quotes.
+ * zh-CN: 将字符串按 Rhino 的规则做转义, 并包裹单引号.
+ */
+ override fun escapeToStr(src: String) = "'${ScriptRuntime.escapeString(src, '\'')}'"
+
+ override fun newPromise(): Resolvable = ScriptPromiseAdapter()
+
+ override fun onError(t: Throwable) = AutoJs.instance.globalConsole.error(t)
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/event/CoroutineSyncEventHost.kt b/app/src/main/java/org/autojs/autojs/event/CoroutineSyncEventHost.kt
new file mode 100644
index 00000000..c72a0807
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/event/CoroutineSyncEventHost.kt
@@ -0,0 +1,68 @@
+@file:Suppress("MemberVisibilityCanBePrivate", "ReplacePutWithAssignment", "unused")
+
+package org.autojs.autojs.event
+
+import androidx.annotation.Keep
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Created by SuperMonster003 on May 23, 2025.
+ */
+// @Reference to com.stardust.event.CoroutineSyncEventHost from Auto.js Pro 9.3.11 by SuperMonster003 on May 23, 2025.
+class CoroutineSyncEventHost(
+ val scope: CoroutineScope,
+ val consumer: (Event) -> Boolean,
+) : IEventEmitter {
+
+ var onError: ((Throwable) -> Unit)? = null
+ val syncEventTable = ConcurrentHashMap()
+ var alwaysSync: Boolean = false
+
+ override fun emitEvent(event: String, vararg args: Any?): EventResult {
+ require(event.isNotBlank()) { "event" }
+ return emitInScope(event, scope, *args)
+ }
+
+ fun emitInScope(eventName: String, scope: CoroutineScope, vararg args: Any?): EventResult {
+ val sync = alwaysSync || syncEventTable[eventName] ?: false
+ val evt = Event(arguments = args, name = eventName, sync = sync, consumed = false, result = null)
+ return when {
+ sync -> {
+ val handled = runCatching {
+ consumer(evt)
+ }.onFailure {
+ onError?.invoke(it.apply { printStackTrace() })
+ }.isSuccess
+ when {
+ handled -> EventResult(evt.result, !evt.consumed)
+ else -> IGNORE_RESULT
+ }
+ }
+ else -> {
+ scope.launch(Dispatchers.Default) {
+ runCatching {
+ consumer(evt)
+ }.onFailure {
+ onError?.invoke(it.apply { printStackTrace() })
+ }
+ }
+ IGNORE_RESULT
+ }
+ }
+ }
+
+
+ companion object {
+
+ val IGNORE_RESULT = EventResult(null, true)
+
+ @Keep
+ @Suppress("ArrayInDataClass")
+ data class Event(val arguments: Array, val name: String, val sync: Boolean, var consumed: Boolean = false, var result: Any? = null)
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/event/IEventEmitter.kt b/app/src/main/java/org/autojs/autojs/event/IEventEmitter.kt
new file mode 100644
index 00000000..0a28a9ec
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/event/IEventEmitter.kt
@@ -0,0 +1,9 @@
+package org.autojs.autojs.event
+
+interface IEventEmitter {
+
+ fun emitEvent(event: String, vararg args: Any?): EventResult
+
+}
+
+data class EventResult(val result: Any?, val callSuper: Boolean)
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/execution/ScriptExecuteActivity.kt b/app/src/main/java/org/autojs/autojs/execution/ScriptExecuteActivity.kt
index 7de3b39e..05236d67 100644
--- a/app/src/main/java/org/autojs/autojs/execution/ScriptExecuteActivity.kt
+++ b/app/src/main/java/org/autojs/autojs/execution/ScriptExecuteActivity.kt
@@ -44,13 +44,15 @@ import org.mozilla.javascript.ContinuationPending
* Created by Stardust on Feb 5, 2017.
* Modified by SuperMonster003 as of Nov 15, 2023.
*/
-class ScriptExecuteActivity : AppCompatActivity() {
+class ScriptExecuteActivity : AppCompatActivity(), OnActivityResultDelegate.DelegateHost {
private var mRuntime: ScriptRuntime? = null
private var mExecutionListener: ScriptExecutionListener? = null
private var mScriptSource: ScriptSource? = null
private var mResult: Any? = null
+ private val mMediator = OnActivityResultDelegate.Mediator()
+
private lateinit var mScriptEngine: ScriptEngine<*>
private lateinit var mScriptExecution: ActivityScriptExecution
@@ -235,6 +237,7 @@ class ScriptExecuteActivity : AppCompatActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
emit("activity_result", requestCode, resultCode, data)
+ mMediator.onActivityResult(requestCode, resultCode, data)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -258,6 +261,10 @@ class ScriptExecuteActivity : AppCompatActivity() {
}
}
+ override fun getOnActivityResultDelegateMediator(): OnActivityResultDelegate.Mediator {
+ return mMediator
+ }
+
class ActivityScriptExecution internal constructor(
private val mScriptEngineManager: ScriptEngineManager,
task: ScriptExecutionTask?,
diff --git a/app/src/main/java/org/autojs/autojs/runtime/ScriptRuntime.kt b/app/src/main/java/org/autojs/autojs/runtime/ScriptRuntime.kt
index 82c879ef..a13cc310 100644
--- a/app/src/main/java/org/autojs/autojs/runtime/ScriptRuntime.kt
+++ b/app/src/main/java/org/autojs/autojs/runtime/ScriptRuntime.kt
@@ -420,6 +420,8 @@ class ScriptRuntime private constructor(builder: Builder) {
val js_ResultAdapter by lazy { rhinoRequire("result-adapter") as BaseFunction }
+ val js_UiExt by lazy { rhinoRequire("ui-ext") as BaseFunction }
+
private val js_object_observe_lite_min by lazy { rhinoRequire("object-observe-lite.min") as BaseFunction }
private val js_array_observe_min by lazy { rhinoRequire("array-observe.min") as BaseFunction }
diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/Resolvable.kt b/app/src/main/java/org/autojs/autojs/runtime/api/Resolvable.kt
new file mode 100644
index 00000000..d2f13fd4
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/runtime/api/Resolvable.kt
@@ -0,0 +1,7 @@
+package org.autojs.autojs.runtime.api
+
+interface Resolvable {
+
+ fun resolve(result: Any?)
+
+}
diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/ScriptPromiseAdapter.kt b/app/src/main/java/org/autojs/autojs/runtime/api/ScriptPromiseAdapter.kt
index a26617d4..4a05cab1 100644
--- a/app/src/main/java/org/autojs/autojs/runtime/api/ScriptPromiseAdapter.kt
+++ b/app/src/main/java/org/autojs/autojs/runtime/api/ScriptPromiseAdapter.kt
@@ -1,6 +1,6 @@
package org.autojs.autojs.runtime.api
-class ScriptPromiseAdapter {
+class ScriptPromiseAdapter : Resolvable {
interface Callback {
fun call(arg: Any?)
@@ -31,7 +31,7 @@ class ScriptPromiseAdapter {
return this
}
- fun resolve(result: Any?) {
+ override fun resolve(result: Any?) {
mResult = result
mResolveCallback?.call(result)
}
@@ -44,4 +44,5 @@ class ScriptPromiseAdapter {
companion object {
private val UNSET = Object()
}
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/ui/UI.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/ui/UI.kt
index 56de11a4..83bdeccf 100644
--- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/ui/UI.kt
+++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/ui/UI.kt
@@ -18,6 +18,7 @@ import org.autojs.autojs.core.ui.inflater.inflaters.ViewGroupInflater
import org.autojs.autojs.core.ui.inflater.inflaters.ViewInflater
import org.autojs.autojs.core.ui.nativeview.NativeView
import org.autojs.autojs.core.ui.widget.JsListView
+import org.autojs.autojs.core.ui.widget.JsWebView
import org.autojs.autojs.execution.ScriptExecuteActivity
import org.autojs.autojs.extension.AnyExtensions.isJsNullish
import org.autojs.autojs.extension.AnyExtensions.jsBrief
@@ -228,8 +229,9 @@ class UI(private val scriptRuntime: ScriptRuntime) : AugmentableProxy(scriptRunt
}
override fun afterCreateView(inflateContext: InflateContext, view: View, node: Node?, viewName: String, parent: ViewGroup?): View {
- if (view is JsListView) {
- initListView(scriptRuntime, view)
+ when (view) {
+ is JsListView -> initListView(scriptRuntime, view)
+ is JsWebView -> initWebView(scriptRuntime, view)
}
val widget = inflateContext.get("widget")
if (widget is NativeObject) {
@@ -835,6 +837,10 @@ class UI(private val scriptRuntime: ScriptRuntime) : AugmentableProxy(scriptRunt
})
}
+ private fun initWebView(scriptRuntime: ScriptRuntime, webView: JsWebView) {
+ callFunction(scriptRuntime.js_UiExt, scriptRuntime.topLevelScope, null, arrayOf(webView))
+ }
+
private fun wrapUiAction(scriptRuntime: ScriptRuntime, action: BaseFunction) = Runnable {
when {
!getActivity(scriptRuntime).isJsNullish() -> callFunction(scriptRuntime, action, scriptRuntime.topLevelScope, arrayOf())
diff --git a/app/src/main/java/org/autojs/autojs/util/ContextUtils.kt b/app/src/main/java/org/autojs/autojs/util/ContextUtils.kt
new file mode 100644
index 00000000..21f458cc
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/util/ContextUtils.kt
@@ -0,0 +1,24 @@
+package org.autojs.autojs.util
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+
+object ContextUtils {
+
+ @JvmStatic
+ @Throws(IllegalArgumentException::class)
+ fun Context.requireActivity(): Activity {
+ return this.findActivity() ?: throw IllegalArgumentException("Context cannot be cast to Activity")
+ }
+
+ @JvmStatic
+ fun Context.findActivity(): Activity? {
+ var context: Context? = this
+ while (context !is Activity) {
+ context = (context as? ContextWrapper)?.baseContext
+ }
+ return context as? Activity?
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/autojs/autojs/util/DeviceUtils.kt b/app/src/main/java/org/autojs/autojs/util/DeviceUtils.kt
index 25b5a23b..6325fe9f 100644
--- a/app/src/main/java/org/autojs/autojs/util/DeviceUtils.kt
+++ b/app/src/main/java/org/autojs/autojs/util/DeviceUtils.kt
@@ -25,6 +25,7 @@ object DeviceUtils {
@JvmStatic
fun getDeviceSummary(context: Context) = DeviceInfo(context).toString()
+ @JvmStatic
fun getDeviceSummaryWithSimpleAppInfo(context: Context) = DeviceInfo(context).toStringWithSimpleAppInfo()
// @Reference to com.heinrichreimersoftware.androidissuereporter.model.DeviceInfo
diff --git a/app/src/main/java/org/autojs/autojs/util/FileUtils.kt b/app/src/main/java/org/autojs/autojs/util/FileUtils.kt
index 2edafcaa..285f49f1 100644
--- a/app/src/main/java/org/autojs/autojs/util/FileUtils.kt
+++ b/app/src/main/java/org/autojs/autojs/util/FileUtils.kt
@@ -182,6 +182,9 @@ object FileUtils {
/** Embedded JavaScript templating file, used for HTML templates. */
EJS("ejs", TypeDataHolder.CODE),
+ /** Vue.js code file. */
+ VUE("vue", TypeDataHolder.CODE),
+
/** NFEX code file. */
NFEX("nfex", TypeDataHolder.CODE),
diff --git a/app/src/main/java/org/autojs/autojs/util/IntentUtils.kt b/app/src/main/java/org/autojs/autojs/util/IntentUtils.kt
index c99b900b..490d9e4e 100644
--- a/app/src/main/java/org/autojs/autojs/util/IntentUtils.kt
+++ b/app/src/main/java/org/autojs/autojs/util/IntentUtils.kt
@@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
+import android.provider.OpenableColumns
import android.provider.Settings
import android.view.View
import androidx.core.content.FileProvider
@@ -225,6 +226,29 @@ object IntentUtils {
false.also { e.printStackTrace() }
}
+ @JvmStatic
+ fun getFileName(context: Context, uri: Uri): String {
+ var result: String? = null
+ if (uri.scheme == "content") {
+ context.contentResolver.query(uri, null, null, null, null).use { cursor ->
+ if (cursor != null && cursor.moveToFirst()) {
+ val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ if (index >= 0) {
+ result = cursor.getString(index)
+ }
+ }
+ }
+ }
+ if (result == null) {
+ result = uri.path
+ val cut = result!!.lastIndexOf('/')
+ if (cut != -1) {
+ result = result.substring(cut + 1)
+ }
+ }
+ return result
+ }
+
fun requestAppUsagePermission(context: Context) = try {
Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
diff --git a/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt b/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt
index 550f0fd5..9f529d35 100644
--- a/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt
+++ b/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt
@@ -3,6 +3,7 @@
package org.autojs.autojs.util
import android.content.Intent
+import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.Log
@@ -327,6 +328,14 @@ object RhinoUtils {
@JvmStatic
fun isBackgroundThread() = !isMainThread()
+ @JvmStatic
+ fun dispatchToMainThread(r: Runnable) = dispatchToMainThread(Handler(Looper.getMainLooper()), r)
+
+ @JvmStatic
+ fun dispatchToMainThread(handler: Handler, r: Runnable) {
+ if (isMainThread()) r.run() else handler.post(r)
+ }
+
@JvmStatic
fun encodeURI(str: String): String = encodeURI(null, str)