diff --git a/autojs/src/main/assets/modules/axios/browser-libs/XMLHttpRequest.js b/autojs/src/main/assets/modules/axios/browser-libs/XMLHttpRequest.js index c65a7fad0..5d65a8ff3 100644 --- a/autojs/src/main/assets/modules/axios/browser-libs/XMLHttpRequest.js +++ b/autojs/src/main/assets/modules/axios/browser-libs/XMLHttpRequest.js @@ -7,6 +7,7 @@ let HttpUrl = Packages.okhttp3.HttpUrl; let MediaType = Packages.okhttp3.MediaType; let Headers = Packages.okhttp3.Headers; + let InterruptedIOException = java.io.InterruptedIOException const stream = require("stream"); let EventTarget = require("./EventTarget.js"); @@ -24,6 +25,7 @@ ///////XMLHttpRequest对象 let XMLHttpRequest = function () { this.responseType = 'text'; + this.timeout = 0; setReadonlyAttribute(this, 'upload', {}) setReadonlyAttribute(this, 'readyState', 0); setReadonlyAttribute(this, 'status', 0); @@ -105,12 +107,7 @@ XMLHttpRequest.prototype.send = function (body) { atl.addTask(); const xhr = this; - const { - url, - method, - ac, - headers, - } = this._requestData + const { url, method, ac, headers, } = this._requestData const responseType = xhr.responseType; const builder = new Request.Builder(); @@ -120,12 +117,20 @@ let reqBody = parserReqBody(xhr, body); let request = builder.url(url.build()).method(method, reqBody).build(); - let call = XMLHttpRequest._okHttpClient.newCall(request); + let client = XMLHttpRequest._okHttpClient; + if (xhr.timeout > 0) { + client = new OkHttpClient.Builder().callTimeout(xhr.timeout, java.util.concurrent.TimeUnit.MILLISECONDS) + .followRedirects(true).build(); + } + let call = client.newCall(request); setReadonlyAttribute(xhr, '_call', call, false); call.enqueue({ onFailure(call, e) { xhr._setReadyState(4); setReadonlyAttribute(xhr, 'statusText', e.message) + if (e instanceof InterruptedIOException) { + xhr.dispatchEvent(new Event('timeout')); + } xhr.dispatchEvent(new Event('error')) xhr.dispatchEvent(new Event('loadend')) atl.removeTask() diff --git a/autojs/src/main/assets/modules/npm/process.js b/autojs/src/main/assets/modules/npm/process.js index 73bad8e9f..a6591a094 100644 --- a/autojs/src/main/assets/modules/npm/process.js +++ b/autojs/src/main/assets/modules/npm/process.js @@ -1,25 +1,24 @@ -const { - EventEmitter -} = require("events"); -const es = require("event-stream"); + +const { EventEmitter } = require("events"); +// const es = require("event-stream"); const process = new EventEmitter(); //events.on('exit', process.emit.bind(process, 'exit')) - +/* process.stdout = es.map(function(data, callback) { console.log(data); return callback(null, data) }) -/* + process.stdout.end = function() {}; process.stdout.destroy = function(){}; -*/ + process.stderr = es.map(function(data, callback) { console.error(data); return callback(null, data) }) - +*/ process.nextTick = setImmediate; process.env = {} //log(process); diff --git a/autojs/src/main/assets/modules/npm/stream.js b/autojs/src/main/assets/modules/npm/stream.js index f63edcea6..477a4509d 100644 --- a/autojs/src/main/assets/modules/npm/stream.js +++ b/autojs/src/main/assets/modules/npm/stream.js @@ -2,35 +2,42 @@ importClass(java.io.OutputStream); importClass(java.io.InputStream); importClass(java.io.FileOutputStream); importClass(java.io.FileInputStream); -importClass(java.util.concurrent.CompletableFuture); const stream = require("stream-browserify"); +let mainThread; -function addTask(fn, callback) { - CompletableFuture.runAsync(function() { +function addTask(th, fn, callback) { + th.setImmediate(function () { try { const data = fn(); - return setImmediate(callback, null, data) + return mainThread.setImmediate(callback, null, data) } catch (e) { //console.error(e) - return setImmediate(callback, e) + return mainThread.setImmediate(callback, e) } }) } +function createThread() { + return threads.start(() => { + setInterval(() => { }, 1000) + }) +} -stream.fromInputStream = function(inp, options) { +stream.fromInputStream = function (inp, options) { + if (!mainThread) mainThread = threads.currentThread(); if (!(inp instanceof InputStream)) throw TypeError('需要InputStream流'); options = options || {}; options.highWaterMark = options.highWaterMark || (1024 * 64); options.autoDestroy = true; options.emitClose = true; - options.destroy = function(e, cb) { + options.destroy = function (e, cb) { this._inp.close(); runtime.loopers.removeAsyncTask(this._task) + this._thread.interrupt(); return cb(e) } - options.read = function(size) { - addTask(() => { + options.read = function (size) { + addTask(this._thread, () => { const buffer = Buffer.alloc(size); const bytes = buffer.getBytes(); const i = this._inp.read(bytes); @@ -49,30 +56,33 @@ stream.fromInputStream = function(inp, options) { } const readable = new stream.Readable(options) setReadonlyAttribute(readable, '_inp', inp, false) + setReadonlyAttribute(readable, '_thread', createThread(), false) setReadonlyAttribute(readable, '_task', runtime.loopers.createAndAddAsyncTask('stream'), false) return readable; } -stream.fromOutputStream = function(out, options) { +stream.fromOutputStream = function (out, options) { + if (!mainThread) mainThread = threads.currentThread(); if (!(out instanceof OutputStream)) throw TypeError('需要OutputStream流'); options = options || {}; options.highWaterMark = options.highWaterMark || (1024 * 64); options.autoDestroy = true; options.emitClose = true; - options.destroy = function(e, cb) { + options.destroy = function (e, cb) { this._out.close(); runtime.loopers.removeAsyncTask(this._task) + this._thread.interrupt(); return cb(e) } - options.final = function(callback) { - addTask(() => { + options.final = function (callback) { + addTask(this._thread, () => { this._out.flush(); }, (err) => { callback(); }) } - options.write = function(chunk, encoding, callback) { - addTask(() => { + options.write = function (chunk, encoding, callback) { + addTask(this._thread, () => { const buffer = (chunk instanceof Buffer) ? chunk : Buffer.from(chunk, encoding); const bytes = buffer.getBytes() this._out.write(bytes, 0, buffer.length); @@ -83,16 +93,17 @@ stream.fromOutputStream = function(out, options) { } const writable = new stream.Writable(options) setReadonlyAttribute(writable, '_out', out, false); + setReadonlyAttribute(writable, '_thread', createThread(), false) setReadonlyAttribute(writable, '_task', runtime.loopers.createAndAddAsyncTask('stream'), false) return writable; } -stream.createFileReadStream = function(path, bufferSize) { +stream.createFileReadStream = function (path, bufferSize) { return stream.fromInputStream(new FileInputStream( files.path(path)), { highWaterMark: bufferSize || (256 * 1024) }) } -stream.createFileWriteStream = function(path, bufferSize) { +stream.createFileWriteStream = function (path, bufferSize) { return stream.fromOutputStream(new FileOutputStream( files.path(path)), { highWaterMark: bufferSize || (256 * 1024) diff --git a/autojs/src/main/java/com/stardust/autojs/core/looper/Loopers.kt b/autojs/src/main/java/com/stardust/autojs/core/looper/Loopers.kt index 4c308fa0b..b2302a360 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/looper/Loopers.kt +++ b/autojs/src/main/java/com/stardust/autojs/core/looper/Loopers.kt @@ -132,7 +132,9 @@ class Loopers(val runtime: ScriptRuntime) { @Deprecated("使用AsyncTask代替") fun waitWhenIdle(b: Boolean) { - waitWhenIdle = b + (Thread.currentThread() as? TimerThread)?.let { + it.loopers?.createAndAddAsyncTask("events") + }?: createAndAddAsyncTask("events") } fun recycle() { diff --git a/autojs/src/main/java/com/stardust/autojs/core/ui/widget/JsWebView.kt b/autojs/src/main/java/com/stardust/autojs/core/ui/widget/JsWebView.kt index 5ed0cbc7a..641dc9c8e 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/ui/widget/JsWebView.kt +++ b/autojs/src/main/java/com/stardust/autojs/core/ui/widget/JsWebView.kt @@ -9,7 +9,7 @@ import com.stardust.autojs.core.web.JsBridge open class JsWebView : WebView { //val events = EventEmitter() - + @RequiresApi(Build.VERSION_CODES.M) val jsBridge = JsBridge(this) init { @@ -21,7 +21,7 @@ open class JsWebView : WebView { settings.javaScriptCanOpenWindowsAutomatically = true settings.domStorageEnabled = true settings.displayZoomControls = false - webViewClient = JsBridge.SuperWebViewClient() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) webViewClient = JsBridge.SuperWebViewClient() } constructor(context: Context) : super(context) @@ -32,18 +32,7 @@ open class JsWebView : WebView { defStyleAttr ) @RequiresApi(Build.VERSION_CODES.M) - fun injectionJsBridge() { - val context = this.context - val js: String = try { - val inputStream = context.assets.open(JsBridge.sdkPath) - val available = inputStream.available() - val byteArray = ByteArray(available) - inputStream.read(byteArray) - inputStream.close() - String(byteArray) - } catch (e: Exception) { - "" - } - jsBridge.evaluateJavascript(js); + fun injectionJsBridge(){ + JsBridge.injectionJsBridge(this) } } diff --git a/autojs/src/main/java/com/stardust/autojs/core/web/JsBridge.kt b/autojs/src/main/java/com/stardust/autojs/core/web/JsBridge.kt index 79b27ccd4..2ea254bae 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/web/JsBridge.kt +++ b/autojs/src/main/java/com/stardust/autojs/core/web/JsBridge.kt @@ -7,28 +7,56 @@ import androidx.annotation.RequiresApi import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.annotations.Expose -import com.stardust.autojs.core.ui.widget.JsWebView import org.mozilla.javascript.BaseFunction +import org.mozilla.javascript.Context import org.mozilla.javascript.Scriptable +import org.mozilla.javascript.Undefined +import java.io.ByteArrayOutputStream import kotlin.random.Random +@RequiresApi(Build.VERSION_CODES.M) class JsBridge(private val webView: WebView) { companion object { const val WEBOBJECTNAME = "\$autox" const val JAVABRIDGE = "AutoxJavaBridge" const val sdkPath = "web/autox.sdk.v1.js" + + fun evaluateJavascript(js: String, webView: WebView) { + Looper.getMainLooper().queue.addIdleHandler { + webView.evaluateJavascript(js, null) + false + } + } + + fun injectionJsBridge(webView: WebView) { + val js: String = try { + val inputStream = webView.context.assets.open(sdkPath) + val byteArrayOutputStream = ByteArrayOutputStream() + inputStream.use { it -> + it.copyTo(byteArrayOutputStream) + } + String(byteArrayOutputStream.toByteArray()) + } catch (e: Exception) { + "" + } + evaluateJavascript(js, webView); + } + val gson: Gson = GsonBuilder().excludeFieldsWithoutExposeAnnotation().create() } - private val handles = HashMap() + private val handles = HashMap() private val callHandlerData = HashMap() init { webView.addJavascriptInterface(this.JsObject(), JAVABRIDGE) } - fun registerHandler(event: String, handle: CrFunction): JsBridge { - handles[event] = handle + fun registerHandler( + event: String, + handle: BaseFunction + ): JsBridge { + handles[event] = if (handle is Handle) handle else Handle(handle) return this } @@ -37,29 +65,28 @@ class JsBridge(private val webView: WebView) { return this } - @RequiresApi(Build.VERSION_CODES.M) - fun callHandler(event: String, data: String?, callBack: CrFunction?) { + fun callHandler(event: String, data: String?, callBack: BaseFunction?) { + val fn = if (callBack is Handle) callBack else callBack?.let { Handle(it) } val pos = Pos(getId(), event, data) callHandlerData[pos.id] = pos - callBack?.let { + fn?.let { + it.name = "callBack" pos.callBackId = pos.id pos.callBack = it } - val js = "${WEBOBJECTNAME}._onCallHandler(${pos.id})" - evaluateJavascript(js) + val js = "$WEBOBJECTNAME._onCallHandler(${pos.id})" + evaluateJavascript(js, this.webView) } - @RequiresApi(Build.VERSION_CODES.M) fun callHandler(event: String, data: String?) { callHandler(event, data, null) } - @RequiresApi(Build.VERSION_CODES.M) fun callHandler(event: String) { callHandler(event, null, null) } - fun getId(): Int { + private fun getId(): Int { val nextInt = Random.nextInt() if (callHandlerData.containsKey(nextInt)) { return getId() @@ -67,46 +94,80 @@ class JsBridge(private val webView: WebView) { return nextInt } - @RequiresApi(Build.VERSION_CODES.M) - fun evaluateJavascript(js: String) { - Looper.getMainLooper().queue.addIdleHandler { - webView.evaluateJavascript(js, null) - false + class Handle : BaseFunction, (String?, Handle?) -> Unit { + private var jsFn: BaseFunction? = null + private var ktFn: ((data: String?, Handle?) -> Unit)? = null + var name: String = "handle" + + constructor(jsFn: BaseFunction) { + this.jsFn = jsFn + } + + constructor(ktFn: (data: String?, Handle?) -> Unit) { + this.ktFn = ktFn } + + override fun invoke(p1: String?, p2: Handle?) { + ktFn?.invoke(p1, p2) + jsFn?.let { + val cx = Context.getCurrentContext() + try { + val scope = it.parentScope + val args = + arrayListOf(p1, p2).map { v -> Context.javaToJS(v, scope) }.toTypedArray() + it.call(cx ?: Context.enter(), scope, Undefined.SCRIPTABLE_UNDEFINED, args) + } finally { + cx ?: Context.exit() + } + } + } + + fun invokeToMainThread(p1: String?, p2: Handle?) { + Looper.getMainLooper().queue.addIdleHandler { + invoke(p1, p2) + false + } + } + + override fun call( + cx: Context, + scope: Scriptable, + thisObj: Scriptable?, + args: Array? + ) { + val arr = args?.toList() + val data: String = arr?.elementAtOrNull(0) as? String ?: "" + val fn = arr?.elementAtOrNull(1) as? BaseFunction + ktFn?.invoke(data, fn?.let { Handle(fn) }) + jsFn?.call(cx, scope, thisObj, args) + } + + override fun getFunctionName() = name + override fun getArity() = 1 + override fun getLength() = 1 } inner class JsObject { - val callBackData = HashMap() + private val callBackData = HashMap() @JavascriptInterface //web调用安卓 fun callHandle(reqData: String) { val pos = gson.fromJson(reqData, Pos::class.java) + println("onHandle: ${pos.event}") val handler = handles[pos.event] - if (pos.callBackId != null) { - handler?.run(pos.data, object : BaseFunction() { - @RequiresApi(Build.VERSION_CODES.M) - override fun call( - cx: org.mozilla.javascript.Context?, - scope: Scriptable?, - thisObj: Scriptable?, - args: Array - ): Any { - val d: String? = try { - args[0] as String? - } catch (e: Exception) { - null - } - callBackData[pos.callBackId!!] = object { - val data = d - val callBackId = pos.callBackId - } - val js = "${WEBOBJECTNAME}._onCallBack(${pos.callBackId})" - evaluateJavascript(js) - return super.call(cx, scope, thisObj, args) + val callBack: Handle? = if (pos.callBackId != null) { + Handle { data, _ -> + callBackData[pos.callBackId!!] = object { + val data = data + val callBackId = pos.callBackId } - }) - } else handler?.run(pos.data) + val js = "$WEBOBJECTNAME._onCallBack(${pos.callBackId})" + evaluateJavascript(js, webView) + } + } else null + handler?.let { it.name = "callBack" } + handler?.invokeToMainThread(pos.data, callBack) } @JavascriptInterface @@ -120,7 +181,7 @@ class JsBridge(private val webView: WebView) { fun callBack(callBackId: Int, data: String?) { val callBack = callHandlerData[callBackId]?.callBack callHandlerData.remove(callBackId) - callBack?.run(data) + callBack?.invokeToMainThread(data, null) } @JavascriptInterface @@ -138,23 +199,21 @@ class JsBridge(private val webView: WebView) { var callBackId: Int? = null @Expose(serialize = false, deserialize = false) - var callBack: CrFunction? = null - } - - interface CrFunction { - fun run(args: Any?) - fun run(arg1: Any?, arg2: Any?) - fun run(arg1: Any?, arg2: Any?, arg3: Any?) + var callBack: Handle? = null } open class SuperWebViewClient : WebViewClient() { + companion object { + const val SDKPATH = "autox://sdk.v1.js" + } + override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { val url = request.url val context = view.context - if (url.toString().startsWith("autox://sdk.v1.js")) { + if (url.toString() == SDKPATH) { val webResponse: WebResourceResponse? = try { val inputStream = context.assets.open(sdkPath) WebResourceResponse("application/javascript", "UTF-8", inputStream) @@ -166,10 +225,11 @@ class JsBridge(private val webView: WebView) { return super.shouldInterceptRequest(view, request) } - @RequiresApi(Build.VERSION_CODES.M) - override fun onPageFinished(view: WebView, url: String?) { + override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) - (view as JsWebView).injectionJsBridge() + if (view != null) { + injectionJsBridge(view) + } } } } \ No newline at end of file