add events module, including EventEmiiter, KeyEvent, TouchEvent

This commit is contained in:
hyb1996
2017-07-20 15:39:17 +08:00
parent 1d23640362
commit 52cc5214f1
47 changed files with 755 additions and 82 deletions

View File

@@ -1,10 +1,12 @@
package com.stardust.autojs.engine;
import android.os.Looper;
import android.util.Log;
import com.stardust.autojs.rhino.AndroidContextFactory;
import com.stardust.autojs.rhino.RhinoAndroidHelper;
import com.stardust.autojs.runtime.ScriptInterruptedException;
import com.stardust.autojs.runtime.api.Events;
import com.stardust.autojs.script.ScriptSource;
import com.stardust.pio.UncheckedIOException;
@@ -41,6 +43,7 @@ public class RhinoJavaScriptEngine implements ScriptEngine {
private Thread mThread;
private RhinoJavaScriptEngineManager mEngineManager;
private Map<String, Object> mTags = new ConcurrentHashMap<>();
private volatile boolean mDestroyed = false;
public RhinoJavaScriptEngine(RhinoJavaScriptEngineManager engineManager) {
mEngineManager = engineManager;
@@ -73,6 +76,7 @@ public class RhinoJavaScriptEngine implements ScriptEngine {
public void forceStop() {
Log.d(LOG_TAG, "forceStop: interrupt Thread: " + mThread);
mThread.interrupt();
Events.quitLooperIfNeeded(mThread);
}
public RhinoJavaScriptEngineManager getEngineManager() {
@@ -86,6 +90,13 @@ public class RhinoJavaScriptEngine implements ScriptEngine {
contextCount--;
Log.d(LOG_TAG, "contextCount = " + contextCount);
mEngineManager.removeEngine(this);
Events.removeThreadRecord(mThread);
mDestroyed = true;
}
@Override
public boolean isDestroyed() {
return mDestroyed;
}
@Override

View File

@@ -25,6 +25,8 @@ public interface ScriptEngine {
void destroy();
boolean isDestroyed();
void setTag(String key, Object value);
Object getTag(String key);

View File

@@ -8,6 +8,7 @@ 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.Console;
import com.stardust.autojs.runtime.api.Events;
import com.stardust.autojs.runtime.api.UiSelector;
import com.stardust.autojs.runtime.api.image.Images;
import com.stardust.autojs.runtime.api.image.ScreenCaptureRequester;
@@ -41,10 +42,12 @@ public abstract class AbstractScriptRuntime {
@ScriptVariable
public UI ui;
@ScriptVariable
public Dialogs dialogs;
@ScriptVariable
public Events events;
private Images images;
private static WeakReference<Context> applicationContext;
@@ -54,11 +57,13 @@ public abstract class AbstractScriptRuntime {
this.console = console;
this.automator = new SimpleActionAutomator(bridge, this);
this.info = bridge.getInfoProvider();
this.ui = new UI(uiHandler.getContext());
Context context = uiHandler.getContext();
this.ui = new UI(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
images = new Images(uiHandler.getContext(), this, screenCaptureRequester);
images = new Images(context, this, screenCaptureRequester);
}
dialogs = new Dialogs(app, uiHandler);
events = new Events(context, bridge);
}
public static void setApplicationContext(Context context) {
@@ -115,6 +120,7 @@ public abstract class AbstractScriptRuntime {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
images.releaseScreenCapturer();
}
events.recycle();
}
public Object getImages() {

View File

@@ -1,11 +1,11 @@
package com.stardust.autojs.runtime;
import android.accessibilityservice.AccessibilityService;
import android.support.annotation.Nullable;
import com.stardust.automator.AccessibilityEventCommandHost;
import com.stardust.automator.simple_action.SimpleActionPerformHost;
import com.stardust.view.accessibility.AccessibilityInfoProvider;
import com.stardust.view.accessibility.AccessibilityService;
/**
* Created by Stardust on 2017/4/2.

View File

@@ -1,6 +1,7 @@
package com.stardust.autojs.runtime;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import com.stardust.autojs.R;
@@ -85,7 +86,6 @@ public class ScriptRuntime extends AbstractScriptRuntime {
private UiHandler mUiHandler;
private AccessibilityBridge mAccessibilityBridge;
private AbstractShell mRootShell;
private Supplier<AbstractShell> mShellSupplier;
private ScreenMetrics mScreenMetrics = new ScreenMetrics();

View File

@@ -0,0 +1,195 @@
package com.stardust.autojs.runtime.api;
import android.support.annotation.NonNull;
import com.stardust.autojs.runtime.ScriptStopException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TooManyListenersException;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Created by Stardust on 2017/7/19.
*/
public class EventEmitter {
public interface FunctionCaller {
Object call(Object func, Object[] arg);
}
private static class ListenerWrapper {
Object listener;
boolean isOnce;
public ListenerWrapper(Object listener, boolean isOnce) {
this.listener = listener;
this.isOnce = isOnce;
}
}
private class Listeners {
private CopyOnWriteArrayList<ListenerWrapper> mListenerWrappers = new CopyOnWriteArrayList<>();
void add(Object listener, boolean once) {
ensureListenersNotAtLimit();
mListenerWrappers.add(new ListenerWrapper(listener, once));
}
private void ensureListenersNotAtLimit() {
if (mMaxListeners != 0 && mListenersMap.size() >= mMaxListeners) {
throw new ScriptStopException(new TooManyListenersException("max = " + mMaxListeners));
}
}
boolean empty() {
return mListenerWrappers.isEmpty();
}
void emit(Object[] args) {
Iterator<ListenerWrapper> listenerIterator = mListenerWrappers.iterator();
while (listenerIterator.hasNext()) {
ListenerWrapper listenerWrapper = listenerIterator.next();
onEvent(listenerWrapper, args);
if (listenerWrapper.isOnce) {
listenerIterator.remove();
}
}
}
private void onEvent(ListenerWrapper listenerWrapper, Object[] args) {
if (mFunctionCaller != null) {
mFunctionCaller.call(listenerWrapper.listener, args);
}
}
int count() {
return mListenerWrappers.size();
}
Object[] toArray() {
Iterator<ListenerWrapper> listenerIterator = mListenerWrappers.iterator();
ArrayList<Object> listeners = new ArrayList<>(mListenerWrappers.size());
while (listenerIterator.hasNext()) {
listeners.add(listenerIterator.next().listener);
}
return listeners.toArray(new Object[listeners.size()]);
}
void prepend(Object listener, boolean once) {
ensureListenersNotAtLimit();
mListenerWrappers.add(0, new ListenerWrapper(listener, once));
}
void remove(Object listener) {
Iterator<ListenerWrapper> listenerIterator = mListenerWrappers.iterator();
while (listenerIterator.hasNext()) {
ListenerWrapper l = listenerIterator.next();
if (l.listener == listener) {
listenerIterator.remove();
break;
}
}
}
}
private Map<String, Listeners> mListenersMap = new HashMap<>();
private int mMaxListeners = defaultMaxListeners();
private FunctionCaller mFunctionCaller;
public EventEmitter once(String eventName, Object listener) {
getListeners(eventName).add(listener, true);
return this;
}
@NonNull
private Listeners getListeners(String eventName) {
Listeners listeners = mListenersMap.get(eventName);
if (listeners == null) {
listeners = new Listeners();
mListenersMap.put(eventName, listeners);
}
return listeners;
}
public EventEmitter on(String eventName, Object listener) {
getListeners(eventName).add(listener, false);
return this;
}
public EventEmitter addListener(String eventName, Object listener) {
return on(eventName, listener);
}
public boolean emit(String eventName, Object... args) {
Listeners listeners = mListenersMap.get(eventName);
if (listeners == null || listeners.empty())
return false;
listeners.emit(args);
return true;
}
public String[] eventNames() {
return mListenersMap.keySet().toArray(new String[mListenersMap.size()]);
}
public int listenerCount(String eventName) {
Listeners listeners = mListenersMap.get(eventName);
if (listeners == null)
return 0;
return listeners.count();
}
public Object[] listeners(String eventName) {
return getListeners(eventName).toArray();
}
public EventEmitter prependListener(String eventName, Object listener) {
getListeners(eventName).prepend(listener, false);
return this;
}
public EventEmitter prependOnceListener(String eventName, Object listener) {
getListeners(eventName).prepend(listener, true);
return this;
}
public EventEmitter removeAllListeners() {
mListenersMap.clear();
return this;
}
public EventEmitter removeAllListeners(String eventName) {
mListenersMap.remove(eventName);
return this;
}
public EventEmitter removeListener(String eventName, Object listener) {
Listeners listeners = mListenersMap.get(eventName);
if (listeners != null)
listeners.remove(listener);
return this;
}
public EventEmitter setMaxListeners(int n) {
mMaxListeners = n;
return this;
}
public int getMaxListeners() {
return mMaxListeners;
}
public static int defaultMaxListeners() {
return 0;
}
public void setFunctionCaller(FunctionCaller functionCaller) {
mFunctionCaller = functionCaller;
}
}

View File

@@ -0,0 +1,174 @@
package com.stardust.autojs.runtime.api;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.text.method.KeyListener;
import android.view.KeyEvent;
import com.stardust.autojs.runtime.AccessibilityBridge;
import com.stardust.autojs.runtime.ScriptStopException;
import com.stardust.autojs.runtime.record.inputevent.InputEventRecorder;
import com.stardust.autojs.runtime.record.inputevent.TouchObserver;
import com.stardust.view.accessibility.AccessibilityService;
import com.stardust.view.accessibility.OnKeyListener;
import java.util.concurrent.ConcurrentHashMap;
/**
* Created by Stardust on 2017/7/18.
*/
public class Events extends EventEmitter implements OnKeyListener, TouchObserver.OnTouchEventListener {
private static final String PREFIX_KEY_DOWN = "__key_down__#";
private static final String PREFIX_KEY_UP = "__key_up__#";
private static ConcurrentHashMap<Thread, Looper> sLoopers = new ConcurrentHashMap<>();
private AccessibilityBridge mAccessibilityBridge;
private boolean mListeningKey = false;
private Handler mHandler;
private Context mContext;
private TouchObserver mTouchObserver;
private long mLastTouchEventMillis;
private long mTouchEventTimeout = 10;
public Events(Context context, AccessibilityBridge accessibilityBridge) {
mAccessibilityBridge = accessibilityBridge;
mContext = context;
}
public void observeKey() {
ensureListeningKey();
}
public void observeTouch() {
if (mTouchObserver != null)
return;
mTouchObserver = new TouchObserver(mContext);
mTouchObserver.setOnTouchEventListener(this);
mTouchObserver.observe();
}
private void ensureListeningKey() {
if (mListeningKey) {
return;
}
mListeningKey = true;
mAccessibilityBridge.ensureServiceEnabled();
AccessibilityService service = mAccessibilityBridge.getService();
if (service == null)
throw new ScriptStopException();
service.getOnKeyObserver().addListener(this);
Looper.prepare();
sLoopers.put(Thread.currentThread(), Looper.myLooper());
mHandler = new Handler();
}
public void loop() {
Looper.loop();
}
public Events onKeyDown(String keyName, Object listener) {
on(PREFIX_KEY_DOWN + keyName, listener);
return this;
}
public Events onceKeyDown(String keyName, Object listener) {
once(PREFIX_KEY_DOWN + keyName, listener);
return this;
}
public Events removeAllKeyDownListeners(String keyName) {
removeAllListeners(PREFIX_KEY_DOWN + keyName);
return this;
}
public Events onKeyUp(String keyName, Object listener) {
on(PREFIX_KEY_UP + keyName, listener);
return this;
}
public Events onceKeyUp(String keyName, Object listener) {
once(PREFIX_KEY_UP + keyName, listener);
return this;
}
public Events removeAllKeyUpListeners(String keyName) {
removeAllListeners(PREFIX_KEY_UP + keyName);
return this;
}
public Events onTouch(Object listener) {
on("touch", listener);
return this;
}
public Events removeAllTouchListeners() {
removeAllListeners("touch");
return this;
}
public long getTouchEventTimeout() {
return mTouchEventTimeout;
}
public void setTouchEventTimeout(long touchEventTimeout) {
mTouchEventTimeout = touchEventTimeout;
}
public void recycle() {
if (mListeningKey) {
AccessibilityService service = mAccessibilityBridge.getService();
if (service != null) {
service.getOnKeyObserver().removeListener(this);
mListeningKey = false;
}
}
if (mTouchObserver != null) {
mTouchObserver.stop();
}
}
@Override
public void onKeyEvent(final int keyCode, final KeyEvent event) {
mHandler.post(new Runnable() {
@Override
public void run() {
String keyName = KeyEvent.keyCodeToString(keyCode).substring(8).toLowerCase();
emit(keyName, event);
if (event.getAction() == KeyEvent.ACTION_DOWN) {
emit(PREFIX_KEY_DOWN + keyName, event);
emit("key_down", keyCode, event);
} else if (event.getAction() == KeyEvent.ACTION_UP) {
emit(PREFIX_KEY_UP + keyName, event);
emit("key_up", keyCode, event);
}
emit("key", keyCode, event);
}
});
}
@Override
public void onTouch(int x, int y) {
if (System.currentTimeMillis() - mLastTouchEventMillis < mTouchEventTimeout) {
return;
}
mLastTouchEventMillis = System.currentTimeMillis();
emit("touch", x, y);
}
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();
}
}
}

View File

@@ -117,8 +117,7 @@ public class UiSelector extends UiGlobalSelector {
@ScriptInterface
@NonNull
public UiObjectCollection untilFind() {
UiObjectCollection uiObjectCollection;
uiObjectCollection = find();
UiObjectCollection uiObjectCollection = find();
while (uiObjectCollection.empty()) {
if (Thread.currentThread().isInterrupted()) {
throw new ScriptInterruptedException();

View File

@@ -0,0 +1,50 @@
package com.stardust.autojs.runtime.internal;
/**
* Created by Stardust on 2017/7/19.
*/
public class Functions {
@SuppressWarnings("unchecked")
public static Object call(Func func, Object[] args) {
if (func instanceof Func0) {
return ((Func0) func).call();
} else if (func instanceof Func1) {
return ((Func1) func).call(args[0]);
} else if (func instanceof Func2) {
return ((Func2) func).call(args[0], args[1]);
} else if (func instanceof Func3) {
return ((Func3) func).call(args[0], args[1], args[2]);
} else if (func instanceof Func4) {
return ((Func4) func).call(args[0], args[1], args[2], args[3]);
}
throw new IllegalArgumentException("Unknown func: " + func);
}
public interface Func {
}
public interface Func0<R> extends Func {
R call();
}
public interface Func1<T1, R> extends Func {
R call(T1 t1);
}
public interface Func2<T1, T2, R> extends Func {
R call(T1 t1, T2 t2);
}
public interface Func3<T1, T2, T3, R> extends Func {
R call(T1 t1, T2 t2, T3 t3);
}
public interface Func4<T1, T2, T3, T4, R> extends Func {
R call(T1 t1, T2 t2, T3 t3, T4 t4);
}
}

View File

@@ -0,0 +1,175 @@
package com.stardust.autojs.runtime.record;
import java.util.Arrays;
/**
* Created by Stardust on 2017/3/16.
*/
public interface Recorder {
interface OnStateChangedListener {
void onStart();
void onStop();
void onPause();
void onResume();
}
class StateChangeEvent {
private int mOldState;
private int mCurrentState;
public StateChangeEvent(int oldState, int currentState) {
mOldState = oldState;
mCurrentState = currentState;
}
public int getOldState() {
return mOldState;
}
public int getCurrentState() {
return mCurrentState;
}
}
int STATE_NOT_START = 0;
int STATE_RECORDING = 1;
int STATE_PAUSED = 2;
int STATE_STOPPED = 3;
void start();
void stop();
void pause();
void resume();
String getCode();
int getState();
void setOnStateChangedListener(OnStateChangedListener onStateChangedListener);
abstract class AbstractRecorder implements Recorder {
private static final OnStateChangedListener NO_OPERATION_LISTENER = new OnStateChangedListener() {
@Override
public void onStart() {
}
@Override
public void onStop() {
}
@Override
public void onPause() {
}
@Override
public void onResume() {
}
};
private OnStateChangedListener mOnStateChangedListener = NO_OPERATION_LISTENER;
private final boolean mSync;
private int mState = STATE_NOT_START;
public AbstractRecorder(boolean syncOfState) {
mSync = syncOfState;
}
public AbstractRecorder() {
this(false);
}
public void start() {
ensureIsStateOf(STATE_NOT_START);
setState(STATE_RECORDING);
startImpl();
mOnStateChangedListener.onStart();
}
private void ensureIsStateOf(int... expectedStates) {
for (int expectedState : expectedStates) {
if (mState == expectedState)
return;
}
throw new IllegalStateException("expected=" + Arrays.toString(expectedStates) + " state=" + mState);
}
protected abstract void startImpl();
public void stop() {
ensureIsStateOf(STATE_RECORDING, STATE_PAUSED);
setState(STATE_STOPPED);
stopImpl();
mOnStateChangedListener.onStop();
}
protected abstract void stopImpl();
public void pause() {
ensureIsStateOf(STATE_RECORDING);
setState(STATE_PAUSED);
pauseImpl();
mOnStateChangedListener.onPause();
}
protected synchronized void setState(int state) {
if (mSync) {
synchronized (this) {
mState = state;
}
} else {
mState = state;
}
}
public synchronized int getState() {
if (mSync) {
synchronized (this) {
return mState;
}
} else {
return mState;
}
}
protected void pauseImpl() {
}
public void resume() {
ensureIsStateOf(STATE_PAUSED);
setState(STATE_RECORDING);
resumeImpl();
mOnStateChangedListener.onResume();
}
protected void resumeImpl() {
}
public void setOnStateChangedListener(OnStateChangedListener onStateChangedListener) {
mOnStateChangedListener = onStateChangedListener == null ? NO_OPERATION_LISTENER : onStateChangedListener;
}
}
}

View File

@@ -0,0 +1,144 @@
package com.stardust.autojs.runtime.record.accessibility;
import android.accessibilityservice.AccessibilityService;
import android.os.Build;
import android.util.SparseArray;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import com.stardust.automator.UiObject;
import com.stardust.automator.simple_action.FilterAction;
import com.stardust.util.SparseArrayEntries;
import com.stardust.view.accessibility.AccessibilityNodeInfoHelper;
import com.stardust.view.accessibility.NodeInfo;
import org.greenrobot.eventbus.EventBus;
import java.util.List;
/**
* Created by Stardust on 2017/2/14.
*/
public class AccessibilityActionConverter {
private static final SparseArray<EventToScriptConverter> CONVERTER_MAP = new SparseArrayEntries<EventToScriptConverter>()
.entry(AccessibilityEvent.TYPE_VIEW_CLICKED, new DoUtilSucceedConverter("click"))
.entry(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, new DoUtilSucceedConverter("longClick"))
.entry(AccessibilityEvent.TYPE_VIEW_SCROLLED, new DoOnceConverter("//scroll???"))
.entry(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED, new SetTextEventConverter())
.sparseArray();
static {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
CONVERTER_MAP.put(AccessibilityEvent.TYPE_VIEW_CONTEXT_CLICKED, new DoOnceConverter("contextClick"));
}
}
private StringBuilder mScript = new StringBuilder();
private boolean mFirstAction = true;
public AccessibilityActionConverter(boolean shouldIgnoreFirstAction) {
mShouldIgnoreFirstAction = shouldIgnoreFirstAction;
}
private boolean mShouldIgnoreFirstAction = false;
public void record(AccessibilityService service, AccessibilityEvent event) {
EventToScriptConverter converter = CONVERTER_MAP.get(event.getEventType());
if (converter != null) {
if (mFirstAction && mShouldIgnoreFirstAction) {
mFirstAction = false;
return;
}
converter.onAccessibilityEvent(service, event, mScript);
mScript.append("\n");
EventBus.getDefault().post(new AccessibilityActionRecorder.AccessibilityActionRecordEvent(event));
}
}
public String getScript() {
return mScript.toString();
}
public void onResume() {
mFirstAction = true;
}
interface EventToScriptConverter {
void onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event, StringBuilder sb);
}
private static abstract class BoundsEventConverter implements EventToScriptConverter {
@Override
public void onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event, StringBuilder sb) {
AccessibilityNodeInfo source = event.getSource();
if (source == null)
return;
String bounds = NodeInfo.boundsToString(AccessibilityNodeInfoHelper.getBoundsInScreen(source));
source.recycle();
onAccessibilityEvent(event, bounds, sb);
}
protected abstract void onAccessibilityEvent(AccessibilityEvent event, String bounds, StringBuilder sb);
}
private static class DoOnceConverter extends BoundsEventConverter {
private String mActionFunction;
DoOnceConverter(String actionFunction) {
mActionFunction = actionFunction;
}
@Override
protected void onAccessibilityEvent(AccessibilityEvent event, String bounds, StringBuilder sb) {
sb.append(mActionFunction).append(bounds).append(";");
}
}
private static class DoUtilSucceedConverter extends BoundsEventConverter {
private String mActionFunction;
DoUtilSucceedConverter(String actionFunction) {
mActionFunction = actionFunction;
}
@Override
protected void onAccessibilityEvent(AccessibilityEvent event, String bounds, StringBuilder sb) {
sb.append("while(!").append(mActionFunction).append(bounds).append(");");
}
}
private static class SetTextEventConverter implements EventToScriptConverter {
@Override
public void onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event, StringBuilder sb) {
AccessibilityNodeInfo source = event.getSource();
if (source == null)
return;
UiObject uiObject = UiObject.createRoot(service.getRootInActiveWindow());
List<UiObject> editableList = FilterAction.EditableFilter.findEditable(uiObject);
int i = findInEditableList(editableList, source);
sb.append("while(!input(").append(i).append(", \"").append(source.getText()).append("\"));");
source.recycle();
}
private static int findInEditableList(List<UiObject> editableList, AccessibilityNodeInfo editable) {
int i = 0;
for (UiObject nodeInfo : editableList) {
if (AccessibilityNodeInfoHelper.getBoundsInScreen(nodeInfo).equals(AccessibilityNodeInfoHelper.getBoundsInScreen(editable))) {
return i;
}
i++;
}
return -1;
}
}
}

View File

@@ -0,0 +1,92 @@
package com.stardust.autojs.runtime.record.accessibility;
import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;
import com.stardust.autojs.runtime.record.Recorder;
import com.stardust.view.accessibility.AccessibilityDelegate;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Created by Stardust on 2017/2/14.
*/
public class AccessibilityActionRecorder extends Recorder.AbstractRecorder implements AccessibilityDelegate {
public static class AccessibilityActionRecordEvent {
private final AccessibilityEvent mAccessibilityEvent;
public AccessibilityActionRecordEvent(AccessibilityEvent event) {
mAccessibilityEvent = event;
}
public AccessibilityEvent getAccessibilityEvent() {
return mAccessibilityEvent;
}
}
private static final Set<Integer> EVENT_TYPES = new HashSet<>(Arrays.asList(AccessibilityEvent.TYPE_VIEW_CLICKED, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, AccessibilityEvent.TYPE_VIEW_SCROLLED, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED));
private static final long RECORD_TIME_OUT = 10 * 60 * 1000;
private boolean mShouldIgnoreFirstAction = false;
private AccessibilityActionConverter mConverter;
private long mRecordStartMillis;
public AccessibilityActionRecorder() {
super(true);
}
@Override
protected void startImpl() {
mConverter = new AccessibilityActionConverter(mShouldIgnoreFirstAction);
mRecordStartMillis = System.currentTimeMillis();
}
@Override
protected void stopImpl() {
setState(STATE_NOT_START);
}
@Override
protected void resumeImpl() {
mConverter.onResume();
}
@Override
public boolean onAccessibilityEvent(AccessibilityService service, AccessibilityEvent event) {
if (getState() == STATE_RECORDING) {
mConverter.record(service, event);
checkTimeOut();
}
return false;
}
@Override
public Set<Integer> getEventTypes() {
return EVENT_TYPES;
}
private void checkTimeOut() {
if (System.currentTimeMillis() - mRecordStartMillis > RECORD_TIME_OUT) {
stop();
}
}
@Override
public String getCode() {
return mConverter.getScript();
}
public void setShouldIgnoreFirstAction(boolean shouldIgnoreFirstAction) {
mShouldIgnoreFirstAction = shouldIgnoreFirstAction;
}
}

View File

@@ -0,0 +1,25 @@
package com.stardust.autojs.runtime.record.inputevent;
/**
* Created by Stardust on 2017/3/7.
*/
public class EventFormatException extends RuntimeException {
public static EventFormatException forEventStr(String eventStr, NumberFormatException e) {
return new EventFormatException(eventStr, e);
}
public EventFormatException(Exception e) {
super(e);
}
public EventFormatException(String eventStr, Exception e) {
super("eventStr=" + eventStr, e);
}
public EventFormatException(String eventStr) {
super("eventStr=" + eventStr);
}
}

View File

@@ -0,0 +1,136 @@
package com.stardust.autojs.runtime.record.inputevent;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.widget.Toast;
import com.stardust.autojs.runtime.record.Recorder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by Stardust on 2017/3/7.
*/
public abstract class InputEventConverter {
static class Event {
static final Pattern PATTERN = Pattern.compile("^\\[([^\\]]*)\\]\\s+([^:]*):\\s+([^\\s]*)\\s+([^\\s]*)\\s+([^\\s]*)\\s*$");
static Event parseEvent(String eventStr) {
Matcher matcher = Event.PATTERN.matcher(eventStr);
if (!matcher.matches()) {
throw new EventFormatException(eventStr);
}
double time;
try {
time = Double.parseDouble(matcher.group(1));
} catch (NumberFormatException e) {
throw new EventFormatException(eventStr, e);
}
return new Event(time, matcher.group(2), matcher.group(3), matcher.group(4), matcher.group(5));
}
double time;
String device;
String type;
String code;
String value;
public Event(double time, String device, String type, String code, String value) {
this.time = time;
this.device = device;
this.type = type;
this.code = code;
this.value = value;
}
@Override
public String toString() {
return "Event{" +
"time=" + time +
", device='" + device + '\'' +
", type='" + type + '\'' +
", code='" + code + '\'' +
", value='" + value + '\'' +
'}';
}
}
private final static Pattern LAST_INT_PATTERN = Pattern.compile("[^0-9]+([0-9]+)$");
protected boolean mConverting = false;
private int mState = Recorder.STATE_NOT_START;
public void convertEventIfFormatCorrect(String eventStr) {
if (!mConverting)
return;
if (TextUtils.isEmpty(eventStr) || !eventStr.startsWith("["))
return;
Event event = parseEventOrNull(eventStr);
if (event != null) {
convertEvent(event);
}
}
public abstract void convertEvent(@NonNull Event event);
public String getGetEventCommand() {
return "getevent -t -l";
}
public void start() {
mConverting = true;
mState = Recorder.STATE_RECORDING;
}
public void resume() {
mConverting = true;
mState = Recorder.STATE_RECORDING;
}
public void pause() {
mConverting = false;
mState = Recorder.STATE_PAUSED;
}
public void stop() {
mConverting = false;
mState = Recorder.STATE_STOPPED;
}
public abstract String getCode();
private boolean mFirstEventFormatError = true;
public Event parseEventOrNull(String eventStr) {
try {
return Event.parseEvent(eventStr);
} catch (EventFormatException e) {
e.printStackTrace();
if (mFirstEventFormatError) {
mFirstEventFormatError = false;
}
return null;
}
}
public static int parseDeviceNumber(String device) {
Matcher matcher = LAST_INT_PATTERN.matcher(device);
if (matcher.find()) {
String someNumberStr = matcher.group(1);
return Integer.parseInt(someNumberStr);
}
return -1;
}
}

View File

@@ -0,0 +1,79 @@
package com.stardust.autojs.runtime.record.inputevent;
import android.content.Context;
import com.stardust.autojs.runtime.api.Shell;
import com.stardust.autojs.runtime.record.Recorder;
/**
* Created by Stardust on 2017/3/6.
*/
public class InputEventRecorder extends Recorder.AbstractRecorder {
private static final String TAG = "InputEventRecorder";
private String mGetEventCommand;
private Shell mShell;
protected InputEventConverter mInputEventConverter;
private Context mContext;
protected InputEventRecorder(Context context, InputEventConverter inputEventConverter) {
mGetEventCommand = inputEventConverter.getGetEventCommand();
mInputEventConverter = inputEventConverter;
mContext = context;
}
public void listen() {
mShell = new Shell(mContext, true);
mShell.setCallback(new Shell.SimpleCallback() {
@Override
public void onNewLine(String str) {
if (mShell.isInitialized()) {
convertEvent(str);
}
}
@Override
public void onInitialized() {
mShell.exec(mGetEventCommand);
}
@Override
public void onInterrupted(InterruptedException e) {
stop();
}
});
}
@Override
protected void startImpl() {
mInputEventConverter.start();
}
@Override
protected void pauseImpl() {
mInputEventConverter.pause();
}
@Override
protected void resumeImpl() {
mInputEventConverter.resume();
}
@Override
protected void stopImpl() {
mShell.exit();
mInputEventConverter.stop();
}
@Override
public String getCode() {
return mInputEventConverter.getCode();
}
protected void convertEvent(String eventStr) {
mInputEventConverter.convertEventIfFormatCorrect(eventStr);
}
}

View File

@@ -0,0 +1,158 @@
package com.stardust.autojs.runtime.record.inputevent;
import android.support.annotation.NonNull;
import com.stardust.util.MapEntries;
import java.util.HashMap;
import java.util.Map;
/**
* Created by Stardust on 2017/3/7.
*/
public class InputEventToJsConverter extends InputEventConverter {
interface EventHandler {
void handle(Event event);
}
abstract static class Router implements EventHandler {
private Map<String, EventHandler> mEventHandlerMap = new HashMap<>();
Router put(String key, EventHandler handler) {
mEventHandlerMap.put(key, handler);
return this;
}
@Override
public void handle(Event event) {
EventHandler handler = mEventHandlerMap.get(getKey(event));
if (handler != null) {
handler.handle(event);
}
}
protected abstract String getKey(Event event);
}
private class TypeRouter extends Router {
TypeRouter() {
put("EV_ABS", new AbsHandler());
put("EV_SYN", new SyncHandler());
put("EV_KEY", new KeyHandler());
}
@Override
protected String getKey(Event event) {
return event.type;
}
}
private static class Point {
int x, y;
}
private class AbsHandler implements EventHandler {
@Override
public void handle(Event event) {
if (event.code.equals("ABS_MT_POSITION_X")) {
mTouchPoint.x = Integer.parseInt(event.value, 16);
mTouchTime = event.time;
} else if (event.code.equals("ABS_MT_POSITION_Y")) {
mTouchPoint.y = Integer.parseInt(event.value, 16);
mTouchTime = event.time;
}
}
}
private class SyncHandler implements EventHandler {
@Override
public void handle(Event event) {
if (mTouchDown) {
if (mSwipe) {
appendCodeIfNotStopped(event);
mSwipe = false;
} else {
mLastTouchPoint.x = mTouchPoint.x;
mLastTouchPoint.y = mTouchPoint.y;
mSwipe = true;
}
}
}
private void appendCodeIfNotStopped(Event event) {
if (!mConverting)
return;
long interval = (long) (1000 * (event.time - mTouchTime));
mCode.append("sh.Swipe(")
.append(mLastTouchPoint.x).append(", ").append(mLastTouchPoint.y).append(", ")
.append(mTouchPoint.x).append(", ").append(mTouchPoint.y);
if (interval >= 1) {
mCode.append(", ").append(interval);
}
mCode.append(");\n");
}
}
private class KeyHandler implements EventHandler {
private Map<String, String> mKeyPressCodeMap = new MapEntries<String, String>()
.entry("KEY_HOME", "Home")
.entry("KEY_MENU", "Menu")
.entry("KEY_VOLUMEDOWN", "VolumeDown")
.entry("KEY_VOLUMEUP", "VolumeUp")
.entry("KEY_BACK", "Back")
.entry("KEY_CAMERA", "Camera")
.map();
@Override
public void handle(Event event) {
if (event.code.equals("BTN_TOUCH")) {
mTouchDown = event.value.equals("DOWN");
if (!mTouchDown && mSwipe) {
if (mConverting)
mCode.append("sh.Tap(").append(mLastTouchPoint.x).append(", ").append(mLastTouchPoint.y).append(");\n");
mSwipe = false;
}
} else if (event.value.equals("UP")) {
appendKeyPressCode(event);
}
}
private void appendKeyPressCode(Event event) {
String code = mKeyPressCodeMap.get(event.code);
if (code != null) {
mCode.append("sh.").append(code).append("();\n");
}
}
}
private EventHandler mEventHandler = new TypeRouter();
private StringBuilder mCode = new StringBuilder().append("var sh = new Shell(true);\n");
private Point mTouchPoint = new Point(), mLastTouchPoint = new Point();
private boolean mTouchDown = false, mSwipe = false;
private double mTouchTime;
@Override
public void convertEvent(@NonNull Event event) {
mEventHandler.handle(event);
}
@Override
public void stop() {
super.stop();
mCode.append("sh.exitAndWaitFor();");
}
public String getCode() {
return mCode.toString();
}
}

View File

@@ -0,0 +1,52 @@
package com.stardust.autojs.runtime.record.inputevent;
import android.support.annotation.NonNull;
import java.text.DecimalFormat;
/**
* Created by Stardust on 2017/3/7.
*/
public class InputEventToSendEventConverter extends InputEventConverter {
private static final DecimalFormat DELAY_FORMAT = new DecimalFormat("#.###");
private double mLastEventTime;
private StringBuilder mSendEventCommands = new StringBuilder();
@Override
public void convertEvent(@NonNull Event event) {
if (mLastEventTime == 0) {
mLastEventTime = event.time;
} else if (event.time - mLastEventTime > 0.1) {
mSendEventCommands.append("sleep ").append(DELAY_FORMAT.format(event.time - mLastEventTime)).append("\n");
mLastEventTime = event.time;
}
mSendEventCommands.append("sendevent ")
.append(event.device).append(" ")
.append(hex2dec(event.type)).append(" ")
.append(hex2dec(event.code)).append(" ")
.append(hex2dec(event.value)).append("\n");
}
@Override
public String getGetEventCommand() {
return "getevent -t";
}
public String getCode() {
return mSendEventCommands.toString();
}
private static String hex2dec(String hex) {
try {
return String.valueOf((int) Long.parseLong(hex, 16));
} catch (NumberFormatException e) {
throw new EventFormatException(e);
}
}
}

View File

@@ -0,0 +1,115 @@
package com.stardust.autojs.runtime.record.inputevent;
import android.support.annotation.NonNull;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.stardust.util.ScreenMetrics.getDeviceScreenHeight;
import static com.stardust.util.ScreenMetrics.getDeviceScreenWidth;
/**
* Created by Stardust on 2017/5/3.
*/
public class InputEventToSendEventJsConverter extends InputEventConverter {
private double mLastEventTime;
private StringBuilder mCode = new StringBuilder();
private int mTouchDevice = -1;
private int mLastTouchX = -1;
private int mLastTouchY = -1;
public InputEventToSendEventJsConverter() {
mCode.append("var sh = new Shell(true);\n")
.append("sh.SetScreenMetrics(").append(getDeviceScreenWidth()).append(", ")
.append(getDeviceScreenHeight()).append(");\n");
}
@Override
public void convertEvent(@NonNull Event event) {
if (mLastEventTime == 0) {
mLastEventTime = event.time;
} else if (event.time - mLastEventTime > 0.03) {
mCode.append("sh.usleep(").append((long) (1000000 * (event.time - mLastEventTime))).append(");\n");
mLastEventTime = event.time;
}
int device = parseDeviceNumber(event.device);
int type = (int) Long.parseLong(event.type, 16);
int code = (int) Long.parseLong(event.code, 16);
int value = (int) Long.parseLong(event.value, 16);
if (type == 3) {
if (code == 53) {
onTouchX(device, value);
return;
}
if (code == 54) {
onTouchY(device, value);
return;
}
}
checkLastTouch();
mCode.append("sh.SendEvent(");
if (device != mTouchDevice) {
mCode.append(device).append(", ");
}
mCode.append(type).append(", ")
.append(code).append(", ")
.append(value).append(");\n");
}
private void checkLastTouch() {
if (mLastTouchX >= 0) {
mCode.append("sh.TouchX(").append(mLastTouchX).append(");\n");
mLastTouchX = -1;
}
if (mLastTouchY >= 0) {
mCode.append("sh.TouchY(").append(mLastTouchY).append(");\n");
mLastTouchY = -1;
}
}
private void onTouchX(int device, int value) {
if (mTouchDevice == -1) {
setTouchDevice(device);
}
mLastTouchX = value;
}
private void onTouchY(int device, int value) {
if (mTouchDevice == -1) {
setTouchDevice(device);
}
if (mLastTouchX >= 0) {
mCode.append("sh.Touch(")
.append(mLastTouchX).append(", ")
.append(value).append(");\n");
mLastTouchX = -1;
} else {
mLastTouchY = value;
}
}
private void setTouchDevice(int i) {
mCode.append("sh.SetTouchDevice(").append(i).append(");\n");
mTouchDevice = i;
}
@Override
public String getGetEventCommand() {
return "getevent -t";
}
public String getCode() {
return mCode.toString();
}
@Override
public void stop() {
super.stop();
mCode.append("sh.exitAndWaitFor();");
}
}

View File

@@ -0,0 +1,67 @@
package com.stardust.autojs.runtime.record.inputevent;
import android.content.Context;
import android.support.annotation.NonNull;
/**
* Created by Stardust on 2017/5/4.
*/
public class KeyObserver {
public interface KeyListener {
void onKeyDown(String keyName);
void onKeyUp(String keyName);
}
private InputEventRecorder mObserver;
private KeyListener mKeyListener;
public KeyObserver(Context context) {
mObserver = new InputEventRecorder(context, new InputEventConverter() {
@Override
public void convertEvent(@NonNull Event event) {
if (event.value.equalsIgnoreCase("UP")) {
notifyKeyUp(event.code);
}
if (event.value.equalsIgnoreCase("DOWN")) {
notifyKeyDown(event.code);
}
}
@Override
public String getCode() {
return null;
}
});
}
public void setKeyListener(KeyListener keyListener) {
mKeyListener = keyListener;
}
public void startListening() {
mObserver.listen();
mObserver.start();
}
public void stopListening() {
mObserver.stopImpl();
}
private void notifyKeyDown(String keyName) {
if (mKeyListener != null) {
mKeyListener.onKeyDown(keyName);
}
}
private void notifyKeyUp(String keyName) {
if (mKeyListener != null) {
mKeyListener.onKeyUp(keyName);
}
}
}

View File

@@ -0,0 +1,102 @@
package com.stardust.autojs.runtime.record.inputevent;
import android.content.Context;
import android.support.annotation.NonNull;
/**
* Created by Stardust on 2017/7/20.
*/
public class TouchObserver {
public interface OnTouchEventListener {
void onTouch(int x, int y);
}
private int mTouchX, mTouchY;
private InputEventRecorder mRecorder;
private OnTouchEventListener mOnTouchEventListener;
public TouchObserver(Context context) {
mRecorder = new InputEventRecorder(context, new TouchEventObserver());
}
public void observe() {
mRecorder.listen();
mRecorder.start();
}
public void stop() {
mRecorder.stop();
}
public void setOnTouchEventListener(OnTouchEventListener onTouchEventListener) {
mOnTouchEventListener = onTouchEventListener;
}
private void onTouch(int x, int y) {
mTouchX = x;
mTouchY = y;
if(mOnTouchEventListener != null){
mOnTouchEventListener.onTouch(x, y);
}
}
private class TouchEventObserver extends InputEventConverter {
private int mLastTouchX, mLastTouchY;
@Override
public void convertEvent(@NonNull Event event) {
int device = parseDeviceNumber(event.device);
int type = (int) Long.parseLong(event.type, 16);
int code = (int) Long.parseLong(event.code, 16);
int value = (int) Long.parseLong(event.value, 16);
if (type != 3) {
return;
}
if (code == 53) {
onTouchX(value);
return;
}
if (code == 54) {
onTouchY(value);
return;
}
if (mLastTouchX >= 0) {
onTouch(mLastTouchX, mTouchY);
mLastTouchX = -1;
return;
}
if (mLastTouchY >= 0) {
onTouch(mTouchX, mLastTouchY);
mLastTouchY = -1;
}
}
private void onTouchX(int value) {
mLastTouchX = value;
}
private void onTouchY(int value) {
if (mLastTouchX > 0) {
onTouch(mLastTouchX, value);
return;
}
mLastTouchY = value;
}
@Override
public String getGetEventCommand() {
return "getevent -t";
}
@Override
public String getCode() {
return null;
}
}
}

View File

@@ -0,0 +1,22 @@
package com.stardust.autojs.runtime.record.inputevent;
import android.content.Context;
/**
* Created by Stardust on 2017/3/16.
*/
public class TouchRecorder extends InputEventRecorder {
public TouchRecorder(Context context) {
super(context, new InputEventToSendEventJsConverter());
listen();
}
@Override
public void stop() {
super.stop();
}
}