6.6.3 - Alpha5 - webview 元素支持基于 JsBridge 的 Web 页面布局 (Ref to Auto.js Pro) (issue #281)

This commit is contained in:
SuperMonster003
2025-05-25 21:04:08 +08:00
parent 6159302829
commit 299c20ad72
30 changed files with 1454 additions and 30 deletions

View File

@@ -0,0 +1,93 @@
'ui';
ui.layout(
<vertical>
<webview id="web" url="web/index.html" w="*" h="*"/>
</vertical>,
);
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;
}
});

View File

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

View File

@@ -0,0 +1,55 @@
<html>
<body>
<div id="app"></div>
<link rel="stylesheet" href="https://unpkg.com/vant@2.12/lib/index.css"/>
<script src="https://unpkg.com/vue@2.6/dist/vue.min.js"></script>
<script src="https://unpkg.com/vue3-sfc-loader@0.8.4/dist/vue2-sfc-loader.js"></script>
<script src="https://unpkg.com/vant@2.12/lib/vant.min.js"></script>
<script src="autojs://sdk/v1.js"></script>
<script>
let { loadModule, vueVersion } = window['vue2-sfc-loader'];
let options = {
moduleCache: {
vue: Vue,
myData: {
vueVersion,
},
},
async getFile(url) {
let getContentData;
if (typeof $autojs !== 'undefined') {
let res = await $autojs.invoke('fetch', { path: url });
getContentData = (asBinary) => asBinary ? str2ab(res) : res;
} else {
let res = await fetch(url);
if (!res.ok) {
throw Object.assign(new Error(res.statusText + ' ' + url), { res });
}
getContentData = (asBinary) => asBinary ? res.arrayBuffer() : res.text();
}
return {
getContentData,
};
function str2ab(str) {
let buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char
let bufView = new Uint16Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
},
addStyle() {
/* unused here */
},
};
loadModule('/main.vue', options)
.then(component => new Vue(component).$mount('#app'));
</script>
</body>
</html>

View File

@@ -0,0 +1,127 @@
<template>
<van-row>
<van-nav-bar title="基于 Vue 的界面"/>
<van-tabs v-model="activeTab">
<van-tab title="配置">
<van-cell-group title="权限">
<van-cell title="无障碍服务" label="用于脚本自动操作 (点击/长按/滑动等)">
<van-switch
v-model="accessibilityServiceEnabled"
@input="onAccessibilityServiceCheckChanged"
/>
</van-cell>
</van-cell-group>
<van-cell-group title="配置">
<van-cell title="开关样本">
<van-switch
v-model="sampleSwitchChecked"
@change="onSampleSwitchChanged"
/>
</van-cell>
<van-field v-model="greeting"
label="问候语"
placeholder="请输入问候语"
maxLength="20"
input-align="right"
/>
<van-field v-model.number="count"
label="运行次数"
placeholder="请输入运行次数"
maxLength="4"
inputmode="numeric"
input-align="right"
/>
<van-field
label="选择文件"
:value="selectedFilePath"
placeholder="选择一个文件"
readonly
clickable
@click.native="selectFile"
input-align="right"
/>
</van-cell-group>
</van-tab>
<van-tab title="运行">
<van-cell title="查看日志" is-link @click="showLog"/>
<van-row type="flex" justify="center">
<van-button type="primary" @click="run" style="margin-top: 12px;">运行</van-button>
</van-row>
</van-tab>
<van-tab title="关于">
<van-cell
value="运行环境"
:title="appVersionName ? `AutoJs6 ${appVersionName}` : `AutoJs6`"
label="WebView + Android"
@click="showDeviceInfoDialog"
/>
<van-cell
title="Vue.js 2.6"
label="渐进式 JavaScript 框架"
is-link
@click="openVueWebsite"
/>
<van-cell
title="Vant 2.12"
label="轻量, 可靠的移动端 Vue 组件库"
is-link
@click="openVantWebsite"
/>
</van-tab>
</van-tabs>
</van-row>
</template>
<script>
export default {
data() {
return {
accessibilityServiceEnabled: false,
activeTab: 0,
sampleSwitchChecked: true,
greeting: 'Hello',
count: 192,
appVersionName: '',
selectedFilePath: '',
};
},
created() {
$autojs.invoke('get-accessibility-enabled').then((value) => {
this.accessibilityServiceEnabled = value;
});
$autojs.invoke('get-app-version-name').then((value) => {
this.appVersionName = value;
});
},
methods: {
onAccessibilityServiceCheckChanged(checked) {
$autojs.invoke('set-accessibility-enabled', checked);
},
onSampleSwitchChanged(checked) {
$autojs.invoke('toast-log', `样本开关已${checked ? '开启' : '关闭'}`);
},
showLog() {
$autojs.invoke('show-log');
},
openVantWebsite() {
$autojs.send('open-url', 'https://vant-ui.github.io/vant/v2/#/zh-CN/');
},
openVueWebsite() {
$autojs.send('open-url', 'https://cn.vuejs.org/');
},
run() {
$autojs.invoke('toast-log', `greeting: "${this.greeting}"\ncount: ${this.count}`);
},
selectFile() {
$autojs.invoke('select-file', '*/*').then((path) => {
this.selectedFilePath = path || '';
});
},
showDeviceInfoDialog() {
$autojs.invoke('show-device-info-dialog');
},
},
};
</script>

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Interactive HTML</title>
</head>
<body>
<link rel="stylesheet" href="style.css"/>
<script src="autojs://sdk/v1.js"></script>
<script src="index.js"></script>
<button id="testButton">点击测试</button>
</body>
</html>

View File

@@ -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', '按钮已被点击');
});
};

View File

@@ -0,0 +1,13 @@
'ui';
ui.layout(
<vertical>
<webview id="web" url="index.html" w="*" h="*"/>
</vertical>,
);
let web = ui['web'];
web.jsBridge.handle('toast-log', (event, msg) => {
toastLog(msg);
});

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -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<PendingJsEvent>())
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<Array<out String?>?>?) {
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<Array<out Uri?>?>?, 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<Array<out Uri?>?>?,
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)
}
}
}
}
}

View File

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

View File

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

View File

@@ -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<String, Boolean>()
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<out Any?>, val name: String, val sync: Boolean, var consumed: Boolean = false, var result: Any? = null)
}
}

View File

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

View File

@@ -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?,

View File

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

View File

@@ -0,0 +1,7 @@
package org.autojs.autojs.runtime.api
interface Resolvable {
fun resolve(result: Any?)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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