From 12037ba4884a1eb35d395e1d69d10d82bed7b4e9 Mon Sep 17 00:00:00 2001 From: hyb1996 <946994919@qq.com> Date: Sat, 29 Jul 2017 17:46:23 +0800 Subject: [PATCH] change JavaScriptEngine to LoopBasedJavaScriptEngine Everything works like Node.js. loop() will not need be called explicitly any more. Including some changes: * Every thread of script has a looper. * Use a servant thread in gestures functions of GlobalActionAutomator and image available listener of ScreenCapture. * For keeping events key and touch listener alive, add Loopers.waitWhenIdle to control whether or not Looper should quit when it has nothing to do --- .../help/developerguide/如何写自动操作脚本.md | 16 +++ .../documentation/事件与按键、触摸监听.md | 2 - .../main/assets/help/documentation/定时器.md | 20 +--- .../scriptdroid/ui/main/MainActivity.java | 17 ++-- .../scriptdroid/ui/main/SideMenuFragment.java | 1 - .../src/main/assets/javascript_engine_init.js | 2 + autojs/src/main/assets/modules/__timers__.js | 6 +- .../stardust/autojs/ScriptEngineService.java | 4 - .../engine/AbstractScriptEngineManager.java | 18 +--- .../engine/LoopBasedJavaScriptEngine.java | 59 +++++++++++ .../autojs/engine/RhinoJavaScriptEngine.java | 12 ++- .../engine/RhinoJavaScriptEngineManager.java | 42 +------- .../autojs/engine/ScriptEngineManager.java | 2 - .../autojs/runtime/AbstractScriptRuntime.java | 33 ++++++- .../stardust/autojs/runtime/api/Events.java | 25 +++-- .../stardust/autojs/runtime/api/Loopers.java | 99 +++++++++++++++++++ .../stardust/autojs/runtime/api/Timers.java | 43 +++----- .../autojs/runtime/api/image/Images.java | 9 +- .../runtime/api/image/ScreenCapturer.java | 94 ++++++++++++------ .../simpleaction/SimpleActionAutomator.java | 15 ++- .../automator/GlobalActionAutomator.java | 36 ++++++- 21 files changed, 382 insertions(+), 173 deletions(-) create mode 100644 app/src/main/assets/help/developerguide/如何写自动操作脚本.md create mode 100644 autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java create mode 100644 autojs/src/main/java/com/stardust/autojs/runtime/api/Loopers.java diff --git a/app/src/main/assets/help/developerguide/如何写自动操作脚本.md b/app/src/main/assets/help/developerguide/如何写自动操作脚本.md new file mode 100644 index 00000000..d52505e1 --- /dev/null +++ b/app/src/main/assets/help/developerguide/如何写自动操作脚本.md @@ -0,0 +1,16 @@ + +## 控件 + +对于一般软件而言,其界面是由控件组成的。 + +一段文本、一张图片、一个列表都是一个控件(widget),文本用文本控件(TextView)显示,图片用图片控件(ImageView)显示。 + +此外还有一类特殊的控件,其本身不显示内容,而是作为其他控件的"容器"使用,决定在他里面的控件的位置和排放。这类控件称为"布局"(layout)。 + +常见的布局有线性布局(LinearLayout), 相对布局(RelativeLayout), 帧布局(FrameLayout), 表格布局(GridLayout)等。 + +例如线性布局,在他里面的控件是线性(垂直或平行)地摆放的。比如一个水平的线性布局,里面依次有一个文本控件和一个图片控件,那么显示时图片将会在文本的右边。 + +## 控件的属性 + +打开悬浮窗,找到 diff --git a/app/src/main/assets/help/documentation/事件与按键、触摸监听.md b/app/src/main/assets/help/documentation/事件与按键、触摸监听.md index 2512ff83..a12f7a4b 100644 --- a/app/src/main/assets/help/documentation/事件与按键、触摸监听.md +++ b/app/src/main/assets/help/documentation/事件与按键、触摸监听.md @@ -8,7 +8,6 @@ events也是一个\[EventEmitter\](#EventEmitter),可以使用EventEmitter的所有函数,这里只介绍其自身的函数。 -注意,events内置的触摸、按键事件必须调用loop()函数才能正常工作。例如: ``` events.observeKey(); events.onKeyDown("volume_up", function(event){ @@ -18,7 +17,6 @@ events.onKeyDown("menu", function(event){ toast("菜单键被按下了"); }); -loop(); //后面的语句不会再执行了 ``` diff --git a/app/src/main/assets/help/documentation/定时器.md b/app/src/main/assets/help/documentation/定时器.md index 865d8b3a..c4f11548 100644 --- a/app/src/main/assets/help/documentation/定时器.md +++ b/app/src/main/assets/help/documentation/定时器.md @@ -2,22 +2,6 @@ timer 模块暴露了一个全局的 API,用于在某个未来时间段调用 Auto.js 中的计时器函数实现了与 Web 浏览器提供的定时器类似的 API,除了它使用了一个不同的内部实现,它是基于 Android Looper-Handler机制构建的。其实现机制与Node.js比较相似。 -不同于Web浏览器和Node.js,在Auto.js中,必须显式地调用loop()函数开始Looper循环才能使所有计时器开始工作。loop()函数是一个"死循环", 在他之后的语句将不会被执行。 - -### loop() - -开始Looper循环。使所有定时器工作所必须。 -注意,何时调用loop不会影响setTimeout等函数的计时,例如: -``` -setTimout(function(){ - toast("hello"); - exit(); -}, 2000); -sleep(1000); -loop(); -``` -这段代码将在运行后大约2秒显示"hello"。 - ### setImmediate(callback\[, ...args\]) * callback \ 在 Auto.js 循环的当前回合结束时要调用的函数。 * ...args \ 当调用 callback 时要传入的可选参数。 @@ -49,12 +33,12 @@ callback 可能不会精确地在 delay 毫秒被调用。 Auto.js 不能保证 setImmediate()、setInterval() 和 setTimeout() 方法每次都会返回表示预定的计时器的id。 它们可用于取消定时器并防止触发。 -### clearImmediate(id)# +### clearImmediate(id) * id \ 一个 setImmediate() 返回的 id。 取消一个由 setImmediate() 创建的 Immediate 对象。 -### clearInterval(id)# +### clearInterval(id) * id \ 一个 setInterval() 返回的 id。 取消一个由 setInterval() 创建的 Timeout 对象。 diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/main/MainActivity.java b/app/src/main/java/com/stardust/scriptdroid/ui/main/MainActivity.java index 4093757c..636fc974 100644 --- a/app/src/main/java/com/stardust/scriptdroid/ui/main/MainActivity.java +++ b/app/src/main/java/com/stardust/scriptdroid/ui/main/MainActivity.java @@ -292,6 +292,12 @@ public class MainActivity extends BaseActivity implements OnActivityResultDelega mDrawerHeaderBackgroundSaver.select(this, mActivityResultMediator); } + @Override + protected void onPause() { + super.onPause(); + stopService(new Intent(this, DownloadService.class)); + } + @Override protected void onResume() { super.onResume(); @@ -477,6 +483,11 @@ public class MainActivity extends BaseActivity implements OnActivityResultDelega EventBus.getDefault().unregister(this); } + @NonNull + @Override + public OnActivityResultDelegate.Mediator getOnActivityResultDelegateMediator() { + return mActivityResultMediator; + } public static void onRecordStop(Context context, String script) { Intent intent = new Intent(context, MainActivity_.class) @@ -488,11 +499,5 @@ public class MainActivity extends BaseActivity implements OnActivityResultDelega context.startActivity(intent); } - @NonNull - @Override - public OnActivityResultDelegate.Mediator getOnActivityResultDelegateMediator() { - return mActivityResultMediator; - } - } \ No newline at end of file diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/main/SideMenuFragment.java b/app/src/main/java/com/stardust/scriptdroid/ui/main/SideMenuFragment.java index 0991db51..c98d92a8 100644 --- a/app/src/main/java/com/stardust/scriptdroid/ui/main/SideMenuFragment.java +++ b/app/src/main/java/com/stardust/scriptdroid/ui/main/SideMenuFragment.java @@ -44,7 +44,6 @@ import java.util.concurrent.Executor; @EFragment(R.layout.fragment_side_menu) public class SideMenuFragment extends android.support.v4.app.Fragment { - public static void setFragment(FragmentActivity activity, int viewId) { SideMenuFragment fragment = new SideMenuFragment_(); activity.getSupportFragmentManager().beginTransaction().replace(viewId, fragment).commit(); diff --git a/autojs/src/main/assets/javascript_engine_init.js b/autojs/src/main/assets/javascript_engine_init.js index a5aa9c7e..225175f1 100644 --- a/autojs/src/main/assets/javascript_engine_init.js +++ b/autojs/src/main/assets/javascript_engine_init.js @@ -1,4 +1,6 @@ +__runtime__.init(); + if(__engine_name__ == "rhino"){ __importClass__ = importClass; var importClass = function(pack){ diff --git a/autojs/src/main/assets/modules/__timers__.js b/autojs/src/main/assets/modules/__timers__.js index f6dd3c08..44384e5a 100644 --- a/autojs/src/main/assets/modules/__timers__.js +++ b/autojs/src/main/assets/modules/__timers__.js @@ -2,7 +2,11 @@ module.exports = function(__runtime__, scope){ var timers = Object.create(__runtime__.timers); - scope.__asGlobal__(timers, ['loop', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'setImmediate', 'clearImmediate']); + scope.__asGlobal__(timers, ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'setImmediate', 'clearImmediate']); + + scope.loop = function(){ + console.warn("loop() has been deprecated and has no effect. Remove it from your code."); + } return timers; } diff --git a/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java b/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java index 92686195..a2f6b7b9 100644 --- a/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java +++ b/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java @@ -183,10 +183,6 @@ public class ScriptEngineService { mUiHandler.toast(mContext.getString(R.string.text_no_running_script)); } - public String[] getGlobalFunctions() { - return mScriptEngineManager.getGlobalFunctions(); - } - public Set getEngines() { return mScriptEngineManager.getEngines(); } diff --git a/autojs/src/main/java/com/stardust/autojs/engine/AbstractScriptEngineManager.java b/autojs/src/main/java/com/stardust/autojs/engine/AbstractScriptEngineManager.java index 0d1f86cf..739a8770 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/AbstractScriptEngineManager.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/AbstractScriptEngineManager.java @@ -2,12 +2,6 @@ package com.stardust.autojs.engine; import android.content.Context; -import com.stardust.autojs.BuildConfig; -import com.stardust.autojs.script.ScriptSource; -import com.stardust.autojs.script.StringScriptSource; -import com.stardust.pio.PFile; - -import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -25,10 +19,10 @@ public abstract class AbstractScriptEngineManager implements ScriptEngineManager private final Set mEngines = new HashSet<>(); private EngineLifecycleCallback mEngineLifecycleCallback; - private android.content.Context mContext; + private android.content.Context mAndroidContext; - public AbstractScriptEngineManager(Context context) { - mContext = context; + public AbstractScriptEngineManager(Context androidContext) { + mAndroidContext = androidContext; } public ScriptEngine createEngine() { @@ -61,10 +55,8 @@ public abstract class AbstractScriptEngineManager implements ScriptEngineManager protected abstract ScriptEngine createEngineInner(); - public abstract String[] getGlobalFunctions(); - - public android.content.Context getContext() { - return mContext; + public android.content.Context getAndroidContext() { + return mAndroidContext; } protected void putProperties(ScriptEngine engine) { diff --git a/autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java b/autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java new file mode 100644 index 00000000..b89692b1 --- /dev/null +++ b/autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java @@ -0,0 +1,59 @@ +package com.stardust.autojs.engine; + +import android.os.Handler; +import android.os.Looper; +import android.os.MessageQueue; + +import com.stardust.autojs.runtime.api.Loopers; +import com.stardust.autojs.script.ScriptSource; + +/** + * Created by Stardust on 2017/7/28. + */ + +public class LoopBasedJavaScriptEngine extends RhinoJavaScriptEngine { + + private Handler mHandler; + private boolean mLooping = false; + + public LoopBasedJavaScriptEngine(RhinoJavaScriptEngineManager engineManager) { + super(engineManager); + } + + @Override + public Object execute(final ScriptSource source) { + Runnable r = new Runnable() { + @Override + public void run() { + LoopBasedJavaScriptEngine.super.execute(source); + } + }; + mHandler.post(r); + if (!mLooping && Looper.myLooper() != Looper.getMainLooper()) { + mLooping = true; + Looper.loop(); + } + return null; + } + + @Override + public void forceStop() { + Loopers.quitForThread(getThread()); + super.forceStop(); + } + + @Override + public synchronized void destroy() { + Loopers.quitForThread(getThread()); + super.destroy(); + } + + @Override + public void init() { + Loopers.prepare(); + mHandler = new Handler(); + super.init(); + } + + +} diff --git a/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java b/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java index b289ba7b..e6db9469 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java @@ -77,7 +77,6 @@ public class RhinoJavaScriptEngine implements ScriptEngine { public void forceStop() { Log.d(LOG_TAG, "forceStop: interrupt Thread: " + mThread); mThread.interrupt(); - Timers.quitLooperIfNeeded(mThread); } public RhinoJavaScriptEngineManager getEngineManager() { @@ -91,7 +90,6 @@ public class RhinoJavaScriptEngine implements ScriptEngine { contextCount--; Log.d(LOG_TAG, "contextCount = " + contextCount); mEngineManager.removeEngine(this); - Timers.removeThreadRecord(mThread); mDestroyed = true; } @@ -110,6 +108,10 @@ public class RhinoJavaScriptEngine implements ScriptEngine { return mTags.get(key); } + public Thread getThread() { + return mThread; + } + @Override public void init() { ScriptableObject.putProperty(mScriptable, "__engine_name__", "rhino"); @@ -128,7 +130,7 @@ public class RhinoJavaScriptEngine implements ScriptEngine { for (String path : mRequirePath) { list.add(new File(path).toURI()); } - AssetAndUrlModuleSourceProvider provider = new AssetAndUrlModuleSourceProvider(getEngineManager().getContext(), list); + AssetAndUrlModuleSourceProvider provider = new AssetAndUrlModuleSourceProvider(getEngineManager().getAndroidContext(), list); new RequireBuilder() .setModuleScriptProvider(new SoftCachingModuleScriptProvider(provider)) .setSandboxed(false) @@ -152,9 +154,9 @@ public class RhinoJavaScriptEngine implements ScriptEngine { protected Context createContext() { if (!ContextFactory.hasExplicitGlobal()) { - ContextFactory.initGlobal(new InterruptibleAndroidContextFactory(new File(mEngineManager.getContext().getCacheDir(), "classes"))); + ContextFactory.initGlobal(new InterruptibleAndroidContextFactory(new File(mEngineManager.getAndroidContext().getCacheDir(), "classes"))); } - Context context = new RhinoAndroidHelper(mEngineManager.getContext()).enterContext(); + Context context = new RhinoAndroidHelper(mEngineManager.getAndroidContext()).enterContext(); contextCount++; context.setOptimizationLevel(-1); context.setLanguageVersion(Context.VERSION_ES6); diff --git a/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngineManager.java b/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngineManager.java index 85dd4068..6df23686 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngineManager.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngineManager.java @@ -7,22 +7,7 @@ import com.stardust.autojs.script.SequenceScriptSource; import com.stardust.autojs.script.StringScriptSource; import com.stardust.pio.PFile; -import org.mozilla.javascript.Context; -import org.mozilla.javascript.Scriptable; -import org.mozilla.javascript.commonjs.module.RequireBuilder; -import org.mozilla.javascript.commonjs.module.provider.ModuleSource; -import org.mozilla.javascript.commonjs.module.provider.SoftCachingModuleScriptProvider; -import org.mozilla.javascript.commonjs.module.provider.UrlModuleSourceProvider; - -import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; /** * Created by Stardust on 2017/3/1. @@ -40,7 +25,7 @@ public class RhinoJavaScriptEngineManager extends AbstractScriptEngineManager { } protected RhinoJavaScriptEngine createEngineInner() { - RhinoJavaScriptEngine engine = new RhinoJavaScriptEngine(this); + RhinoJavaScriptEngine engine = new LoopBasedJavaScriptEngine(this); return engine; } @@ -58,7 +43,7 @@ public class RhinoJavaScriptEngineManager extends AbstractScriptEngineManager { private String readInitScript() { try { - return PFile.read(getContext().getAssets().open("javascript_engine_init.js")); + return PFile.read(getAndroidContext().getAssets().open("javascript_engine_init.js")); } catch (IOException e) { throw new RuntimeException(e); } @@ -76,28 +61,5 @@ public class RhinoJavaScriptEngineManager extends AbstractScriptEngineManager { return mInitScript; } - @Override - public String[] getGlobalFunctions() { - if (mFunctions == null) - mFunctions = getGlobalFunctionsInner(); - return mFunctions; - } - - private String[] getGlobalFunctionsInner() { - ScriptEngine engine = createEngine(); - ScriptSource source = new StringScriptSource("this", "this"); - engine.setTag("script", source); - engine.init(); - Scriptable scriptable = (Scriptable) engine.execute(source); - Object[] ids = scriptable.getIds(); - String[] functions = new String[ids.length]; - for (int i = 0; i < ids.length; i++) { - functions[i] = ids[i].toString(); - } - engine.destroy(); - return functions; - } - - } diff --git a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java index 23b5d18c..913cd5b2 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java @@ -26,8 +26,6 @@ public interface ScriptEngineManager { Set getEngines(); - String[] getGlobalFunctions(); - int stopAll(); } diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/AbstractScriptRuntime.java b/autojs/src/main/java/com/stardust/autojs/runtime/AbstractScriptRuntime.java index 803991dd..b2fc84ab 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/AbstractScriptRuntime.java +++ b/autojs/src/main/java/com/stardust/autojs/runtime/AbstractScriptRuntime.java @@ -2,11 +2,13 @@ package com.stardust.autojs.runtime; import android.content.Context; import android.os.Build; +import android.os.Looper; import android.support.annotation.CallSuper; import com.stardust.autojs.engine.ScriptEngine; import com.stardust.autojs.runtime.api.AbstractShell; import com.stardust.autojs.runtime.api.AppUtils; +import com.stardust.autojs.runtime.api.Loopers; import com.stardust.autojs.runtime.api.ScriptBridges; import com.stardust.autojs.runtime.api.Console; import com.stardust.autojs.runtime.api.Events; @@ -21,8 +23,6 @@ import com.stardust.util.ScreenMetrics; import com.stardust.util.UiHandler; import com.stardust.view.accessibility.AccessibilityInfoProvider; -import org.mozilla.javascript.annotations.JSFunction; - import java.lang.ref.WeakReference; /** @@ -56,15 +56,22 @@ public abstract class AbstractScriptRuntime { public ScriptBridges bridges = new ScriptBridges(); @ScriptVariable - public Timers timers = new Timers(bridges); + public Loopers loopers; + + @ScriptVariable + public Timers timers; private Images images; + private UiHandler mUiHandler; + private AccessibilityBridge mAccessibilityBridge; private static WeakReference applicationContext; public AbstractScriptRuntime(UiHandler uiHandler, Console console, AccessibilityBridge bridge, AppUtils appUtils, ScreenCaptureRequester screenCaptureRequester) { this.app = appUtils; this.console = console; + mAccessibilityBridge = bridge; + mUiHandler = uiHandler; this.automator = new SimpleActionAutomator(bridge, this); this.info = bridge.getInfoProvider(); Context context = uiHandler.getContext(); @@ -73,7 +80,18 @@ public abstract class AbstractScriptRuntime { images = new Images(context, this, screenCaptureRequester); } dialogs = new Dialogs(app, uiHandler); - events = new Events(context, bridge, bridges, timers); + } + + /** + * Call in init.js + */ + @CallSuper + public void init() { + if (loopers != null) + throw new IllegalStateException("already initialized"); + loopers = new Loopers(); + events = new Events(mUiHandler.getContext(), mAccessibilityBridge, bridges, loopers); + timers = new Timers(bridges); } public static void setApplicationContext(Context context) { @@ -130,10 +148,15 @@ public abstract class AbstractScriptRuntime { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { images.releaseScreenCapturer(); } - events.recycle(); + if (events != null) + events.recycle(); + if (loopers != null) + loopers.quitAll(); } public Object getImages() { return images; } + + } diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/Events.java b/autojs/src/main/java/com/stardust/autojs/runtime/api/Events.java index 65b5170c..36c533f7 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/api/Events.java +++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/Events.java @@ -27,27 +27,29 @@ public class Events extends EventEmitter implements OnKeyListener, TouchObserver private AccessibilityBridge mAccessibilityBridge; private Context mContext; private TouchObserver mTouchObserver; - private Timers mTimers; private long mLastTouchEventMillis; private long mTouchEventTimeout = 10; private boolean mListeningKey = false; + private Loopers mLoopers; + private Handler mHandler; - public Events(Context context, AccessibilityBridge accessibilityBridge, ScriptBridges bridges, Timers timers) { + public Events(Context context, AccessibilityBridge accessibilityBridge, ScriptBridges bridges, Loopers loopers) { super(bridges); mAccessibilityBridge = accessibilityBridge; mContext = context; - mTimers = timers; + mLoopers = loopers; } - public EventEmitter emitter(){ + public EventEmitter emitter() { return new EventEmitter(mBridges); } public void observeKey() { if (mListeningKey) return; + ensureHandler(); + mLoopers.waitWhenIdle(true); mListeningKey = true; - mTimers.prepareLoopIfNeeded(); mAccessibilityBridge.ensureServiceEnabled(); AccessibilityService service = mAccessibilityBridge.getService(); if (service == null) @@ -55,10 +57,17 @@ public class Events extends EventEmitter implements OnKeyListener, TouchObserver service.getOnKeyObserver().addListener(this); } + private void ensureHandler() { + if(mHandler == null){ + mHandler = new Handler(); + } + } + public void observeTouch() { - mTimers.prepareLoopIfNeeded(); if (mTouchObserver != null) return; + ensureHandler(); + mLoopers.waitWhenIdle(true); mTouchObserver = new TouchObserver(mContext); mTouchObserver.setOnTouchEventListener(this); mTouchObserver.observe(); @@ -128,7 +137,7 @@ public class Events extends EventEmitter implements OnKeyListener, TouchObserver @Override public void onKeyEvent(final int keyCode, final KeyEvent event) { - mTimers.post(new Runnable() { + mHandler.post(new Runnable() { @Override public void run() { String keyName = KeyEvent.keyCodeToString(keyCode).substring(8).toLowerCase(); @@ -151,7 +160,7 @@ public class Events extends EventEmitter implements OnKeyListener, TouchObserver return; } mLastTouchEventMillis = System.currentTimeMillis(); - mTimers.post(new Runnable() { + mHandler.post(new Runnable() { @Override public void run() { emit("touch", new Point(x, y)); diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/Loopers.java b/autojs/src/main/java/com/stardust/autojs/runtime/api/Loopers.java new file mode 100644 index 00000000..e0676af4 --- /dev/null +++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/Loopers.java @@ -0,0 +1,99 @@ +package com.stardust.autojs.runtime.api; + +import android.graphics.Paint; +import android.os.Handler; +import android.os.Looper; +import android.os.MessageQueue; + +import com.stardust.autojs.runtime.ScriptInterruptedException; +import com.stardust.lang.ThreadCompat; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by Stardust on 2017/7/29. + */ + +public class Loopers { + + private volatile Looper mServantLooper; + private static volatile ConcurrentHashMap sLoopers = new ConcurrentHashMap<>(); + + public volatile boolean waitWhenIdle = false; + + public Loopers() { + if (Looper.myLooper() == Looper.getMainLooper()) { + waitWhenIdle = true; + } + Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { + @Override + public boolean queueIdle() { + Looper l = Looper.myLooper(); + if (l != null && !waitWhenIdle) + l.quit(); + return true; + } + }); + } + + + private void initServantThread() { + new ThreadCompat(new Runnable() { + @Override + public void run() { + Looper.prepare(); + final Object lock = Loopers.this; + 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; + } + + public void quitServantLooper() { + if (mServantLooper == null) + return; + mServantLooper.quit(); + } + + public void waitWhenIdle(boolean b) { + waitWhenIdle = b; + } + + public void quitAll() { + quitServantLooper(); + } + + public static void prepare() { + if(Looper.myLooper() == Looper.getMainLooper()) + return; + if (Looper.myLooper() == null) + Looper.prepare(); + Looper l = Looper.myLooper(); + if (l != null) + sLoopers.put(Thread.currentThread(), l); + } + + public static void quitForThread(Thread thread) { + Looper looper = sLoopers.remove(thread); + if (looper != null) + looper.quit(); + } +} diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/Timers.java b/autojs/src/main/java/com/stardust/autojs/runtime/api/Timers.java index 3746008e..1205d1ea 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/api/Timers.java +++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/Timers.java @@ -12,18 +12,23 @@ import java.util.concurrent.ConcurrentHashMap; public class Timers { - private static ConcurrentHashMap sLoopers = new ConcurrentHashMap<>(); - private Handler mHandler; private SparseArray mHandlerCallbacks = new SparseArray<>(); private int mCallbackMaxId = 0; private ScriptBridges mBridges; + private Handler mHandler; public Timers(ScriptBridges bridges) { mBridges = bridges; } + private void ensureHander(){ + if(mHandler == null){ + mHandler = new Handler(); + } + } + public int setTimeout(final Object callback, long delay, final Object... args) { - prepareLoopIfNeeded(); + ensureHander(); mCallbackMaxId++; final int id = mCallbackMaxId; Runnable r = new Runnable() { @@ -39,6 +44,7 @@ public class Timers { } public void post(Runnable r) { + ensureHander(); mHandler.post(r); } @@ -47,7 +53,7 @@ public class Timers { } public int setInterval(final Object listener, final long interval, final Object... args) { - prepareLoopIfNeeded(); + ensureHander(); mCallbackMaxId++; final int id = mCallbackMaxId; Runnable r = new Runnable() { @@ -67,7 +73,7 @@ public class Timers { } public int setImmediate(final Object listener, final Object... args) { - prepareLoopIfNeeded(); + ensureHander(); mCallbackMaxId++; final int id = mCallbackMaxId; Runnable r = new Runnable() { @@ -94,32 +100,5 @@ public class Timers { } } - public void prepareLoopIfNeeded() { - if (Looper.myLooper() != null) - return; - Looper.prepare(); - Looper looper = Looper.myLooper(); - if (looper != null) { - // null check is not necessary, just to make Android Studio happy - sLoopers.put(Thread.currentThread(), looper); - } - mHandler = new Handler(); - } - - public void loop() { - Looper.loop(); - } - - public static void removeThreadRecord(Thread thread) { - sLoopers.remove(thread); - } - - public static void quitLooperIfNeeded(Thread thread) { - Looper looper = sLoopers.get(thread); - if (looper != null) { - looper.quit(); - } - } - } diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/image/Images.java b/autojs/src/main/java/com/stardust/autojs/runtime/api/image/Images.java index fc6616d4..42bb480b 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/api/image/Images.java +++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/image/Images.java @@ -10,12 +10,14 @@ import android.graphics.Matrix; import android.graphics.Point; import android.media.Image; import android.os.Build; +import android.os.Handler; import android.support.annotation.RequiresApi; import android.util.Log; import com.stardust.autojs.runtime.AbstractScriptRuntime; import com.stardust.autojs.runtime.ScriptInterruptedException; import com.stardust.autojs.runtime.ScriptVariable; +import com.stardust.autojs.runtime.api.Loopers; import com.stardust.concurrent.VolatileBox; import com.stardust.pio.UncheckedIOException; import com.stardust.util.ScreenMetrics; @@ -45,6 +47,7 @@ public class Images { mContext = context; } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public boolean requestScreenCapture(final int width, final int height) { mScriptRuntime.requiresApi(21); colorFinder.prestartThreads(); @@ -54,7 +57,8 @@ public class Images { @Override public void onRequestResult(int result, Intent data) { if (result == Activity.RESULT_OK) { - mScreenCapturer = new ScreenCapturer(mContext, data, width, height); + mScreenCapturer = new ScreenCapturer(mContext, data, width, height, ScreenMetrics.getDeviceScreenDensity(), + new Handler(mScriptRuntime.loopers.getServantLooper())); requestResult.setAndNotify(true); } else { requestResult.setAndNotify(false); @@ -65,6 +69,7 @@ public class Images { return requestResult.blockedGetOrThrow(ScriptInterruptedException.class); } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public boolean requestScreenCapture() { return requestScreenCapture(ScreenMetrics.getDeviceScreenWidth(), ScreenMetrics.getDeviceScreenHeight()); } @@ -72,7 +77,7 @@ public class Images { @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public Image captureScreen() { mScriptRuntime.requiresApi(21); - if(mScreenCapturer == null){ + if (mScreenCapturer == null) { throw new SecurityException("No screen capture permission"); } colorFinder.prestartThreads(); diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/image/ScreenCapturer.java b/autojs/src/main/java/com/stardust/autojs/runtime/api/image/ScreenCapturer.java index 7c1bc04d..08ce08c8 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/api/image/ScreenCapturer.java +++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/image/ScreenCapturer.java @@ -3,6 +3,7 @@ package com.stardust.autojs.runtime.api.image; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.graphics.Paint; import android.graphics.PixelFormat; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; @@ -11,17 +12,15 @@ import android.media.ImageReader; import android.media.projection.MediaProjection; import android.media.projection.MediaProjectionManager; import android.os.Build; +import android.os.Handler; import android.os.Looper; +import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.util.Log; -import android.widget.ImageView; import com.stardust.autojs.runtime.ScriptInterruptedException; import com.stardust.util.ScreenMetrics; -import java.util.HashSet; -import java.util.concurrent.ConcurrentHashMap; - /** * Created by Stardust on 2017/5/17. */ @@ -36,15 +35,25 @@ public class ScreenCapturer { private final Object mImageWaitingLock = new Object(); private volatile Image mImage; private volatile Image mLatestImage; + private final int mScreenWidth; + private final int mScreenHeight; + private final int mScreenDensity; + private Handler mHandler; + private volatile boolean mImageAvailable = false; - public ScreenCapturer(Context context, Intent data, int screenWidth, int screenHeight, int screenDensity) { + public ScreenCapturer(Context context, Intent data, int screenWidth, int screenHeight, int screenDensity, Handler handler) { + mScreenWidth = screenWidth; + mScreenHeight = screenHeight; + mScreenDensity = screenDensity; + mHandler = handler; MediaProjectionManager manager = (MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE); initVirtualDisplay(manager, data, screenWidth, screenHeight, screenDensity); + mHandler = handler; startAcquireImageLoop(); } public ScreenCapturer(Context context, Intent data, int screenWidth, int screenHeight) { - this(context, data, screenWidth, screenHeight, ScreenMetrics.getDeviceScreenDensity()); + this(context, data, screenWidth, screenHeight, ScreenMetrics.getDeviceScreenDensity(), null); } public ScreenCapturer(Context context, Intent data) { @@ -57,47 +66,61 @@ public class ScreenCapturer { mVirtualDisplay = mMediaProjection.createVirtualDisplay("screen-mirror", screenWidth, screenHeight, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null); - } private void startAcquireImageLoop() { + if (mHandler != null) { + setImageListener(mHandler); + return; + } new Thread(new Runnable() { @Override public void run() { Looper.prepare(); mImageAcquireLooper = Looper.myLooper(); - mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { - @Override - public void onImageAvailable(ImageReader reader) { - if (mLatestImage != null) { - mLatestImage.close(); - } - mLatestImage = reader.acquireNextImage(); - if (mLatestImage != null) { - synchronized (mImageWaitingLock) { - mImageWaitingLock.notify(); - } - } - } - }, null); + setImageListener(new Handler()); Looper.loop(); } }).start(); } - public Image capture() { - if (mLatestImage == null) { - if (mImage != null) { - return mImage; + private void setImageListener(Handler handler) { + mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + if (mImageAvailable) { + return; + } + mLatestImage = reader.acquireLatestImage(); + if (mLatestImage != null) { + mImageAvailable = true; + synchronized (mImageWaitingLock) { + mImageWaitingLock.notify(); + } + } } + }, handler); + } + + @Nullable + public Image capture() { + if (!mImageAvailable) { waitForImageAvailable(); + return mLatestImage; } - if (mImage != null) { - mImage.close(); + if (mLatestImage != null) { + tryClose(mLatestImage); + } + mLatestImage = mImageReader.acquireLatestImage(); + return mLatestImage; + } + + private void tryClose(Image image) { + try { + image.close(); + } catch (Exception ignored) { + } - mImage = mLatestImage; - mLatestImage = null; - return mImage; } private void waitForImageAvailable() { @@ -111,6 +134,17 @@ public class ScreenCapturer { } } + public int getScreenWidth() { + return mScreenWidth; + } + + public int getScreenHeight() { + return mScreenHeight; + } + + public int getScreenDensity() { + return mScreenDensity; + } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public void release() { diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/simpleaction/SimpleActionAutomator.java b/autojs/src/main/java/com/stardust/autojs/runtime/simpleaction/SimpleActionAutomator.java index f9c698ca..0497c7a7 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/simpleaction/SimpleActionAutomator.java +++ b/autojs/src/main/java/com/stardust/autojs/runtime/simpleaction/SimpleActionAutomator.java @@ -4,6 +4,7 @@ import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.GestureDescription; import android.graphics.Rect; import android.os.Build; +import android.os.Handler; import android.support.annotation.RequiresApi; import android.util.Log; import android.view.accessibility.AccessibilityNodeInfo; @@ -12,6 +13,7 @@ import com.stardust.autojs.runtime.AbstractScriptRuntime; import com.stardust.autojs.runtime.AccessibilityBridge; import com.stardust.autojs.runtime.ScriptInterface; import com.stardust.autojs.runtime.api.AutomatorConfig; +import com.stardust.autojs.runtime.api.Loopers; import com.stardust.automator.GlobalActionAutomator; import com.stardust.automator.UiObject; import com.stardust.automator.simple_action.ActionFactory; @@ -30,7 +32,8 @@ public class SimpleActionAutomator { private AccessibilityBridge mAccessibilityBridge; private AbstractScriptRuntime mScriptRuntime; - private GlobalActionAutomator mGlobalActionAutomator = new GlobalActionAutomator(); + private GlobalActionAutomator mGlobalActionAutomator; + private ScreenMetrics mScreenMetrics; public SimpleActionAutomator(AccessibilityBridge accessibilityBridge, AbstractScriptRuntime scriptRuntime) { mAccessibilityBridge = accessibilityBridge; @@ -190,6 +193,10 @@ public class SimpleActionAutomator { private void prepareForGesture() { ensureAccessibilityServiceEnabled(); mScriptRuntime.requiresApi(24); + if (mGlobalActionAutomator != null) + return; + mGlobalActionAutomator = new GlobalActionAutomator(new Handler(mScriptRuntime.loopers.getServantLooper())); + mGlobalActionAutomator.setScreenMetrics(mScreenMetrics); mGlobalActionAutomator.setService(mAccessibilityBridge.getService()); } @@ -229,8 +236,10 @@ public class SimpleActionAutomator { return service.performGlobalAction(action); } + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) @ScriptInterface public boolean paste(ActionTarget target) { + mScriptRuntime.requiresApi(18); return performAction(target.createAction(AccessibilityNodeInfo.ACTION_PASTE)); } @@ -260,7 +269,9 @@ public class SimpleActionAutomator { } public void setScreenMetrics(ScreenMetrics metrics) { - mGlobalActionAutomator.setScreenMetrics(metrics); + mScreenMetrics = metrics; + if (mGlobalActionAutomator != null) + mGlobalActionAutomator.setScreenMetrics(metrics); } } diff --git a/automator/src/main/java/com/stardust/automator/GlobalActionAutomator.java b/automator/src/main/java/com/stardust/automator/GlobalActionAutomator.java index 213070c6..7a96a76b 100644 --- a/automator/src/main/java/com/stardust/automator/GlobalActionAutomator.java +++ b/automator/src/main/java/com/stardust/automator/GlobalActionAutomator.java @@ -6,6 +6,7 @@ import android.graphics.Path; import android.os.Build; import android.os.Handler; import android.os.Looper; +import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.view.ViewConfiguration; @@ -20,6 +21,11 @@ public class GlobalActionAutomator { private AccessibilityService mService; private ScreenMetrics mScreenMetrics; + private Handler mHandler; + + public GlobalActionAutomator(@Nullable Handler handler) { + mHandler = handler; + } public void setService(AccessibilityService service) { mService = service; @@ -95,10 +101,36 @@ public class GlobalActionAutomator { for (GestureDescription.StrokeDescription stroke : strokes) { builder.addStroke(stroke); } + if (mHandler == null) { + return gesturesWithoutHandler(builder.build()); + } else { + return gesturesWithHandler(builder.build()); + } + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private boolean gesturesWithHandler(GestureDescription description) { + final VolatileBox result = new VolatileBox<>(false); + mService.dispatchGesture(description, new AccessibilityService.GestureResultCallback() { + @Override + public void onCompleted(GestureDescription gestureDescription) { + result.setAndNotify(true); + } + + @Override + public void onCancelled(GestureDescription gestureDescription) { + result.setAndNotify(false); + } + }, mHandler); + return result.blockedGet(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private boolean gesturesWithoutHandler(GestureDescription description) { prepareLooperIfNeeded(); final VolatileBox result = new VolatileBox<>(false); Handler handler = new Handler(Looper.myLooper()); - mService.dispatchGesture(builder.build(), new AccessibilityService.GestureResultCallback() { + mService.dispatchGesture(description, new AccessibilityService.GestureResultCallback() { @Override public void onCompleted(GestureDescription gestureDescription) { result.set(true); @@ -129,7 +161,7 @@ public class GlobalActionAutomator { private void quitLoop() { Looper looper = Looper.myLooper(); if (looper != null) { - looper.quitSafely(); + looper.quit(); } }