From 8401ce40c6f6fb5398df74b3e6fbf55f0232847c Mon Sep 17 00:00:00 2001 From: hyb1996 <946994919@qq.com> Date: Tue, 11 Sep 2018 13:01:37 +0800 Subject: [PATCH] feat(debug): use json socket to connect to remote debug client --- .../autojs/pluginclient/DevPluginClient.java | 187 ----------------- .../DevPluginResponseHandler.java | 16 +- .../autojs/pluginclient/DevPluginService.java | 105 ++++++++-- .../autojs/pluginclient/JsonSocket.java | 195 ++++++++++++++++-- .../autojs/autojs/pluginclient/Router.java | 43 +++- .../autojs/autojs/tool/EmptyObservers.java | 15 ++ .../autojs/ui/main/drawer/DrawerFragment.java | 24 ++- app/src/main/res/values/strings.xml | 1 + 8 files changed, 344 insertions(+), 242 deletions(-) delete mode 100644 app/src/main/java/org/autojs/autojs/pluginclient/DevPluginClient.java create mode 100644 app/src/main/java/org/autojs/autojs/tool/EmptyObservers.java diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginClient.java b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginClient.java deleted file mode 100644 index 6087b883..00000000 --- a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginClient.java +++ /dev/null @@ -1,187 +0,0 @@ -package org.autojs.autojs.pluginclient; - -import android.os.Build; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import org.autojs.autojs.tool.SimpleObserver; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.Socket; -import java.util.Map; - -import io.reactivex.Observable; -import io.reactivex.subjects.PublishSubject; - -/** - * Created by Stardust on 2017/5/10. - */ - -public class DevPluginClient { - - public static class State { - - public static final int DISCONNECTED = 0; - public static final int CONNECTING = 1; - public static final int CONNECTED = 2; - - private final int mState; - private final Throwable mException; - - public State(int state, Throwable exception) { - mState = state; - mException = exception; - } - - public State(int state) { - this(state, null); - } - - public int getState() { - return mState; - } - - public Throwable getException() { - return mException; - } - } - - private static final State STATE_CONNECTED = new State(State.CONNECTED); - private static final State STATE_CONNECTING = new State(State.CONNECTING); - - private volatile Socket mSocket; - private Handler mResponseHandler; - private String host; - private int port; - private OutputStream mOutputStream; - private final PublishSubject mConnection; - private int mState; - - public DevPluginClient(String host, int port, PublishSubject connection) { - this.host = host; - this.port = port; - mConnection = connection; - mConnection.subscribe(state -> mState = state.getState()); - } - - public int getState() { - return mState; - } - - public void setResponseHandler(Handler handler) { - mResponseHandler = handler; - } - - public void connectToServer() { - if (mState != State.DISCONNECTED) { - throw new IllegalStateException("Connecting or Connected!"); - } - new Thread(() -> { - if (mState != State.DISCONNECTED) { - return; - } - mConnection.onNext(STATE_CONNECTING); - try { - connect(); - if (mSocket != null) - readDataFromSocket(); - close(); - } catch (Exception e) { - e.printStackTrace(); - close(); - mConnection.onNext(new State(State.DISCONNECTED, e)); - } - }).start(); - } - - - private void readDataFromSocket() throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(mSocket.getInputStream())); - JsonParser parser = new JsonParser(); - while (!Thread.currentThread().isInterrupted()) { - String line = reader.readLine(); - if (line == null) { - return; - } - if (mResponseHandler != null) { - handleData(parser, line); - - } - } - } - - private void handleData(JsonParser parser, String line) { - try { - JsonElement jsonElement = parser.parse(line); - JsonObject jsonObject = jsonElement.getAsJsonObject(); - mResponseHandler.handle(jsonObject); - } catch (Exception e) { - e.printStackTrace(); - //ignore exception thrown by data handler - } - } - - private void connect() throws IOException { - mSocket = new Socket(host, port); - mConnection.onNext(STATE_CONNECTED); - mOutputStream = mSocket.getOutputStream(); - sendDeviceName(); - } - - private void sendDeviceName() { - JsonObject object = new JsonObject(); - object.addProperty("type", "device_name"); - object.addProperty("device_name", Build.BRAND + " " + Build.MODEL); - send(object).subscribe(); - } - - - public Observable send(final JsonObject object) { - if (mState != State.CONNECTED) { - throw new IllegalStateException("Not connected!"); - } - return Observable.fromCallable(() -> { - mOutputStream.write(object.toString().getBytes()); - mOutputStream.write("\n".getBytes()); - mOutputStream.flush(); - return object; - }); - } - - public Observable send(Map data) { - JsonObject object = new JsonObject(); - for (Map.Entry entry : data.entrySet()) { - Object value = entry.getValue(); - if (value instanceof Number) { - object.addProperty(entry.getKey(), (Number) value); - } else if (value instanceof Boolean) { - object.addProperty(entry.getKey(), (Boolean) value); - } else if (value instanceof Character) { - object.addProperty(entry.getKey(), (Character) value); - } else { - object.addProperty(entry.getKey(), value.toString()); - } - } - return send(object); - } - - public boolean close() { - if (mSocket != null) { - try { - mSocket.close(); - } catch (IOException e) { - e.printStackTrace(); - } - mSocket = null; - mOutputStream = null; - return true; - } - return false; - } - - -} diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginResponseHandler.java b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginResponseHandler.java index 29f28966..93469521 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginResponseHandler.java +++ b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginResponseHandler.java @@ -25,18 +25,18 @@ import java.util.HashMap; public class DevPluginResponseHandler implements Handler { - private Router mRouter = new Router("type") + private Router mRouter = new Router.RootRouter("type") .handler("command", new Router("command") .handler("run", data -> { String script = data.get("script").getAsString(); String name = getName(data); - String viewId = data.get("view_id").getAsString(); - runScript(viewId, name, script); + String id = data.get("id").getAsString(); + runScript(id, name, script); return false; }) .handler("stop", data -> { - String viewId = data.get("view_id").getAsString(); - stopScript(viewId); + String id = data.get("id").getAsString(); + stopScript(id); return true; }) .handler("save", data -> { @@ -46,11 +46,11 @@ public class DevPluginResponseHandler implements Handler { return false; }) .handler("rerun", data -> { - String viewId = data.get("view_id").getAsString(); + String id = data.get("id").getAsString(); String script = data.get("script").getAsString(); String name = getName(data); - stopScript(viewId); - runScript(viewId, name, script); + stopScript(id); + runScript(id, name, script); return false; }) .handler("stopAll", data -> { diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.java b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.java index 01a53af3..0effc14c 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.java +++ b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.java @@ -1,7 +1,19 @@ package org.autojs.autojs.pluginclient; +import android.annotation.SuppressLint; +import android.os.Build; +import android.util.Pair; + import com.google.gson.JsonObject; +import org.autojs.autojs.tool.EmptyObservers; + +import java.io.IOException; +import java.net.Socket; + +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; @@ -11,20 +23,48 @@ import io.reactivex.subjects.PublishSubject; public class DevPluginService { + public static class State { + + public static final int DISCONNECTED = 0; + public static final int CONNECTING = 1; + public static final int CONNECTED = 2; + + private final int mState; + private final Throwable mException; + + public State(int state, Throwable exception) { + mState = state; + mException = exception; + } + + public State(int state) { + this(state, null); + } + + public int getState() { + return mState; + } + + public Throwable getException() { + return mException; + } + } + + private static final int PORT = 9317; private static DevPluginService sInstance = new DevPluginService(); - private DevPluginClient mClient; - private final PublishSubject mConnection = PublishSubject.create(); + private final PublishSubject mConnectionState = PublishSubject.create(); + private volatile JsonSocket mSocket; public static DevPluginService getInstance() { return sInstance; } public boolean isConnected() { - return mClient != null && mClient.getState() == DevPluginClient.State.CONNECTED; + return mSocket != null && !mSocket.isClosed(); } public boolean isDisconnected() { - return mClient == null || mClient.getState() == DevPluginClient.State.DISCONNECTED; + return mSocket == null || mSocket.isClosed(); } public void disconnectIfNeeded() { @@ -34,37 +74,68 @@ public class DevPluginService { } public void disconnect() { - mClient.close(); - mClient = null; - mConnection.onNext(new DevPluginClient.State(DevPluginClient.State.DISCONNECTED)); + mSocket.close(); + mSocket = null; } - public PublishSubject getConnection() { - return mConnection; + public Observable connectionState() { + return mConnectionState; } - public void connectToServer(String host) { - int port = 1209; + public Observable connectToServer(String host) { + int port = PORT; String ip = host; int i = host.lastIndexOf(':'); if (i > 0 && i < host.length() - 1) { port = Integer.parseInt(host.substring(i + 1)); ip = host.substring(0, i); } - mClient = new DevPluginClient(ip, port, mConnection); - mClient.setResponseHandler(new DevPluginResponseHandler()); - mClient.connectToServer(); + return createSocket(ip, port) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext(socket -> mSocket = socket); } + private Observable createSocket(String ip, int port) { + return Observable.fromCallable(() -> { + JsonSocket jsonSocket = new JsonSocket(new Socket(ip, port)); + DevPluginResponseHandler handler = new DevPluginResponseHandler(); + jsonSocket.data() + .observeOn(AndroidSchedulers.mainThread()) + .doOnComplete(() -> mConnectionState.onNext(new State(State.DISCONNECTED))) + .subscribe(data -> handler.handle(data.getAsJsonObject()), e -> { + e.printStackTrace(); + mConnectionState.onNext(new State(State.DISCONNECTED)); + }); + + writePair(jsonSocket, "device_name", new Pair<>("device_name", Build.BRAND + " " + Build.MODEL)); + return jsonSocket; + }) + .subscribeOn(Schedulers.io()); + } + + private static int write(JsonSocket socket, String type, JsonObject data) throws IOException { + JsonObject json = new JsonObject(); + json.addProperty("type", type); + json.add("data", data); + return socket.write(json); + } + + private static int writePair(JsonSocket socket, String type, Pair pair) throws IOException { + JsonObject data = new JsonObject(); + data.addProperty(pair.first, pair.second); + return write(socket, type, data); + } + + + @SuppressLint("CheckResult") public void log(String log) { if (!isConnected()) return; JsonObject object = new JsonObject(); object.addProperty("type", "log"); object.addProperty("log", log); - mClient.send(object) + Observable.fromCallable(() -> mSocket.write(object)) .subscribeOn(Schedulers.io()) - .subscribe(ignored -> { - }, Throwable::printStackTrace); + .subscribe(EmptyObservers.consumer(), Throwable::printStackTrace); } } diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java index d383a290..b938bce5 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java +++ b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java @@ -1,61 +1,214 @@ package org.autojs.autojs.pluginclient; +import android.util.Log; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + import java.io.IOException; import java.io.InputStream; -import java.io.PipedInputStream; +import java.io.OutputStream; import java.net.Socket; -import java.nio.ByteBuffer; + +import io.reactivex.Observable; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.PublishSubject; public class JsonSocket { - private Socket mSocket; + private static final byte DELIMITER = '#'; + private static final String LOG_TAG = "JsonSocket"; - public JsonSocket(Socket socket) { + private final Socket mSocket; + private final JsonParser mJsonParser = new JsonParser(); + private OutputStream mOutputStream; + private final PublishSubject mJsonElementPublishSubject = PublishSubject.create(); + private volatile boolean mClosed = false; + + public JsonSocket(Socket socket) throws IOException { mSocket = socket; + mOutputStream = socket.getOutputStream(); + new Thread(new SocketReader(socket)).start(); + } + public Observable data() { + return mJsonElementPublishSubject; + } + + public int write(JsonElement element) throws IOException { + byte[] bytes = element.toString().getBytes(); + String length = String.valueOf(bytes.length) + DELIMITER; + mOutputStream.write(length.getBytes()); + mOutputStream.write(bytes); + Log.d(LOG_TAG, "write: length = " + bytes.length + ", json = " + element); + return bytes.length; + } + + public void close() { + mJsonElementPublishSubject.onComplete(); + mClosed = true; + try { + mSocket.close(); + } catch (IOException ignored) { + } } - private static class SocketReader implements Runnable { + private void close(Exception e) { + if (mClosed) { + return; + } + mJsonElementPublishSubject.onError(e); + mClosed = true; + try { + mSocket.close(); + } catch (IOException ignored) { + } + } + + private void dispatchJson(String json) { + try { + JsonElement element = mJsonParser.parse(json); + mJsonElementPublishSubject.onNext(element); + } catch (JsonParseException e) { + e.printStackTrace(); + } + + } + + public boolean isClosed() { + return mClosed; + } + + private static class ByteQueue { + byte[] data; + int offset = 0; + int length = 0; + + public ByteQueue(int initialCapacity) { + data = new byte[initialCapacity]; + } + + int read(InputStream stream) throws IOException { + if (length >= data.length) { + resize(); + } + int end = offset + length; + int n; + if (end >= data.length) { + n = stream.read(data, 0, offset); + } else { + n = stream.read(data, end, data.length - end); + } + length += n; + return n; + } + + + void pop(int len) { + if (len > length) { + throw new IllegalArgumentException("pop " + len + " but current length is " + length); + } + offset += len; + if (offset >= data.length) { + offset -= data.length; + } + } + + String popAsString(int len) { + if (len > length) { + throw new IllegalArgumentException("popAsString " + len + " but current length is " + length); + } + int end = offset + len; + String str; + if (end < data.length) { + str = new String(data, offset, len); + } else { + byte[] bytes = new byte[len]; + System.arraycopy(data, offset, bytes, 0, data.length - offset); + System.arraycopy(data, 0, bytes, data.length - offset, len - (data.length - offset)); + str = new String(bytes); + } + pop(len); + return str; + } + + private void resize() { + byte[] newData = new byte[data.length * 2]; + System.arraycopy(data, 0, newData, 0, data.length); + data = newData; + } + + + } + + private class SocketReader implements Runnable { private final Socket mSocket; private final InputStream mInputStream; - private ByteBuffer mByteBuffer; private int mJsonDataLength = -1; - private int mReceivedDataLength = 0; - private byte[] mBuffer; + private ByteQueue mByteQueue = new ByteQueue(4096); private SocketReader(Socket socket) throws IOException { mSocket = socket; mInputStream = mSocket.getInputStream(); - mByteBuffer = ByteBuffer.allocateDirect(3); } @Override public void run() { - try { readLoop(); - - } catch (IOException e) { - - } finally { + close(); + } catch (Exception e) { + e.printStackTrace(); + close(e); } } - private void readLoop() throws IOException { - byte[] buffer = new byte[4096]; + + private void readLoop() throws Exception { int n; - while ((n = mInputStream.read(buffer)) > 0) { - onChunk(buffer, 0, n); + while ((n = mByteQueue.read(mInputStream)) > 0) { + onChunk(mByteQueue, n); } } - private void onChunk(byte[] data, int offset, int length) { - if(mJsonDataLength != 0){ - + private void onChunk(ByteQueue byteQueue, int chunkSize) { + if (mJsonDataLength <= 0) { + tryReadingJsonDataLength(byteQueue, chunkSize); + return; + } + if (byteQueue.length < mJsonDataLength) { + return; + } + String json = byteQueue.popAsString(mJsonDataLength); + Log.d(LOG_TAG, "json = " + json); + mJsonDataLength = -1; + dispatchJson(json); + + } + + + private void tryReadingJsonDataLength(ByteQueue byteQueue, int chunkSize) { + int end = byteQueue.offset + byteQueue.length; + for (int i = 1; i <= chunkSize; i++) { + if (byteQueue.data[end - i] == DELIMITER) { + String jsonDataLength = new String(byteQueue.data, byteQueue.offset, end - i); + Log.d(LOG_TAG, "json data length = " + jsonDataLength); + byteQueue.pop(end - i + 1); + receiveJsonDataLength(jsonDataLength); + } + } + } + + private void receiveJsonDataLength(String jsonDataLength) { + try { + mJsonDataLength = Integer.parseInt(jsonDataLength); + } catch (Exception e) { + e.printStackTrace(); } } } diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/Router.java b/app/src/main/java/org/autojs/autojs/pluginclient/Router.java index 644e2b39..d0601a54 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/Router.java +++ b/app/src/main/java/org/autojs/autojs/pluginclient/Router.java @@ -1,5 +1,8 @@ package org.autojs.autojs.pluginclient; +import android.util.Log; + +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.util.HashMap; @@ -11,13 +14,18 @@ import java.util.Map; public class Router implements Handler { - private Map mHandlerMap = new HashMap<>(); - private String mKey; + private static final String LOG_TAG = "Router"; + protected Map mHandlerMap = new HashMap<>(); + private final String mKey; public Router(String key) { mKey = key; } + public String getKey() { + return mKey; + } + public Router handler(String value, Handler handler) { mHandlerMap.put(value, handler); return this; @@ -25,8 +33,35 @@ public class Router implements Handler { @Override public boolean handle(JsonObject data) { - Handler handler = mHandlerMap.get(data.get(mKey).getAsString()); - return handler != null && handler.handle(data); + Log.d(LOG_TAG, "handle: " + data); + JsonElement key = data.get(getKey()); + if (key == null || !key.isJsonPrimitive()) { + Log.w(LOG_TAG, "no such key: " + getKey()); + return false; + } + Handler handler = mHandlerMap.get(key.getAsString()); + return handleInternal(data, key.getAsString(), handler); + } + + protected boolean handleInternal(JsonObject json, String key, Handler handler) { + return handler != null && handler.handle(json); + } + + public static class RootRouter extends Router { + + public RootRouter(String key) { + super(key); + } + + @Override + protected boolean handleInternal(JsonObject json, String key, Handler handler) { + JsonElement data = json.get("data"); + if (data == null || !data.isJsonObject()) { + Log.w(LOG_TAG, "json has no object data: " + json); + return false; + } + return handler != null && handler.handle(data.getAsJsonObject()); + } } } diff --git a/app/src/main/java/org/autojs/autojs/tool/EmptyObservers.java b/app/src/main/java/org/autojs/autojs/tool/EmptyObservers.java new file mode 100644 index 00000000..789f1bbc --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/tool/EmptyObservers.java @@ -0,0 +1,15 @@ +package org.autojs.autojs.tool; + +import io.reactivex.functions.Consumer; + +public class EmptyObservers { + + private static final Consumer CONSUMER = ignored -> { + + }; + + @SuppressWarnings("unchecked") + public static Consumer consumer() { + return CONSUMER; + } +} diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.java b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.java index d6c92d15..de7c4463 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.java +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.java @@ -19,11 +19,12 @@ import com.bumptech.glide.request.target.SimpleTarget; import com.bumptech.glide.request.transition.Transition; import com.stardust.app.GlobalAppContext; import com.stardust.notification.NotificationListenerService; + import org.autojs.autojs.Pref; import org.autojs.autojs.R; import org.autojs.autojs.network.GlideApp; import org.autojs.autojs.network.UserService; -import org.autojs.autojs.pluginclient.DevPluginClient; +import org.autojs.autojs.tool.EmptyObservers; import org.autojs.autojs.ui.common.NotAskAgainDialog; import org.autojs.autojs.ui.floating.CircularMenu; import org.autojs.autojs.ui.floating.FloatyWindowManger; @@ -41,12 +42,17 @@ import org.autojs.autojs.ui.update.UpdateInfoDialogBuilder; import org.autojs.autojs.ui.user.WebActivity; import org.autojs.autojs.ui.user.WebActivity_; import org.autojs.autojs.ui.widget.AvatarView; + import com.stardust.theme.ThemeColorManager; + import org.autojs.autojs.theme.ThemeColorManagerCompat; + import com.stardust.view.accessibility.AccessibilityService; + import org.autojs.autojs.pluginclient.DevPluginService; import org.autojs.autojs.tool.AccessibilityServiceTool; import org.autojs.autojs.tool.WifiTool; + import com.stardust.util.IntentUtil; import org.androidannotations.annotations.AfterViews; @@ -112,12 +118,12 @@ public class DrawerFragment extends android.support.v4.app.Fragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mConnectionStateDisposable = DevPluginService.getInstance().getConnection() + mConnectionStateDisposable = DevPluginService.getInstance().connectionState() .observeOn(AndroidSchedulers.mainThread()) .subscribe(state -> { if (mConnectionItem != null) { - setChecked(mConnectionItem, state.getState() == DevPluginClient.State.CONNECTED); - setProgress(mConnectionItem, state.getState() == DevPluginClient.State.CONNECTING); + setChecked(mConnectionItem, state.getState() == DevPluginService.State.CONNECTED); + setProgress(mConnectionItem, state.getState() == DevPluginService.State.CONNECTING); } if (state.getException() != null) { showMessage(state.getException().getMessage()); @@ -251,7 +257,8 @@ public class DrawerFragment extends android.support.v4.app.Fragment { .title(R.string.text_server_address) .input("", host, (dialog, input) -> { Pref.saveServerAddress(input.toString()); - DevPluginService.getInstance().connectToServer(input.toString()); + DevPluginService.getInstance().connectToServer(input.toString()) + .subscribe(EmptyObservers.consumer(), this::onConnectException); }) .neutralText(R.string.text_help) .onNeutral((dialog, which) -> { @@ -262,6 +269,13 @@ public class DrawerFragment extends android.support.v4.app.Fragment { .show(); } + private void onConnectException(Throwable e) { + e.printStackTrace(); + setChecked(mConnectionItem, false); + Toast.makeText(GlobalAppContext.get(), getString(R.string.error_connect_to_remote, e.getMessage()), + Toast.LENGTH_LONG).show(); + } + void checkForUpdates(DrawerMenuItemViewHolder holder) { setProgress(mCheckForUpdatesItem, true); VersionService.getInstance().checkForUpdates() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f972154c..1db03db0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -380,4 +380,5 @@ 关闭 执行 使用系统闹钟唤醒Auto.js + 连接失败: %s