diff --git a/app/src/main/java/org/autojs/autojs/core/looper/Loopers.java b/app/src/main/java/org/autojs/autojs/core/looper/Loopers.java deleted file mode 100644 index 62324a57..00000000 --- a/app/src/main/java/org/autojs/autojs/core/looper/Loopers.java +++ /dev/null @@ -1,211 +0,0 @@ -package org.autojs.autojs.core.looper; - -import android.os.Handler; -import android.os.Looper; -import android.os.MessageQueue; -import android.util.Log; - -import androidx.annotation.Nullable; - -import org.autojs.autojs.lang.ThreadCompat; -import org.autojs.autojs.rhino.AutoJsContext; -import org.autojs.autojs.runtime.ScriptRuntime; -import org.autojs.autojs.runtime.api.Threads; -import org.autojs.autojs.runtime.api.Timers; -import org.autojs.autojs.runtime.exception.ScriptInterruptedException; -import org.mozilla.javascript.Context; - -import java.util.HashSet; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * Created by Stardust on 2017/7/29. - */ -@SuppressWarnings("ConstantConditions") -public class Loopers implements MessageQueue.IdleHandler { - - private static final String LOG_TAG = "Loopers"; - - public interface LooperQuitHandler { - boolean shouldQuit(); - } - - private static final Runnable EMPTY_RUNNABLE = () -> { - }; - - private final ThreadLocal waitWhenIdle = new ThreadLocal<>() { - @Nullable - @Override - protected Boolean initialValue() { - return Looper.myLooper() == Looper.getMainLooper(); - } - }; - private final ThreadLocal> waitIds = new ThreadLocal<>() { - @Nullable - @Override - protected HashSet initialValue() { - return new HashSet<>(); - } - }; - private final ThreadLocal maxWaitId = new ThreadLocal<>() { - @Nullable - @Override - protected Integer initialValue() { - return 0; - } - }; - private final ThreadLocal> looperQuitHandlers = new ThreadLocal<>(); - private volatile Looper mServantLooper; - private final Timers mTimers; - private LooperQuitHandler mMainLooperQuitHandler; - private final Handler mMainHandler; - private final Looper mMainLooper; - private final Threads mThreads; - private final MessageQueue mMainMessageQueue; - - public Loopers(ScriptRuntime runtime) { - mTimers = runtime.timers; - mThreads = runtime.threads; - prepare(); - mMainLooper = Looper.myLooper(); - mMainHandler = new Handler(); - mMainMessageQueue = Looper.myQueue(); - } - - - public Looper getMainLooper() { - return mMainLooper; - } - - public void addLooperQuitHandler(LooperQuitHandler handler) { - CopyOnWriteArrayList handlers = looperQuitHandlers.get(); - if (handlers == null) { - handlers = new CopyOnWriteArrayList<>(); - looperQuitHandlers.set(handlers); - } - handlers.add(handler); - } - - public boolean removeLooperQuitHandler(LooperQuitHandler handler) { - CopyOnWriteArrayList handlers = looperQuitHandlers.get(); - return handlers != null && handlers.remove(handler); - } - - private boolean shouldQuitLooper() { - if (Thread.currentThread().isInterrupted()) { - return true; - } - if (mTimers.hasPendingCallbacks()) { - return false; - } - if (waitWhenIdle.get() || !waitIds.get().isEmpty()) { - return false; - } - if (((AutoJsContext) Context.getCurrentContext()).hasPendingContinuation()) { - return false; - } - CopyOnWriteArrayList handlers = looperQuitHandlers.get(); - if (handlers == null) { - return true; - } - for (LooperQuitHandler handler : handlers) { - if (!handler.shouldQuit()) { - return false; - } - } - return true; - } - - - private void initServantThread() { - final Object lock = Loopers.this; - new ThreadCompat(() -> { - Looper.prepare(); - mServantLooper = Looper.myLooper(); - synchronized (lock) { - lock.notifyAll(); - } - Looper.loop(); - }).start(); - } - - public Looper getServantLooper() { - if (mServantLooper == null) { - initServantThread(); - synchronized (this) { - try { - this.wait(); - } catch (InterruptedException e) { - throw new ScriptInterruptedException(); - } - } - } - return mServantLooper; - } - - private void quitServantLooper() { - if (mServantLooper == null) - return; - mServantLooper.quit(); - } - - public int waitWhenIdle() { - int id = maxWaitId.get(); - Log.d(LOG_TAG, "waitWhenIdle: " + id); - maxWaitId.set(id + 1); - waitIds.get().add(id); - return id; - } - - public void doNotWaitWhenIdle(int waitId) { - Log.d(LOG_TAG, "doNotWaitWhenIdle: " + waitId); - waitIds.get().remove(waitId); - } - - public void waitWhenIdle(boolean b) { - waitWhenIdle.set(b); - } - - public void recycle() { - quitServantLooper(); - mMainMessageQueue.removeIdleHandler(this); - } - - public void setMainLooperQuitHandler(LooperQuitHandler mainLooperQuitHandler) { - mMainLooperQuitHandler = mainLooperQuitHandler; - } - - @Override - public boolean queueIdle() { - Looper l = Looper.myLooper(); - if (l == null) - return true; - if (l == mMainLooper) { - Log.d(LOG_TAG, "main looper queueIdle"); - if (shouldQuitLooper() && !mThreads.hasRunningThreads() && - mMainLooperQuitHandler != null && mMainLooperQuitHandler.shouldQuit()) { - Log.d(LOG_TAG, "main looper quit"); - l.quit(); - } - } else { - Log.d(LOG_TAG, "looper queueIdle: " + l); - if (shouldQuitLooper()) { - l.quit(); - } - } - return true; - } - - public void prepare() { - if (Looper.myLooper() == null) - LooperHelper.prepare(); - Looper.myQueue().addIdleHandler(this); - } - - public void notifyThreadExit(TimerThread thread) { - Log.d(LOG_TAG, "notifyThreadExit: " + thread); - //当子线程退成时,主线程需要检查自身是否退出(主线程在所有子线程执行完成后才能退出,如果主线程已经执行完任务仍然要等待所有子线程), - //此时通过向主线程发送一个空的Runnable,主线程执行完这个Runnable后会触发IdleHandler,从而检查自身是否退出 - mMainHandler.post(EMPTY_RUNNABLE); - } -} diff --git a/app/src/main/java/org/autojs/autojs/core/looper/Loopers.kt b/app/src/main/java/org/autojs/autojs/core/looper/Loopers.kt new file mode 100644 index 00000000..3185e93e --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/looper/Loopers.kt @@ -0,0 +1,189 @@ +package org.autojs.autojs.core.looper + +import android.os.Looper +import android.os.MessageQueue +import android.util.Log +import org.autojs.autojs.lang.ThreadCompat +import org.autojs.autojs.rhino.AutoJsContext +import org.autojs.autojs.runtime.ScriptRuntime +import org.autojs.autojs.runtime.exception.ScriptInterruptedException +import org.mozilla.javascript.Context +import java.util.concurrent.ConcurrentLinkedQueue + +/** + * Created by Stardust on 2017/7/29. + */ +/** + * update by aiselp on 2023/6/4 + * 调整内容: + * 使此类只负责单loop线程生命周期管理,移除繁琐的调用链 + * 调整timer由此类创建 + * 通过向此类添加AsyncTask以监听线程退出事件 + */ +class Loopers(val runtime: ScriptRuntime) { + @Deprecated("使用AsyncTask代替") + interface LooperQuitHandler { + fun shouldQuit(): Boolean + } + + open class AsyncTask(private val describe: String) { + private val allBind = ConcurrentLinkedQueue() + var isEnd: Boolean = false + private set + + //线程即将退出时调用,返回true阻止线程退出,只要有一个task返回true线程就不会退出 + open fun onFinish(loopers: Loopers): Boolean { + return true + } + + fun end() { + isEnd = true + } + + //线程正在退出,这里应该结束任务的执行,回收资源 + open fun onStop(loopers: Loopers) {} + override fun toString(): String { + return "AsyncTask: $describe" + } + } + + private var waitWhenIdle: Boolean + + @Volatile + private var mServantLooper: Looper? = null + private var mMainLooperQuitHandler: LooperQuitHandler? = null + private val allTasks = ConcurrentLinkedQueue() + val mTimer: Timer + val myLooper: Looper + + init { + prepare() + myLooper = Looper.myLooper()!! + mTimer = Timer(runtime, myLooper) + waitWhenIdle = myLooper == Looper.getMainLooper() + } + + fun createAndAddAsyncTask(describe: String): AsyncTask { + val task = AsyncTask(describe) + allTasks.add(task) + return task + } + + fun addAsyncTask(task: AsyncTask) { + synchronized(myLooper) { + allTasks.add(task) + } + } + + fun removeAsyncTask(task: AsyncTask) { + synchronized(myLooper) { + allTasks.remove(task) + mTimer.post(EMPTY_RUNNABLE) + } + } + + private fun checkTask(): Boolean { + allTasks.removeAll(allTasks.filter { it.isEnd }.toSet()) + for (task in allTasks) { + if (task.onFinish(this)) return true + } + return false + } + + private fun shouldQuitLooper(): Boolean { + synchronized(myLooper) { + if (Thread.currentThread().isInterrupted) return true + if (mTimer.hasPendingCallbacks()) return false + //检查是否有运行中的线程 + if (checkTask()) return false + if (waitWhenIdle) return false + if ((Context.getCurrentContext() as AutoJsContext).hasPendingContinuation()) { + return false + } + return true + } + } + + private fun initServantThread() { + ThreadCompat { + Looper.prepare() + val lock = this@Loopers as Object + mServantLooper = Looper.myLooper() + synchronized(lock) { lock.notifyAll() } + Looper.loop() + }.start() + } + + val servantLooper: Looper + get() { + if (mServantLooper == null) { + initServantThread() + val lock = this as java.lang.Object + synchronized(lock) { + try { + lock.wait() + } catch (e: InterruptedException) { + throw ScriptInterruptedException(e) + } + } + } + return mServantLooper!! + } + + @Deprecated("使用AsyncTask代替") + fun waitWhenIdle(b: Boolean) { + waitWhenIdle = b + } + + fun recycle() { + Log.d(LOG_TAG, "recycle") + for (task in allTasks.filter { !it.isEnd }) { + try { + task.onStop(this) + } catch (e: Exception) { + Log.w(LOG_TAG, e) + } + } + mServantLooper?.quit() + } + + @Deprecated("使用AsyncTask代替") + fun setMainLooperQuitHandler(mainLooperQuitHandler: LooperQuitHandler?) { + mMainLooperQuitHandler = mainLooperQuitHandler + } + + private fun prepare() { + if (Looper.myLooper() == null) LooperHelper.prepare() + Looper.myQueue().addIdleHandler(MessageQueue.IdleHandler { + if (this == runtime.loopers) { + Log.d(LOG_TAG, "main looper queueIdle") + if (shouldQuitLooper() && + mMainLooperQuitHandler != null && + mMainLooperQuitHandler!!.shouldQuit() + ) { + Log.d(LOG_TAG, "main looper quit") + Looper.myLooper()!!.quitSafely() + } + }else { + Log.d(LOG_TAG, "looper queueIdle $this") + if (shouldQuitLooper()) { + Log.d(LOG_TAG, "looper quit $this") + Looper.myLooper()!!.quitSafely() + } + } + return@IdleHandler true + }) + } + + fun notifyThreadExit(thread: TimerThread) { + Log.d(LOG_TAG, "notifyThreadExit: $thread") + //当子线程退成时,主线程需要检查自身是否退出(主线程在所有子线程执行完成后才能退出,如果主线程已经执行完任务仍然要等待所有子线程), + //此时通过向主线程发送一个空的Runnable,主线程执行完这个Runnable后会触发IdleHandler,从而检查自身是否退出 + //mHandler.post(EMPTY_RUNNABLE) + } + + companion object { + private const val LOG_TAG = "Loopers" + private val EMPTY_RUNNABLE = Runnable {} + } +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/core/looper/Timer.java b/app/src/main/java/org/autojs/autojs/core/looper/Timer.java deleted file mode 100644 index dae315b6..00000000 --- a/app/src/main/java/org/autojs/autojs/core/looper/Timer.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.autojs.autojs.core.looper; - -import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; -import android.util.SparseArray; - -import org.autojs.autojs.runtime.ScriptRuntime; -import org.autojs.autojs.concurrent.VolatileBox; - -/** - * Created by Stardust on 2017/12/27. - */ -public class Timer { - - private final SparseArray mHandlerCallbacks = new SparseArray<>(); - private int mCallbackMaxId = 0; - private final ScriptRuntime mRuntime; - private final Handler mHandler; - private long mMaxCallbackUptimeMillis = 0; - private final VolatileBox mMaxCallbackMillisForAllThread; - - public Timer(ScriptRuntime runtime, VolatileBox maxCallbackMillisForAllThread) { - mRuntime = runtime; - mMaxCallbackMillisForAllThread = maxCallbackMillisForAllThread; - mHandler = new Handler(); - } - - public Timer(ScriptRuntime runtime, VolatileBox maxCallbackMillisForAllThread, Looper looper) { - mRuntime = runtime; - mMaxCallbackMillisForAllThread = maxCallbackMillisForAllThread; - mHandler = new Handler(looper); - } - - public int setTimeout(final Object callback, final long delay, final Object... args) { - mCallbackMaxId++; - final int id = mCallbackMaxId; - Runnable r = () -> { - callFunction(callback, args); - mHandlerCallbacks.remove(id); - }; - mHandlerCallbacks.put(id, r); - postDelayed(r, delay); - return id; - } - - private void callFunction(Object callback, Object[] args) { - if(Looper.myLooper() == Looper.getMainLooper()){ - try { - mRuntime.bridges.callFunction(callback, null, args); - }catch (Exception e){ - mRuntime.exit(e); - } - }else { - mRuntime.bridges.callFunction(callback, null, args); - } - } - - public boolean clearTimeout(int id) { - return clearCallback(id); - } - - public int setInterval(final Object listener, final long interval, final Object... args) { - mCallbackMaxId++; - final int id = mCallbackMaxId; - final Runnable r = new Runnable() { - @Override - public void run() { - if (mHandlerCallbacks.get(id) == null) - return; - callFunction(listener, args); - postDelayed(this, interval); - } - }; - mHandlerCallbacks.put(id, r); - postDelayed(r, interval); - return id; - } - - public void postDelayed(Runnable r, long interval) { - long uptime = SystemClock.uptimeMillis() + interval; - mHandler.postAtTime(r, uptime); - mMaxCallbackUptimeMillis = Math.max(mMaxCallbackUptimeMillis, uptime); - synchronized (mMaxCallbackMillisForAllThread) { - mMaxCallbackMillisForAllThread.set(Math.max(mMaxCallbackMillisForAllThread.get(), uptime)); - } - } - - public void post(Runnable r) { - - } - - public boolean clearInterval(int id) { - return clearCallback(id); - } - - public int setImmediate(final Object listener, final Object... args) { - mCallbackMaxId++; - final int id = mCallbackMaxId; - Runnable r = () -> { - callFunction(listener, args); - mHandlerCallbacks.remove(id); - }; - mHandlerCallbacks.put(id, r); - postDelayed(r, 0); - return id; - } - - public boolean clearImmediate(int id) { - return clearCallback(id); - } - - private boolean clearCallback(int id) { - Runnable callback = mHandlerCallbacks.get(id); - if (callback != null) { - mHandler.removeCallbacks(callback); - mHandlerCallbacks.remove(id); - return true; - } - return false; - } - - public boolean hasPendingCallbacks() { - return mMaxCallbackUptimeMillis > SystemClock.uptimeMillis(); - } - - public void removeAllCallbacks() { - mHandler.removeCallbacksAndMessages(null); - } - -} diff --git a/app/src/main/java/org/autojs/autojs/core/looper/Timer.kt b/app/src/main/java/org/autojs/autojs/core/looper/Timer.kt new file mode 100644 index 00000000..95dad6c9 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/looper/Timer.kt @@ -0,0 +1,129 @@ +package org.autojs.autojs.core.looper + +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import org.autojs.autojs.runtime.ScriptRuntime +import org.mozilla.javascript.BaseFunction +import org.mozilla.javascript.Context +import org.mozilla.javascript.Scriptable +import java.util.concurrent.ConcurrentHashMap +import kotlin.random.Random + +/** + * Created by Stardust on 2017/12/27. + */ +class Timer( + runtime: ScriptRuntime, + looper: Looper +) { + private val myLooper: Looper = looper + private val mHandlerCallbacks = ConcurrentHashMap() + private val mRuntime: ScriptRuntime = runtime + private val mHandler: Handler = Handler(looper) + private val isUiLoop: Boolean = looper == Looper.getMainLooper() + + constructor(runtime: ScriptRuntime, ) : this(runtime, Looper.myLooper()!!) + + fun setTimeout(callback: Any, delay: Long, vararg args: Any?): Int { + val id = createTimerId() + val r = Runnable { + callFunction(callback, null, args) + mHandlerCallbacks.remove(id) + } + mHandlerCallbacks[id] = r + postDelayed(r, delay) + return id + } + + private fun callFunction(callback: Any, thiz: Any?, args :Any?) { + val myArgs = args?.let { args as Array<*> }?: emptyArray() + val map = myArgs.map { Context.javaToJS(it, callback as BaseFunction) } + try { + (callback as BaseFunction).call(Context.getCurrentContext(),callback.parentScope, + thiz as? Scriptable?, map.toTypedArray() + ) + } catch (e: Exception) { + if (isUiLoop) { + mRuntime.exit(e) + } else throw e + } + } + + @Synchronized + private fun createTimerId(): Int { + var id: Int + do { + id = Random.nextInt() + } while (mHandlerCallbacks.containsKey(id)) + mHandlerCallbacks[id] = EMPTY_RUNNABLE + return id + } + + fun setInterval(listener: Any, interval: Long, vararg args: Any?): Int { + val id = createTimerId() + val r: Runnable = object : Runnable { + override fun run() { + if (mHandlerCallbacks[id] == null) return + callFunction(listener, null, args) + postDelayed(this, interval) + } + } + mHandlerCallbacks[id] = r + postDelayed(r, interval) + return id + } + + fun postDelayed(r: Runnable, interval: Long) { + synchronized(myLooper) { + val uptime = SystemClock.uptimeMillis() + interval + mHandler.postAtTime(r, uptime) + } + } + + fun post(r: Runnable) { + synchronized(myLooper) { + mHandler.post(r) + } + } + + fun clearInterval(id: Int): Boolean = clearCallback(id) + fun clearImmediate(id: Int): Boolean = clearCallback(id) + fun clearTimeout(id: Int): Boolean = clearCallback(id) + + fun setImmediate(listener: Any, vararg args: Any?): Int { + val id = createTimerId() + val r = Runnable { + callFunction(listener, null, args) + mHandlerCallbacks.remove(id) + } + mHandlerCallbacks[id] = r + post(r) + return id + } + + + private fun clearCallback(id: Int): Boolean { + val callback = mHandlerCallbacks[id] + if (callback != null) { + mHandler.removeCallbacks(callback) + mHandlerCallbacks.remove(id) + if (mHandlerCallbacks.isEmpty()) mHandler.post(EMPTY_RUNNABLE) + return true + } + return false + } + + fun hasPendingCallbacks(): Boolean { + return mHandlerCallbacks.size > 0 + } + + fun removeAllCallbacks() { + mHandler.removeCallbacksAndMessages(null) + } + + companion object { + private const val LOG_TAG = "Timer" + private val EMPTY_RUNNABLE = Runnable {} + } +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/core/looper/TimerThread.java b/app/src/main/java/org/autojs/autojs/core/looper/TimerThread.java deleted file mode 100644 index 97fe478c..00000000 --- a/app/src/main/java/org/autojs/autojs/core/looper/TimerThread.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.autojs.autojs.core.looper; - -import static org.autojs.autojs.util.StringUtils.str; - -import android.os.Handler; -import android.os.Looper; - -import androidx.annotation.CallSuper; -import androidx.annotation.NonNull; - -import org.autojs.autojs.concurrent.VolatileBox; -import org.autojs.autojs.engine.RhinoJavaScriptEngine; -import org.autojs.autojs.lang.ThreadCompat; -import org.autojs.autojs.runtime.ScriptRuntime; -import org.autojs.autojs.runtime.exception.ScriptInterruptedException; -import org.autojs.autojs6.R; - -import java.util.concurrent.ConcurrentHashMap; - -/** - * Created by Stardust on 2017/12/27. - */ -public class TimerThread extends ThreadCompat { - - private static final ConcurrentHashMap sTimerMap = new ConcurrentHashMap<>(); - - private Timer mTimer; - private final VolatileBox mMaxCallbackUptimeMillisForAllThreads; - private final ScriptRuntime mRuntime; - private final Runnable mTarget; - private boolean mRunning = false; - private final Object mRunningLock = new Object(); - - public TimerThread(ScriptRuntime runtime, VolatileBox maxCallbackUptimeMillisForAllThreads, Runnable target) { - super(target); - mRuntime = runtime; - mTarget = target; - mMaxCallbackUptimeMillisForAllThreads = maxCallbackUptimeMillisForAllThreads; - } - - @Override - public void run() { - mRuntime.loopers.prepare(); - mTimer = new Timer(mRuntime, mMaxCallbackUptimeMillisForAllThreads); - sTimerMap.put(Thread.currentThread(), mTimer); - ((RhinoJavaScriptEngine) mRuntime.engines.myEngine()).enterContext(); - notifyRunning(); - new Handler().post(mTarget); - try { - Looper.loop(); - } catch (Throwable e) { - if (!ScriptInterruptedException.causedByInterrupted(e)) { - mRuntime.console.error(Thread.currentThread() + ": ", e); - } - } finally { - onExit(); - mTimer = null; - org.mozilla.javascript.Context.exit(); - sTimerMap.remove(Thread.currentThread(), mTimer); - } - } - - @Override - public void interrupt() { - LooperHelper.quitForThread(this); - super.interrupt(); - } - - private void notifyRunning() { - synchronized (mRunningLock) { - mRunning = true; - mRunningLock.notifyAll(); - } - } - - @CallSuper - protected void onExit() { - mRuntime.loopers.notifyThreadExit(this); - } - - public static Timer getTimerForThread(Thread thread) { - return sTimerMap.get(thread); - } - - public static Timer getTimerForCurrentThread() { - return getTimerForThread(Thread.currentThread()); - } - - public int setTimeout(Object callback, long delay, Object... args) { - return getTimer().setTimeout(callback, delay, args); - } - - public Timer getTimer() { - if (mTimer == null) { - throw new IllegalStateException(str(R.string.error_thread_is_not_alive)); - } - return mTimer; - } - - public boolean clearTimeout(int id) { - return getTimer().clearTimeout(id); - } - - public int setInterval(Object listener, long interval, Object... args) { - return getTimer().setInterval(listener, interval, args); - } - - public boolean clearInterval(int id) { - return getTimer().clearInterval(id); - } - - public int setImmediate(Object listener, Object... args) { - return getTimer().setImmediate(listener, args); - } - - public boolean clearImmediate(int id) { - return getTimer().clearImmediate(id); - } - - public void waitFor() throws InterruptedException { - synchronized (mRunningLock) { - if (mRunning) - return; - mRunningLock.wait(); - } - } - - @NonNull - @Override - public String toString() { - return "Thread[" + getName() + "," + getPriority() + "]"; - } - -} diff --git a/app/src/main/java/org/autojs/autojs/core/looper/TimerThread.kt b/app/src/main/java/org/autojs/autojs/core/looper/TimerThread.kt new file mode 100644 index 00000000..8006f4f4 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/looper/TimerThread.kt @@ -0,0 +1,126 @@ +package org.autojs.autojs.core.looper + +import android.os.Looper +import androidx.annotation.CallSuper +import org.autojs.autojs.engine.RhinoJavaScriptEngine +import org.autojs.autojs.lang.ThreadCompat +import org.autojs.autojs.runtime.ScriptRuntime +import org.autojs.autojs.runtime.exception.ScriptInterruptedException +import org.mozilla.javascript.Context +import java.util.concurrent.ConcurrentHashMap + +/** + * Created by Stardust on 2017/12/27. + */ +open class TimerThread(private val mRuntime: ScriptRuntime, private val mTarget: Runnable) : + ThreadCompat(mTarget) { + private var mTimer: Timer? = null + private var mRunning = false + private val mRunningLock = Object() + private val mAsyncTask = Loopers.AsyncTask("TimerThread") + var loopers: Loopers? = null + init { + mRuntime.loopers.addAsyncTask(mAsyncTask) + } + override fun run() { + loopers = Loopers(mRuntime) + mTimer = loopers!!.mTimer + sTimerMap[currentThread()] = mTimer!! + (mRuntime.engines.myEngine() as RhinoJavaScriptEngine).enterContext() + notifyRunning() + mTimer!!.post(mTarget) + try { + Looper.loop() + } catch (e: Throwable) { + if (!ScriptInterruptedException.causedByInterrupted(e)) { + mRuntime.console.error(currentThread().toString() + ": ", e) + } + } finally { + //mRuntime.console.log("TimerThread exit"); + onExit() + mTimer = null + Context.exit() + sTimerMap.remove(currentThread(), mTimer) + } + } + + override fun interrupt() { + LooperHelper.quitForThread(this) + super.interrupt() + } + + private fun notifyRunning() { + synchronized(mRunningLock) { + mRunning = true + mRunningLock.notifyAll() + } + } + + @CallSuper + protected open fun onExit() { + mRuntime.loopers.removeAsyncTask(mAsyncTask) + mRuntime.loopers.notifyThreadExit(this) + } + + fun setTimeout(callback: Any, delay: Long, vararg args: Any?): Int { + return timer.setTimeout(callback, delay, *args as Array) + } + + fun setTimeout(callback: Any): Int { + return setTimeout(callback, 1) + } + + val timer: Timer + get() { + checkNotNull(mTimer) { "thread is not alive" } + return mTimer as Timer + } + + fun clearTimeout(id: Int): Boolean { + return timer.clearTimeout(id) + } + + fun setInterval(listener: Any?, interval: Long, vararg args: Any?): Int { + return timer.setInterval(listener!!, interval, *args as Array) + } + + fun setInterval(listener: Any?): Int { + return setInterval(listener, 1) + } + + fun clearInterval(id: Int): Boolean { + return timer.clearInterval(id) + } + + fun setImmediate(listener: Any, vararg args: Any?): Int { + return timer.setImmediate(listener, *args as Array) + } + + fun clearImmediate(id: Int): Boolean { + return timer.clearImmediate(id) + } + + @Throws(InterruptedException::class) + fun waitFor() { + synchronized(mRunningLock) { + if (mRunning) return + mRunningLock.wait() + } + } + + override fun toString(): String { + return "Thread[$name,$priority]" + } + + companion object { + private val sTimerMap = ConcurrentHashMap() + + @JvmStatic + fun getTimerForThread(thread: Thread): Timer? { + return sTimerMap[thread] + } + + val timerForCurrentThread: Timer? + get() = getTimerForThread(currentThread()) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/core/ui/dialog/JsDialogBuilder.java b/app/src/main/java/org/autojs/autojs/core/ui/dialog/JsDialogBuilder.java index 3cade410..0ced880e 100644 --- a/app/src/main/java/org/autojs/autojs/core/ui/dialog/JsDialogBuilder.java +++ b/app/src/main/java/org/autojs/autojs/core/ui/dialog/JsDialogBuilder.java @@ -20,7 +20,7 @@ public class JsDialogBuilder extends MaterialDialog.Builder { private final Timer mTimer; private final Loopers mLoopers; private JsDialog mDialog; - private volatile int mWaitId = -1; + private volatile Loopers.AsyncTask task; public JsDialogBuilder(Context context, ScriptRuntime runtime) { @@ -55,14 +55,14 @@ public class JsDialogBuilder extends MaterialDialog.Builder { } }); dismissListener(dialog -> { - mTimer.postDelayed(() -> mLoopers.doNotWaitWhenIdle(mWaitId), 0); + mTimer.postDelayed(() -> mLoopers.removeAsyncTask(task), 0); emit("dismiss", dialog); }); cancelListener(dialog -> emit("cancel", dialog)); } public void onShowCalled() { - mTimer.postDelayed(() -> mWaitId = mLoopers.waitWhenIdle(), 0); + mTimer.postDelayed(() -> task = mLoopers.createAndAddAsyncTask("js-dialog"), 0); } public JsDialog getDialog() { diff --git a/app/src/main/java/org/autojs/autojs/engine/RhinoJavaScriptEngine.kt b/app/src/main/java/org/autojs/autojs/engine/RhinoJavaScriptEngine.kt index 52243e86..3adb1842 100644 --- a/app/src/main/java/org/autojs/autojs/engine/RhinoJavaScriptEngine.kt +++ b/app/src/main/java/org/autojs/autojs/engine/RhinoJavaScriptEngine.kt @@ -5,6 +5,7 @@ import android.view.View import org.autojs.autojs.core.automator.UiObjectCollection import org.autojs.autojs.core.ui.ViewExtras import org.autojs.autojs.engine.module.AssetAndUrlModuleSourceProvider +import org.autojs.autojs.engine.module.ScopeRequire import org.autojs.autojs.execution.ExecutionConfig import org.autojs.autojs.pio.UncheckedIOException import org.autojs.autojs.project.ScriptConfig @@ -121,11 +122,7 @@ open class RhinoJavaScriptEngine(private val mAndroidContext: android.content.Co mAndroidContext, MODULES_PATH, listOf(File("/").toURI()) ) - RequireBuilder() - .setModuleScriptProvider(SoftCachingModuleScriptProvider(provider)) - .setSandboxed(true) - .createRequire(context, scope) - .install(scope) + ScopeRequire(context, scope,SoftCachingModuleScriptProvider(provider)).install(scope) } protected fun createScope(context: Context): TopLevelScope { diff --git a/app/src/main/java/org/autojs/autojs/engine/module/AssetAndUrlModuleSourceProvider.java b/app/src/main/java/org/autojs/autojs/engine/module/AssetAndUrlModuleSourceProvider.java deleted file mode 100644 index fd9738fd..00000000 --- a/app/src/main/java/org/autojs/autojs/engine/module/AssetAndUrlModuleSourceProvider.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.autojs.autojs.engine.module; - -import android.content.res.AssetManager; - -import org.autojs.autojs.engine.encryption.ScriptEncryption; -import org.autojs.autojs.script.EncryptedScriptFileHeader; -import org.mozilla.javascript.commonjs.module.provider.ModuleSource; - -import java.io.ByteArrayInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLConnection; -import java.util.List; - -/** - * Created by Stardust on 2017/5/9. - */ -public class AssetAndUrlModuleSourceProvider extends UrlModuleSourceProvider { - - private final android.content.Context mContext; - private final URI mBaseURI; - private final String mAssetDirPath; - private final AssetManager mAssetManager; - - public AssetAndUrlModuleSourceProvider(android.content.Context context, String assetDirPath, List list) { - super(list, null); - mContext = context; - mAssetDirPath = assetDirPath; - mBaseURI = URI.create("file:///android_asset/" + assetDirPath); - mAssetManager = mContext.getAssets(); - } - - @Override - protected ModuleSource loadFromPrivilegedLocations(String moduleId, Object validator) throws IOException, URISyntaxException { - String moduleIdWithExtension = moduleId; - if (!moduleIdWithExtension.endsWith(".js")) { - moduleIdWithExtension += ".js"; - } - try { - return new ModuleSource(new InputStreamReader(mAssetManager.open(mAssetDirPath + "/" + moduleIdWithExtension)), null, - new URI(mBaseURI.toString() + "/" + moduleIdWithExtension), mBaseURI, validator); - } catch (FileNotFoundException e) { - return super.loadFromPrivilegedLocations(moduleId, validator); - } - } - - @Override - protected Reader getReader(URLConnection urlConnection) throws IOException { - InputStream stream = urlConnection.getInputStream(); - byte[] bytes = new byte[stream.available()]; - stream.read(bytes); - stream.close(); - if (EncryptedScriptFileHeader.isValidFile(bytes)) { - byte[] clearText = ScriptEncryption.decrypt(bytes, EncryptedScriptFileHeader.BLOCK_SIZE, bytes.length); - return new InputStreamReader(new ByteArrayInputStream(clearText)); - } - return new InputStreamReader(new ByteArrayInputStream(bytes)); - } -} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/engine/module/AssetAndUrlModuleSourceProvider.kt b/app/src/main/java/org/autojs/autojs/engine/module/AssetAndUrlModuleSourceProvider.kt new file mode 100644 index 00000000..96dfdbcb --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/engine/module/AssetAndUrlModuleSourceProvider.kt @@ -0,0 +1,141 @@ +package org.autojs.autojs.engine.module + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import com.google.gson.Gson +import okhttp3.OkHttpClient +import okhttp3.Request +import org.mozilla.javascript.commonjs.module.provider.ModuleSource +import org.mozilla.javascript.commonjs.module.provider.ModuleSourceProviderBase +import java.io.File +import java.io.InputStream +import java.io.InputStreamReader +import java.net.URI + +class AssetAndUrlModuleSourceProvider( + context: Context, + assetDirPath: String, + list: List? = null +) : ModuleSourceProviderBase() { + val mContext = context + private val okHttpClient = OkHttpClient.Builder().followRedirects(true).build() + private val contentResolver: ContentResolver = context.contentResolver + private val moduleSources: ArrayList = arrayListOf(mBaseURI, npmModuleSource) + + companion object { + val mBaseURI: URI = URI.create("file:/android_asset/modules") + val npmModuleSource:URI = URI.create("file:/android_asset/modules/npm") + } + //初始化脚本以及启动文件只会从此方法加载模块,子模块加载没有以"./"或"../"开头的模块也会从此方法加载 + override fun loadFromPrivilegedLocations(moduleId: String, validator: Any?): ModuleSource? { + //println("加载私有模块:$moduleId") + val uri = if (moduleId.startsWith("/")) { + File(moduleId).toURI() + } else if (moduleId.startsWith("http://") || moduleId.startsWith("https://")) { + URI.create(moduleId) + } else null + if (uri != null) { + return loadFromUri(uri, File(uri.path).parentFile?.toURI(), validator) + } + for (baseUri in moduleSources) { + val sourceUri = URI.create("$baseUri/$moduleId") + val moduleSource = loadFromUri(sourceUri, baseUri, validator) + if (moduleSource != null) { + return moduleSource + } + } + return null + } + + //这里处理node_module目录的模块 + override fun loadFromFallbackLocations(moduleId: String, validator: Any?): ModuleSource? { + return super.loadFromFallbackLocations(moduleId, validator) + } + + //子模块以相对路径加载时调用此方法 + override fun loadFromUri(uri: URI, base: URI?, validator: Any?): ModuleSource? { + var uri = uri + if (uri.scheme == null) uri = File(uri.path).toURI() + //println("加载模块:$uri") + if (uri.scheme == "http" || uri.scheme == "https") { + return loadFromHttp(uri, base, validator) + } + val moduleSource = loadAt(uri, base, validator) ?: loadAt( + File(uri.path + ".js").toURI(), base, validator + ) + if (moduleSource != null) { + return moduleSource + } + //尝试从目录加载 + //尝试读取package.json指定的文件 + val mainFile: URI? = try { + val packageFile = File(uri.path, "package.json") + val json = Gson().fromJson>( + InputStreamReader(packageFile.inputStream()), + Map::class.java + ) + val main = json["main"] as String? + main?.let { + packageFile.toURI().resolve(main) + } + } catch (e: Exception) { + null + } + val main: URI = mainFile ?: File(uri.path, "index.js").toURI() + return loadAt(main, uri, validator) + } + + private fun loadAt(uri: URI, base: URI?, validator: Any?): ModuleSource? { + if (uri.scheme == "http" || uri.scheme == "https") { + return loadFromHttp(uri, base, validator) + } + return try { + val inputStream = if (uri.path.startsWith("/android_asset/")) { + mContext.assets.open(uri.path.replace("/android_asset/", "")) + } else contentResolver.openInputStream(Uri.parse(uri.toString())) + if (inputStream != null) { + createModuleSource(inputStream, uri, base, validator) + } else null + } catch (e: Exception) { + null + } + } + + private fun loadFromHttp(uri: URI, base: URI?, validator: Any?): ModuleSource? { + return try { + Request.Builder().url(uri.toString()).build().let { request -> + val response = okHttpClient.newCall(request).execute() + if (!response.isSuccessful) { + response.close() + return null + } + response.body?.let { + val charset = it.contentType()?.charset()?.toString() ?: "utf-8" + return createModuleSource(it.byteStream(), uri, base, validator, charset) + } + } + } catch (e: Exception) { + null + } + } + + private fun createModuleSource( + inputStream: InputStream, + uri: URI, + base: URI?, + validator: Any?, + charset: String? = null + ): ModuleSource { + val id = if (uri.scheme == "file") { + URI.create(uri.path) + } else uri + return ModuleSource( + InputStreamReader(inputStream, charset ?: "utf-8"), + null, + id, + base, + validator + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/engine/module/ScopeRequire.kt b/app/src/main/java/org/autojs/autojs/engine/module/ScopeRequire.kt new file mode 100644 index 00000000..d92620a6 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/engine/module/ScopeRequire.kt @@ -0,0 +1,262 @@ +package org.autojs.autojs.engine.module + +import org.mozilla.javascript.* +import org.mozilla.javascript.commonjs.module.ModuleScope +import org.mozilla.javascript.commonjs.module.ModuleScript +import org.mozilla.javascript.commonjs.module.ModuleScriptProvider + +import java.io.File +import java.net.URI +import java.net.URISyntaxException +import java.util.concurrent.ConcurrentHashMap + + +open class ScopeRequire( + cx: Context, private val nativeScope: Scriptable, + private val moduleScriptProvider: ModuleScriptProvider, private val preExec: Script?, + private val postExec: Script?, private val sandboxed: Boolean = true +) : BaseFunction() { + private var paths: Scriptable? = null + private var mainModuleId: String? = null + private var mainExports: Scriptable? = null + + // Modules that completed loading; visible to all threads + private val exportedModuleInterfaces: MutableMap = ConcurrentHashMap() + private val loadLock = Any() + + constructor(cx: Context, nativeScope: Scriptable, moduleScriptProvider: ModuleScriptProvider) + : this(cx, nativeScope, moduleScriptProvider, null, null, false) + + init { + prototype = getFunctionPrototype(nativeScope) + if (!sandboxed) { + paths = cx.newArray(nativeScope, 0) + defineReadOnlyProperty(this, "paths", paths) + } else paths = null + } + + + fun requireMain(cx: Context, mainModuleId: String): Scriptable? { + if (this.mainModuleId != null) { + if (this.mainModuleId != mainModuleId) { + throw IllegalStateException("Main module already set to " + this.mainModuleId) + } + return mainExports + } + val moduleScript: ModuleScript? = try { + moduleScriptProvider.getModuleScript(cx, mainModuleId, null, null, paths) + } catch (x: RuntimeException) { + throw x + } catch (x: Exception) { + throw RuntimeException(x) + } + if (moduleScript != null) { + mainExports = getExportedModuleInterface( + cx, mainModuleId, + null, null, true + ) + } else if (!sandboxed) { + var mainUri: URI? = try { + URI(mainModuleId) + } catch (_: URISyntaxException) { + null + } + if (mainUri == null || !mainUri.isAbsolute) { + val file = File(mainModuleId) + if (!file.isFile) { + throw ScriptRuntime.throwError( + cx, nativeScope, + "Module \"$mainModuleId\" not found." + ) + } + mainUri = file.toURI() + } + mainExports = getExportedModuleInterface( + cx, mainUri.toString(), + mainUri, null, true + ) + } + this.mainModuleId = mainModuleId + return mainExports + } + + fun install(scope: Scriptable?) { + putProperty(scope, "require", this) + } + + override fun call(cx: Context, scope: Scriptable, thisObj: Scriptable, args: Array?): Any { + if (args == null || args.isEmpty()) { + throw ScriptRuntime.throwError( + cx, scope, + "require() needs one argument" + ) + } + var id = Context.jsToJava(args[0], String::class.java) as String + var uri: URI? = null + var base: URI? = null + if (id.startsWith("./") || id.startsWith("../")) { + if (thisObj !is ModuleScope) { + throw ScriptRuntime.throwError( + cx, scope, + "Can't resolve relative module ID \"" + id + + "\" when require() is used outside of a module" + ) + } + base = thisObj.base + val current = thisObj.uri + uri = current.resolve(id) + if (base == null) { + id = uri.toString() + } else { + id = base.relativize(current).resolve(id).toString() + if (id[0] == '.') { + if (sandboxed) { + throw ScriptRuntime.throwError( + cx, scope, + "Module \"$id\" is not contained in sandbox." + ) + } + id = uri.toString() + } + } + } + return (getExportedModuleInterface(cx, id, uri, base, false))!! + } + + override fun construct(cx: Context, scope: Scriptable, args: Array): Scriptable { + throw ScriptRuntime.throwError( + cx, scope, + "require() can not be invoked as a constructor" + ) + } + + private fun getExportedModuleInterface( + cx: Context, id: String, uri: URI?, base: URI?, isMain: Boolean + ): Scriptable? { + // Check if the requested module is already completely loaded + var exports = exportedModuleInterfaces[id] + if (exports != null) { + if (isMain) { + throw IllegalStateException("Attempt to set main module after it was loaded") + } else + return exports + } + var threadLoadingModules: MutableMap? = + loadingModuleInterfaces.get() as? MutableMap + exports = threadLoadingModules?.get(id) + if (exports != null) return exports + + synchronized(loadLock) { + exports = exportedModuleInterfaces[id] + if (exports != null) return exports + + val moduleScript: ModuleScript = getModule(cx, id, uri, base) + if (sandboxed && !moduleScript.isSandboxed) { + throw ScriptRuntime.throwError( + cx, nativeScope, ("Module \"$id\" is not contained in sandbox.") + ) + } + exports = cx.newObject(nativeScope) + val outermostLocked: Boolean = threadLoadingModules == null + if (outermostLocked) { + threadLoadingModules = HashMap() + loadingModuleInterfaces.set(threadLoadingModules) + } + + threadLoadingModules?.set(id, exports!!) + try { + val newExports: Scriptable = executeModuleScript( + cx, id, exports, + moduleScript, isMain + ) + if (exports !== newExports) { + threadLoadingModules?.put(id, newExports) + exports = newExports + } + } catch (e: RuntimeException) { + threadLoadingModules?.remove(id) + throw e + } finally { + if (outermostLocked) { + exportedModuleInterfaces.putAll((threadLoadingModules!!)) + loadingModuleInterfaces.set(null) + } + } + } + return exports + } + + private fun executeModuleScript( + cx: Context, id: String, + exports: Scriptable?, moduleScript: ModuleScript, isMain: Boolean + ): Scriptable { + val moduleObject = cx.newObject(nativeScope) as ScriptableObject + val uri = moduleScript.uri + val base = moduleScript.base + defineReadOnlyProperty(moduleObject, "id", id) + if (!sandboxed) { + defineReadOnlyProperty(moduleObject, "uri", uri.toString()) + } + val executionScope: Scriptable = ModuleScope(nativeScope, uri, base) + executionScope.put("__filename", executionScope, File(uri.path).path) + executionScope.put("__dirname", executionScope, File(uri.path).parent) + executionScope.put("exports", executionScope, exports) + executionScope.put("module", executionScope, moduleObject) + moduleObject.put("exports", moduleObject, exports) + install(executionScope) + if (isMain) { + defineReadOnlyProperty(this, "main", moduleObject) + } + //创建新作用域 + val funScope = cx.newObject(executionScope) + funScope.parentScope = executionScope + + executeOptionalScript(preExec, cx, funScope) + moduleScript.script.exec(cx, funScope) + executeOptionalScript(postExec, cx, funScope) + return ScriptRuntime.toObject( + cx, nativeScope, + getProperty(moduleObject, "exports") + ) + } + + private fun getModule(cx: Context, id: String, uri: URI?, base: URI?): ModuleScript { + try { + return moduleScriptProvider.getModuleScript(cx, id, uri, base, paths) + ?: throw ScriptRuntime.throwError( + cx, nativeScope, ("Module \"$id\" not found.") + ) + } catch (e: RuntimeException) { + throw e + } catch (e: Exception) { + throw Context.throwAsScriptRuntimeEx(e) + } + } + + override fun getFunctionName() = "require" + override fun getArity() = 1 + override fun getLength() = 1 + + companion object { + private const val serialVersionUID = 1L + + private val loadingModuleInterfaces = ThreadLocal>() + private fun executeOptionalScript( + script: Script?, cx: Context, + executionScope: Scriptable + ) { + script?.exec(cx, executionScope) + } + + private fun defineReadOnlyProperty( + obj: ScriptableObject, + name: String, value: Any? + ) { + putProperty(obj, name, value) + obj.setAttributes( + name, READONLY or + PERMANENT + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/engine/module/UrlModuleSourceProvider.java b/app/src/main/java/org/autojs/autojs/engine/module/UrlModuleSourceProvider.java deleted file mode 100644 index 82b1a7e2..00000000 --- a/app/src/main/java/org/autojs/autojs/engine/module/UrlModuleSourceProvider.java +++ /dev/null @@ -1,360 +0,0 @@ -package org.autojs.autojs.engine.module; - -import org.mozilla.javascript.commonjs.module.provider.DefaultUrlConnectionExpiryCalculator; -import org.mozilla.javascript.commonjs.module.provider.ModuleSource; -import org.mozilla.javascript.commonjs.module.provider.ModuleSourceProviderBase; -import org.mozilla.javascript.commonjs.module.provider.ParsedContentType; -import org.mozilla.javascript.commonjs.module.provider.UrlConnectionExpiryCalculator; -import org.mozilla.javascript.commonjs.module.provider.UrlConnectionSecurityDomainProvider; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.Serializable; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLConnection; -import java.util.Iterator; -import java.util.List; - - -/** - * A URL-based script provider that can load modules against a set of base - * privileged and fallback URIs. It is deliberately not named "URI provider" - * but a "URL provider" since it actually only works against those URIs that - * are URLs (and the JRE has a protocol handler for them). It creates cache - * validators that are suitable for use with both file: and http: URL - * protocols. Specifically, it is able to use both last-modified timestamps and - * ETags for cache revalidation, and follows the HTTP cache expiry calculation - * model, and allows for fallback heuristic expiry calculation when no server - * specified expiry is provided. - * - * @author Attila Szegedi - * @version $Id: UrlModuleSourceProvider.java,v 1.4 2011/04/07 20:26:12 hannes%helma.at Exp $ - */ -public class UrlModuleSourceProvider extends ModuleSourceProviderBase { - private static final long serialVersionUID = 1L; - - private final Iterable privilegedUris; - private final Iterable fallbackUris; - private final UrlConnectionSecurityDomainProvider - urlConnectionSecurityDomainProvider; - private final UrlConnectionExpiryCalculator urlConnectionExpiryCalculator; - - /** - * Creates a new module script provider that loads modules against a set of - * privileged and fallback URIs. It will use a fixed default cache expiry - * of 60 seconds, and provide no security domain objects for the resource. - * - * @param privilegedUris an iterable providing the privileged URIs. Can be - * null if no privileged URIs are used. - * @param fallbackUris an iterable providing the fallback URIs. Can be - * null if no fallback URIs are used. - */ - public UrlModuleSourceProvider(Iterable privilegedUris, - Iterable fallbackUris) { - this(privilegedUris, fallbackUris, - new DefaultUrlConnectionExpiryCalculator(), null); - } - - /** - * Creates a new module script provider that loads modules against a set of - * privileged and fallback URIs. It will use the specified heuristic cache - * expiry calculator and security domain provider. - * - * @param privilegedUris an iterable providing the privileged URIs. Can be - * null if no privileged URIs are used. - * @param fallbackUris an iterable providing the fallback URIs. Can be - * null if no fallback URIs are used. - * @param urlConnectionExpiryCalculator the calculator object for heuristic - * calculation of the resource expiry, used when no expiry is provided by - * the server of the resource. Can be null, in which case the maximum age - * of cached entries without validation will be zero. - * @param urlConnectionSecurityDomainProvider object that provides security - * domain objects for the loaded sources. Can be null, in which case the - * loaded sources will have no security domain associated with them. - */ - public UrlModuleSourceProvider(Iterable privilegedUris, - Iterable fallbackUris, - UrlConnectionExpiryCalculator urlConnectionExpiryCalculator, - UrlConnectionSecurityDomainProvider urlConnectionSecurityDomainProvider) { - this.privilegedUris = privilegedUris; - this.fallbackUris = fallbackUris; - this.urlConnectionExpiryCalculator = urlConnectionExpiryCalculator; - this.urlConnectionSecurityDomainProvider = - urlConnectionSecurityDomainProvider; - } - - @Override - protected ModuleSource loadFromPrivilegedLocations( - String moduleId, Object validator) - throws IOException, URISyntaxException { - return loadFromPathList(moduleId, validator, privilegedUris); - } - - @Override - protected ModuleSource loadFromFallbackLocations( - String moduleId, Object validator) - throws IOException, URISyntaxException { - return loadFromPathList(moduleId, validator, fallbackUris); - } - - private ModuleSource loadFromPathList(String moduleId, - Object validator, Iterable paths) - throws IOException, URISyntaxException { - if (paths == null) { - return null; - } - for (URI path : paths) { - final ModuleSource moduleSource = loadFromUri( - path.resolve(moduleId), path, validator); - if (moduleSource != null) { - return moduleSource; - } - } - return null; - } - - @Override - protected ModuleSource loadFromUri(URI uri, URI base, Object validator) - throws IOException, URISyntaxException { - // We expect modules to have a ".js" file name extension ... - URI fullUri = new URI(uri + ".js"); - ModuleSource source = loadFromActualUri(fullUri, base, validator); - // ... but for compatibility we support modules without extension, - // or ids with explicit extension. - return source != null ? - source : loadFromActualUri(uri, base, validator); - } - - protected ModuleSource loadFromActualUri(URI uri, URI base, Object validator) - throws IOException { - final URL url = new URL(base == null ? null : base.toURL(), uri.toString()); - final long request_time = System.currentTimeMillis(); - final URLConnection urlConnection = openUrlConnection(url); - final URLValidator applicableValidator; - if (validator instanceof final URLValidator uriValidator) { - applicableValidator = uriValidator.appliesTo(uri) ? uriValidator : - null; - } else { - applicableValidator = null; - } - if (applicableValidator != null) { - applicableValidator.applyConditionals(urlConnection); - } - try { - urlConnection.connect(); - if (applicableValidator != null && - applicableValidator.updateValidator(urlConnection, - request_time, urlConnectionExpiryCalculator)) { - close(urlConnection); - return NOT_MODIFIED; - } - - return new ModuleSource(getReader(urlConnection), - getSecurityDomain(urlConnection), uri, base, - new URLValidator(uri, urlConnection, request_time, - urlConnectionExpiryCalculator)); - } catch (FileNotFoundException e) { - return null; - } catch (RuntimeException | IOException e) { - close(urlConnection); - throw e; - } - } - - protected Reader getReader(URLConnection urlConnection) - throws IOException { - return new InputStreamReader(urlConnection.getInputStream(), - getCharacterEncoding(urlConnection)); - } - - protected String getCharacterEncoding(URLConnection urlConnection) { - final ParsedContentType pct = new ParsedContentType( - urlConnection.getContentType()); - final String encoding = pct.getEncoding(); - if (encoding != null) { - return encoding; - } - final String contentType = pct.getContentType(); - if (contentType != null && contentType.startsWith("text/")) { - return "8859_1"; - } - return "utf-8"; - } - - protected Object getSecurityDomain(URLConnection urlConnection) { - return urlConnectionSecurityDomainProvider == null ? null : - urlConnectionSecurityDomainProvider.getSecurityDomain( - urlConnection); - } - - private void close(URLConnection urlConnection) { - try { - urlConnection.getInputStream().close(); - } catch (IOException e) { - onFailedClosingUrlConnection(urlConnection, e); - } - } - - /** - * Override if you want to get notified if the URL connection fails to - * close. Does nothing by default. - * - * @param urlConnection the connection - * @param cause the cause it failed to close. - */ - protected void onFailedClosingUrlConnection(URLConnection urlConnection, - IOException cause) { - } - - /** - * Can be overridden in subclasses to customize the URL connection opening - * process. By default, just calls {@link URL#openConnection()}. - * - * @param url the URL - * @return a connection to the URL. - * @throws IOException if an I/O error occurs. - */ - protected URLConnection openUrlConnection(URL url) throws IOException { - return url.openConnection(); - } - - @Override - protected boolean entityNeedsRevalidation(Object validator) { - return !(validator instanceof URLValidator) - || ((URLValidator) validator).entityNeedsRevalidation(); - } - - private static class URLValidator implements Serializable { - private static final long serialVersionUID = 1L; - - private final URI uri; - private final long lastModified; - private final String entityTags; - private long expiry; - - public URLValidator(URI uri, URLConnection urlConnection, - long request_time, UrlConnectionExpiryCalculator - urlConnectionExpiryCalculator) { - this.uri = uri; - this.lastModified = urlConnection.getLastModified(); - this.entityTags = getEntityTags(urlConnection); - expiry = calculateExpiry(urlConnection, request_time, - urlConnectionExpiryCalculator); - } - - boolean updateValidator(URLConnection urlConnection, long request_time, - UrlConnectionExpiryCalculator urlConnectionExpiryCalculator) - throws IOException { - boolean isResourceChanged = isResourceChanged(urlConnection); - if (!isResourceChanged) { - expiry = calculateExpiry(urlConnection, request_time, - urlConnectionExpiryCalculator); - } - return isResourceChanged; - } - - private boolean isResourceChanged(URLConnection urlConnection) - throws IOException { - if (urlConnection instanceof HttpURLConnection) { - return ((HttpURLConnection) urlConnection).getResponseCode() == - HttpURLConnection.HTTP_NOT_MODIFIED; - } - return lastModified != urlConnection.getLastModified(); - } - - private long calculateExpiry(URLConnection urlConnection, - long request_time, UrlConnectionExpiryCalculator - urlConnectionExpiryCalculator) { - if ("no-cache".equals(urlConnection.getHeaderField("Pragma"))) { - return 0L; - } - final String cacheControl = urlConnection.getHeaderField( - "Cache-Control"); - if (cacheControl != null) { - if (cacheControl.contains("no-cache")) { - return 0L; - } - final int max_age = getMaxAge(cacheControl); - if (-1 != max_age) { - final long response_time = System.currentTimeMillis(); - final long apparent_age = Math.max(0, response_time - - urlConnection.getDate()); - final long corrected_received_age = Math.max(apparent_age, - urlConnection.getHeaderFieldInt("Age", 0) * 1000L); - final long response_delay = response_time - request_time; - final long corrected_initial_age = corrected_received_age + - response_delay; - final long creation_time = response_time - - corrected_initial_age; - return max_age * 1000L + creation_time; - } - } - final long explicitExpiry = urlConnection.getHeaderFieldDate( - "Expires", -1L); - if (explicitExpiry != -1L) { - return explicitExpiry; - } - return urlConnectionExpiryCalculator == null ? 0L : - urlConnectionExpiryCalculator.calculateExpiry(urlConnection); - } - - private int getMaxAge(String cacheControl) { - final int maxAgeIndex = cacheControl.indexOf("max-age"); - if (maxAgeIndex == -1) { - return -1; - } - final int eq = cacheControl.indexOf('=', maxAgeIndex + 7); - if (eq == -1) { - return -1; - } - final int comma = cacheControl.indexOf(',', eq + 1); - final String strAge; - if (comma == -1) { - strAge = cacheControl.substring(eq + 1); - } else { - strAge = cacheControl.substring(eq + 1, comma); - } - try { - return Integer.parseInt(strAge); - } catch (NumberFormatException e) { - return -1; - } - } - - private String getEntityTags(URLConnection urlConnection) { - final List etags = urlConnection.getHeaderFields().get("ETag"); - if (etags == null || etags.isEmpty()) { - return null; - } - final StringBuilder b = new StringBuilder(); - final Iterator it = etags.iterator(); - b.append(it.next()); - while (it.hasNext()) { - b.append(", ").append(it.next()); - } - return b.toString(); - } - - boolean appliesTo(URI uri) { - return this.uri.equals(uri); - } - - void applyConditionals(URLConnection urlConnection) { - if (lastModified != 0L) { - urlConnection.setIfModifiedSince(lastModified); - } - if (entityTags != null && entityTags.length() > 0) { - urlConnection.addRequestProperty("If-None-Match", entityTags); - } - } - - boolean entityNeedsRevalidation() { - return System.currentTimeMillis() > expiry; - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/Sensors.java b/app/src/main/java/org/autojs/autojs/runtime/api/Sensors.java index 7275494b..58b781f9 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/Sensors.java +++ b/app/src/main/java/org/autojs/autojs/runtime/api/Sensors.java @@ -22,7 +22,9 @@ import java.util.Set; /** * Created by Stardust on 2018/2/5. */ -public class Sensors extends EventEmitter implements Loopers.LooperQuitHandler { + +public class Sensors extends EventEmitter { + public class SensorEventEmitter extends EventEmitter implements SensorEventListener { @@ -82,6 +84,13 @@ public class Sensors extends EventEmitter implements Loopers.LooperQuitHandler { private final ScriptBridges mScriptBridges; private final SensorEventEmitter mNoOpSensorEventEmitter; private final ScriptRuntime mScriptRuntime; + private final Loopers.AsyncTask mAsyncTask = new Loopers.AsyncTask("Sensors"){ + @Override + public boolean onFinish(@NonNull Loopers loopers) { + return !mSensorEventEmitters.isEmpty(); + } + }; + public Sensors(Context context, ScriptRuntime runtime) { super(runtime.bridges); @@ -89,7 +98,7 @@ public class Sensors extends EventEmitter implements Loopers.LooperQuitHandler { mScriptBridges = runtime.bridges; mNoOpSensorEventEmitter = new SensorEventEmitter(runtime.bridges); mScriptRuntime = runtime; - runtime.loopers.addLooperQuitHandler(this); + runtime.loopers.addAsyncTask(mAsyncTask); } public SensorEventEmitter register(String sensorName) { @@ -121,11 +130,6 @@ public class Sensors extends EventEmitter implements Loopers.LooperQuitHandler { return emitter; } - @Override - public boolean shouldQuit() { - return mSensorEventEmitters.isEmpty(); - } - public Sensor getSensor(String sensorName) { sensorName = sensorName.toUpperCase(); Integer type = SENSORS.get(sensorName); @@ -158,6 +162,6 @@ public class Sensors extends EventEmitter implements Loopers.LooperQuitHandler { } mSensorEventEmitters.clear(); } - mScriptRuntime.loopers.removeLooperQuitHandler(this); + mScriptRuntime.loopers.removeAsyncTask(mAsyncTask); } } diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/Threads.kt b/app/src/main/java/org/autojs/autojs/runtime/api/Threads.kt index 77b8df62..d3be8e14 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/Threads.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/Threads.kt @@ -1,12 +1,15 @@ package org.autojs.autojs.runtime.api -import org.autojs.autojs.annotation.ScriptInterface import org.autojs.autojs.concurrent.VolatileDispose +import org.autojs.autojs.core.looper.Loopers import org.autojs.autojs.core.looper.MainThreadProxy import org.autojs.autojs.core.looper.TimerThread import org.autojs.autojs.runtime.ScriptRuntime -import org.autojs.autojs.util.StringUtils.str -import org.autojs.autojs6.R +import org.autojs.autojs.runtime.exception.ScriptInterruptedException +import org.mozilla.javascript.BaseFunction +import org.mozilla.javascript.Context +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.locks.ReentrantLock @@ -14,58 +17,91 @@ import java.util.concurrent.locks.ReentrantLock * Created by Stardust on 2017/12/3. */ class Threads(private val mRuntime: ScriptRuntime) { - private val mThreads = HashSet() + val mainThread: Thread = Thread.currentThread() private val mMainThreadProxy = MainThreadProxy(Thread.currentThread(), mRuntime) private var mSpawnCount = 0 + private var mTaskCount = AtomicLong(0) private var mExit = false + private val looperTask = Loopers.AsyncTask("AsyncTaskThreadPool") + private val threadPool = Executors.newFixedThreadPool(20, ThreadFactory { + val thread = Thread(fun() { + Context.enter() + try { + it.run() + } finally { + Context.exit() + } + }) + thread.name = mainThread.name + " (AsyncThread)" + thread + }) - val mainThread: Thread = Thread.currentThread() + fun currentThread(): Any { + val thread = Thread.currentThread() + return if (thread === mainThread) mMainThreadProxy else thread + } - fun currentThread(): Any = Thread.currentThread().let { thread -> - if (thread === mainThread) mMainThreadProxy else thread + fun runTaskForThreadPool(runnable: BaseFunction) { + if (mTaskCount.addAndGet(1) == 1L) mRuntime.loopers.addAsyncTask(looperTask) + threadPool.execute { + try { + runnable.call( + Context.getCurrentContext(), runnable.parentScope, runnable, + emptyArray() + ) + } catch (e: Throwable) { + if (!ScriptInterruptedException.causedByInterrupted(e)) { + mRuntime.console.error("$this: ", e) + } + } finally { + if (mTaskCount.addAndGet(-1) == 0L) { + mRuntime.loopers.removeAsyncTask(looperTask) + } + } + } } fun start(runnable: Runnable): TimerThread { val thread = createThread(runnable) synchronized(mThreads) { - check(!mExit) { str(R.string.error_script_is_on_exiting) } - thread.let { - mThreads.add(it) - it.name = "${mainThread.name} (Spawn-$mSpawnCount)" - mSpawnCount++ - it.start() - } + check(!mExit) { "script exiting" } + mThreads.add(thread) + thread.name = mainThread.name + " (Spawn-" + mSpawnCount + ")" + mSpawnCount++ + thread.start() } return thread } private fun createThread(runnable: Runnable): TimerThread { - val millis = mRuntime.timers.maxCallbackUptimeMillisForAllThreads - return object : TimerThread(mRuntime, millis, runnable) { - + return object : TimerThread(mRuntime, runnable) { override fun onExit() { synchronized(mThreads) { mThreads.remove(currentThread()) } super.onExit() } - } } - fun disposable() = VolatileDispose() + fun disposable(): VolatileDispose<*> { + return VolatileDispose() + } - fun atomic(value: Long) = AtomicLong(value) + fun atomic(value: Long): AtomicLong { + return AtomicLong(value) + } fun atomic() = AtomicLong() fun lock() = ReentrantLock() fun shutDownAll() { + threadPool.shutdownNow() synchronized(mThreads) { - mThreads.apply { - forEach { it.interrupt() } - clear() + for (thread in mThreads) { + thread.interrupt() } + mThreads.clear() } } @@ -76,6 +112,7 @@ class Threads(private val mRuntime: ScriptRuntime) { } } - fun hasRunningThreads(): Boolean = synchronized(mThreads) { return mThreads.isNotEmpty() } - + fun hasRunningThreads(): Boolean { + synchronized(mThreads) { return mThreads.isNotEmpty() } + } } \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/Timers.java b/app/src/main/java/org/autojs/autojs/runtime/api/Timers.java index 37f0977b..56e5628a 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/Timers.java +++ b/app/src/main/java/org/autojs/autojs/runtime/api/Timers.java @@ -1,9 +1,6 @@ package org.autojs.autojs.runtime.api; import android.os.Looper; -import android.os.SystemClock; - -import org.autojs.autojs.concurrent.VolatileBox; import org.autojs.autojs.core.looper.Timer; import org.autojs.autojs.core.looper.TimerThread; import org.autojs.autojs.runtime.ScriptRuntime; @@ -11,28 +8,24 @@ import org.autojs.autojs.runtime.ScriptRuntime; /** * Created by Stardust on 2017/7/21. */ + public class Timers { private static final String LOG_TAG = "Timers"; - private final VolatileBox mMaxCallbackUptimeMillisForAllThreads = new VolatileBox<>(0L); - private final Threads mThreads; - private final Timer mMainTimer; - private final Timer mUiTimer; - + //private VolatileBox mMaxCallbackUptimeMillisForAllThreads = new VolatileBox<>(0L); + private Threads mThreads; + private Timer mUiTimer; + private ScriptRuntime mRuntime; public Timers(ScriptRuntime runtime) { - mMainTimer = new Timer(runtime, mMaxCallbackUptimeMillisForAllThreads); - mUiTimer = new Timer(runtime, mMaxCallbackUptimeMillisForAllThreads, Looper.getMainLooper()); + mUiTimer = new Timer(runtime, Looper.getMainLooper()); mThreads = runtime.threads; + mRuntime = runtime; } public Timer getMainTimer() { - return mMainTimer; - } - - VolatileBox getMaxCallbackUptimeMillisForAllThreads() { - return mMaxCallbackUptimeMillisForAllThreads; + return mRuntime.loopers.getMTimer(); } public Timer getTimerForCurrentThread() { @@ -41,19 +34,27 @@ public class Timers { public Timer getTimerForThread(Thread thread) { if (thread == mThreads.getMainThread()) { - return mMainTimer; + return mRuntime.loopers.getMTimer(); } Timer timer = TimerThread.getTimerForThread(thread); if (timer == null && Looper.myLooper() == Looper.getMainLooper()) { return mUiTimer; } - return timer; + if (timer == null) { + return mRuntime.loopers.getMTimer(); + } else { + return timer; + } } public int setTimeout(Object callback, long delay, Object... args) { return getTimerForCurrentThread().setTimeout(callback, delay, args); } + public int setTimeout(Object callback) { + return setTimeout(callback, 1); + } + public boolean clearTimeout(int id) { return getTimerForCurrentThread().clearTimeout(id); } @@ -62,6 +63,10 @@ public class Timers { return getTimerForCurrentThread().setInterval(listener, interval, args); } + public int setInterval(Object listener) { + return setInterval(listener, 1); + } + public boolean clearInterval(int id) { return getTimerForCurrentThread().clearInterval(id); } @@ -74,17 +79,8 @@ public class Timers { return getTimerForCurrentThread().clearImmediate(id); } - public boolean hasPendingCallbacks() { - //如果是脚本主线程,则检查所有子线程中的定时回调。mFutureCallbackUptimeMillis用来记录所有子线程中定时最久的一个。 - if (mThreads.getMainThread() == Thread.currentThread()) { - return mMaxCallbackUptimeMillisForAllThreads.get() > SystemClock.uptimeMillis(); - } - // 否则检查当前线程的定时回调 - return getTimerForCurrentThread().hasPendingCallbacks(); - } - public void recycle() { - mMainTimer.removeAllCallbacks(); + mRuntime.loopers.getMTimer().removeAllCallbacks(); } } diff --git a/version.properties b/version.properties index 0c9a5fbe..0cb338a1 100644 --- a/version.properties +++ b/version.properties @@ -1,8 +1,8 @@ -#Fri May 26 20:57:48 CST 2023 -BUILD_TIME=1685105868950 +#Sat Jun 10 19:42:58 CST 2023 +BUILD_TIME=1686397378556 COMPILE_SDK_VERSION=33 JAVA_VERSION=20 MIN_SDK_VERSION=24 TARGET_SDK_VERSION=33 -VERSION_BUILD=1909 +VERSION_BUILD=1911 VERSION_NAME=6.3.1