6.0.1 - 增加客户端/服务端连接方式 增加部分全局对象 部分插件及工具版本更新

This commit is contained in:
SuperMonster003
2022-01-01 19:53:25 +08:00
parent 8e8edbf848
commit ad5b7c9b37
44 changed files with 1537 additions and 707 deletions

View File

@@ -6,16 +6,21 @@
# v6.0.1
###### 2021/12/07
###### 2022/01/01
* `新增` polyfill (Object.getOwnPropertyDescriptors)
* `新增` polyfill (Array.prototype.flat)
* `新增` isInteger/isNullish/isPlainObject/isPrimitive/isReference
* `优化` 扩展global.sleep支持随机范围/负数兼容
* `优化` 扩展global.toast支持时长控制/强制覆盖控制/dismiss方法
* `新增` 连接 VSCode 插件支持客户端 (LAN) 及服务端 (LAN/ADB) 方式 (Ref to Auto.js Pro)
* `新增` 增加 $base64 "工具类" (Ref to Auto.js Pro)
* `新增` 增加 isInteger/isNullish/isPlainObject/isPrimitive/isReference 全局方法
* `新增` 增加 polyfill (Object.getOwnPropertyDescriptors)
* `新增` 增加 polyfill (Array.prototype.flat)
* `优化` 扩展 global.sleep 支持 随机范围/负数兼容
* `优化` 扩展 global.toast 支持 时长控制/强制覆盖控制/dismiss
* `优化` 包名对象全局化 (okhttp3/androidx/de)
* `优化` 升级 Android Material 版本 1.5.0-beta01 -> 1.6.0-alpha01
* `优化` 升级 Android Gradle 插件版本 7.2.0-alpha04 -> 7.2.0-alpha05
* `优化` 升级 Android Gradle 插件版本 7.2.0-alpha04 -> 7.2.0-alpha06
* `优化` 升级 Kotlinx Coroutines 版本 1.5.2-native-mt -> 1.6.0-native-mt
* `优化` 升级 Kotlin Gradle 插件版本 1.6.0 -> 1.6.10
* `优化` 升级 Gradle 发行版本 7.3 -> 7.3.3
# v6.0.0

View File

@@ -6,6 +6,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Looper;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.stardust.app.GlobalAppContext;
@@ -15,12 +16,15 @@ import com.stardust.autojs.runtime.accessibility.AccessibilityConfig;
import com.stardust.autojs.runtime.api.AppUtils;
import com.stardust.autojs.runtime.exception.ScriptException;
import com.stardust.autojs.runtime.exception.ScriptInterruptedException;
import com.stardust.view.accessibility.AccessibilityService;
import com.stardust.view.accessibility.LayoutInspector;
import com.stardust.view.accessibility.NodeInfo;
import org.autojs.autojs.BuildConfig;
import org.autojs.autojs.Pref;
import org.autojs.autojs.R;
import org.autojs.autojs.external.fileprovider.AppFileProvider;
import org.autojs.autojs.pluginclient.DevPluginService;
import org.autojs.autojs.tool.AccessibilityServiceTool;
import org.autojs.autojs.ui.floating.FloatyWindowManger;
import org.autojs.autojs.ui.floating.FullScreenFloatyWindow;
import org.autojs.autojs.ui.floating.layoutinspector.LayoutBoundsFloatyWindow;
@@ -28,12 +32,6 @@ import org.autojs.autojs.ui.floating.layoutinspector.LayoutHierarchyFloatyWindow
import org.autojs.autojs.ui.log.LogActivity_;
import org.autojs.autojs.ui.settings.SettingsActivity_;
import com.stardust.view.accessibility.AccessibilityService;
import com.stardust.view.accessibility.LayoutInspector;
import com.stardust.view.accessibility.NodeInfo;
import org.autojs.autojs.tool.AccessibilityServiceTool;
/**
* Created by Stardust on 2017/4/2.
@@ -59,31 +57,30 @@ public class AutoJs extends com.stardust.autojs.AutoJs {
FullScreenFloatyWindow create(NodeInfo nodeInfo);
}
private BroadcastReceiver mLayoutInspectBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
try {
ensureAccessibilityServiceEnabled();
String action = intent.getAction();
if (LayoutBoundsFloatyWindow.class.getName().equals(action)) {
capture(LayoutBoundsFloatyWindow::new);
} else if (LayoutHierarchyFloatyWindow.class.getName().equals(action)) {
capture(LayoutHierarchyFloatyWindow::new);
}
} catch (Exception e) {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw e;
}
}
}
};
private AutoJs(final Application application) {
super(application);
getScriptEngineService().registerGlobalScriptExecutionListener(new ScriptExecutionGlobalListener());
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(LayoutBoundsFloatyWindow.class.getName());
intentFilter.addAction(LayoutHierarchyFloatyWindow.class.getName());
BroadcastReceiver mLayoutInspectBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
try {
ensureAccessibilityServiceEnabled();
String action = intent.getAction();
if (LayoutBoundsFloatyWindow.class.getName().equals(action)) {
capture(LayoutBoundsFloatyWindow::new);
} else if (LayoutHierarchyFloatyWindow.class.getName().equals(action)) {
capture(LayoutHierarchyFloatyWindow::new);
}
} catch (Exception e) {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw e;
}
}
}
};
LocalBroadcastManager.getInstance(application).registerReceiver(mLayoutInspectBroadcastReceiver, intentFilter);
}
@@ -115,7 +112,7 @@ public class AutoJs extends com.stardust.autojs.AutoJs {
@Override
public String println(int level, CharSequence charSequence) {
String log = super.println(level, charSequence);
DevPluginService.getInstance().log(log);
DevPluginService.getInstance().print(log);
return log;
}
};

View File

@@ -1,12 +1,16 @@
package org.autojs.autojs.autojs;
import android.annotation.SuppressLint;
import com.stardust.app.GlobalAppContext;
import com.stardust.autojs.engine.JavaScriptEngine;
import com.stardust.autojs.execution.ScriptExecution;
import com.stardust.autojs.execution.ScriptExecutionListener;
import org.autojs.autojs.App;
import com.stardust.autojs.runtime.api.Console;
import org.autojs.autojs.R;
import java.math.BigDecimal;
/**
* Created by Stardust on 2017/5/3.
*/
@@ -24,13 +28,26 @@ public class ScriptExecutionGlobalListener implements ScriptExecutionListener {
onFinish(execution);
}
@SuppressLint("DefaultLocale")
private void onFinish(ScriptExecution execution) {
Long millis = (Long) execution.getEngine().getTag(ENGINE_TAG_START_TIME);
if (millis == null)
return;
if (millis != null) {
printSeconds(execution, millis);
}
}
private void printSeconds(ScriptExecution execution, Long millis) {
double seconds = (System.currentTimeMillis() - millis) / 1000.0;
AutoJs.getInstance().getScriptEngineService().getGlobalConsole()
.verbose(GlobalAppContext.getString(R.string.text_execution_finished), execution.getSource().toString(), seconds);
@SuppressLint("DefaultLocale")
BigDecimal secondsString = new BigDecimal(String.format("%.3f", seconds)).stripTrailingZeros();
printSeconds(execution, secondsString);
}
private void printSeconds(ScriptExecution execution, BigDecimal seconds) {
Console console = AutoJs.getInstance().getScriptEngineService().getGlobalConsole();
console.verbose(GlobalAppContext.getString(R.string.text_execution_finished), execution.getSource().toString(), seconds);
}
@Override

View File

@@ -1,5 +1,7 @@
package org.autojs.autojs.model.explorer;
import androidx.annotation.NonNull;
import com.stardust.pio.PFile;
import com.stardust.util.ObjectHelper;
import com.stardust.util.Objects;
@@ -18,7 +20,7 @@ public class ExplorerFileItem implements ExplorerItem {
"js", "java", "xml", "json", "txt", "log", "ts"
));
private PFile mFile;
private final PFile mFile;
private final ExplorerPage mParent;
public ExplorerFileItem(PFile file, ExplorerPage parent) {
@@ -104,6 +106,7 @@ public class ExplorerFileItem implements ExplorerItem {
return type.equals("js") || type.equals("auto");
}
@NonNull
@Override
public String toString() {
return getClass().getSimpleName() + "{" +

View File

@@ -0,0 +1,138 @@
package org.autojs.autojs.pluginclient;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class Buffer {
public final int length;
public final byte[] bytes;
public Buffer(int length) {
this.bytes = new byte[length];
this.length = this.bytes.length;
}
public Buffer(byte[] bytes) {
this.bytes = bytes;
if (this.bytes != null) {
this.length = this.bytes.length;
} else {
this.length = 0;
}
}
public int readInt8(int offset) {
return ((int) this.bytes[offset] & 0xff);
}
public int readInt16BE(int offset) {
return (((int) this.bytes[offset + 2] & 0xff) << 8) |
((int) this.bytes[offset + 3] & 0xff);
}
public int readInt16LE(int offset) {
return ((int) this.bytes[offset] & 0xff) |
(((int) this.bytes[offset + 1] & 0xff) << 8);
}
public int readInt32BE(int offset) {
return (((int) this.bytes[offset] & 0xff) << 24) |
(((int) this.bytes[offset + 1] & 0xff) << 16) |
(((int) this.bytes[offset + 2] & 0xff) << 8) |
((int) this.bytes[offset + 3] & 0xff);
}
public int readInt32LE(int offset) {
return ((int) this.bytes[offset] & 0xff) |
(((int) this.bytes[offset + 1] & 0xff) << 8) |
(((int) this.bytes[offset + 2] & 0xff) << 16) |
(((int) this.bytes[offset + 3] & 0xff) << 24);
}
public int readUInt8(int offset) {
return this.readInt8(offset);
}
public int readUInt16BE(int offset) {
return this.readInt16BE(offset);
}
public int readUInt16LE(int offset) {
return this.readInt16LE(offset);
}
public int readUInt32BE(int offset) {
return this.readInt32BE(offset);
}
public int readUInt32LE(int offset) {
return this.readInt32LE(offset);
}
public void writeInt8(int value, int offset) {
this.bytes[offset] = (byte) (value & 0xffL);
}
public void writeInt16BE(int value, int offset) {
this.bytes[offset] = (byte) ((value >>> 8L) & 0xffL);
this.bytes[offset + 1] = (byte) (value & 0xffL);
}
public void writeInt16LE(int value, int offset) {
this.bytes[offset] = (byte) (value & 0xffL);
this.bytes[offset + 1] = (byte) ((value >>> 8L) & 0xffL);
}
public void writeInt32BE(int value, int offset) {
this.bytes[offset] = (byte) ((value >>> 24L) & 0xffL);
this.bytes[offset + 1] = (byte) ((value >>> 16L) & 0xffL);
this.bytes[offset + 2] = (byte) ((value >>> 8L) & 0xffL);
this.bytes[offset + 3] = (byte) (value & 0xffL);
}
public void writeInt32LE(int value, int offset) {
this.bytes[offset] = (byte) (value & 0xffL);
this.bytes[offset + 1] = (byte) ((value >>> 8L) & 0xffL);
this.bytes[offset + 2] = (byte) ((value >>> 16L) & 0xffL);
this.bytes[offset + 3] = (byte) ((value >>> 24L) & 0xffL);
}
public void writeUInt8(int value, int offset) {
this.writeInt8(value, offset);
}
public void writeUInt16BE(int value, int offset) {
this.writeInt16BE(value, offset);
}
public void writeUInt16LE(int value, int offset) {
this.writeInt16LE(value, offset);
}
public void writeUInt32BE(int value, int offset) {
this.writeInt32BE(value, offset);
}
public void writeUInt32LE(int value, int offset) {
this.writeInt32LE(value, offset);
}
public Buffer slice(int start, int end) {
int len = end - start;
if (len <= 0) {
return null;
}
ByteBuffer buffer = ByteBuffer.wrap(this.bytes, start, len);
return new Buffer(buffer.array());
}
@NonNull
@Override
public String toString() {
return StandardCharsets.UTF_8.decode(ByteBuffer.wrap(this.bytes)).toString();
}
}

View File

@@ -2,7 +2,6 @@ package org.autojs.autojs.pluginclient;
import android.annotation.SuppressLint;
import android.text.TextUtils;
import android.widget.Toast;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
@@ -37,8 +36,7 @@ import io.reactivex.schedulers.Schedulers;
public class DevPluginResponseHandler implements Handler {
private Router mRouter = new Router.RootRouter("type")
private final Router mRouter = new Router.RootRouter("type")
.handler("command", new Router("command")
.handler("run", data -> {
String script = data.get("script").getAsString();
@@ -80,10 +78,10 @@ public class DevPluginResponseHandler implements Handler {
return true;
}));
private HashMap<String, ScriptExecution> mScriptExecutions = new HashMap<>();
private final HashMap<String, ScriptExecution> mScriptExecutions = new HashMap<>();
private final File mCacheDir;
@SuppressWarnings("ResultOfMethodCallIgnored")
public DevPluginResponseHandler(File cacheDir) {
mCacheDir = cacheDir;
if (cacheDir.exists()) {
@@ -101,14 +99,15 @@ public class DevPluginResponseHandler implements Handler {
return mRouter.handle(data);
}
public Observable<File> handleBytes(JsonObject data, JsonWebSocket.Bytes bytes) {
public Observable<File> handleBytes(JsonObject data, JsonSocket.Bytes bytes) {
String id = data.get("data").getAsJsonObject().get("id").getAsString();
String idMd5 = MD5.md5(id);
return Observable.fromCallable(() -> {
File dir = new File(mCacheDir, idMd5);
Zip.unzip(new ByteArrayInputStream(bytes.byteString.toByteArray()), dir);
return dir;
})
return Observable
.fromCallable(() -> {
File dir = new File(mCacheDir, idMd5);
Zip.unzip(new ByteArrayInputStream(bytes.byteString.toByteArray()), dir);
return dir;
})
.subscribeOn(Schedulers.io());
}
@@ -121,7 +120,6 @@ public class DevPluginResponseHandler implements Handler {
mScriptExecutions.put(viewId, Scripts.INSTANCE.run(new StringScriptSource("[remote]" + name, script)));
}
private void launchProject(String dir) {
try {
new ProjectLauncher(dir)
@@ -132,7 +130,6 @@ public class DevPluginResponseHandler implements Handler {
}
}
private void stopScript(String viewId) {
ScriptExecution execution = mScriptExecutions.get(viewId);
if (execution != null) {
@@ -163,7 +160,7 @@ public class DevPluginResponseHandler implements Handler {
GlobalAppContext.toast(R.string.text_script_save_successfully);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@SuppressLint("CheckResult")
private void saveProject(String name, String dir) {
if (TextUtils.isEmpty(name)) {
@@ -171,19 +168,20 @@ public class DevPluginResponseHandler implements Handler {
}
name = PFiles.getNameWithoutExtension(name);
File toDir = new File(Pref.getScriptDirPath(), name);
Observable.fromCallable(() -> {
copyDir(new File(dir), toDir);
return toDir.getPath();
}).subscribeOn(Schedulers.io())
Observable
.fromCallable(() -> {
copyDir(new File(dir), toDir);
return toDir.getPath();
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(dest ->
GlobalAppContext.toast(R.string.text_project_save_success, dest),
err ->
GlobalAppContext.toast(R.string.text_project_save_error, err.getMessage())
);
.subscribe(dest -> GlobalAppContext.toast(R.string.text_project_save_success, dest),
err -> GlobalAppContext.toast(R.string.text_project_save_error, err.getMessage())
);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private void copyDir(File fromDir, File toDir) throws FileNotFoundException {
toDir.mkdirs();
File[] files = fromDir.listFiles();

View File

@@ -1,34 +1,22 @@
package org.autojs.autojs.pluginclient;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import androidx.annotation.WorkerThread;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.Nullable;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.stardust.app.GlobalAppContext;
import com.stardust.util.MapBuilder;
import org.autojs.autojs.BuildConfig;
import org.autojs.autojs.tool.ThreadTool;
import java.io.File;
import java.net.SocketTimeoutException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.io.IOException;
import java.net.ServerSocket;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import okhttp3.OkHttpClient;
import okhttp3.Request;
/**
* Created by Stardust on 2017/5/11.
@@ -36,11 +24,9 @@ import okhttp3.Request;
public class DevPluginService {
private static final int CLIENT_VERSION = 2;
private static final String LOG_TAG = "DevPluginService";
private static final String TYPE_HELLO = "hello";
private static final String TYPE_BYTES_COMMAND = "bytes_command";
private static final long HANDSHAKE_TIMEOUT = 10 * 1000;
public static DevPluginService getInstance() {
return sInstance;
}
public static class State {
@@ -69,234 +55,121 @@ public class DevPluginService {
}
}
private static final int PORT = 9317;
private static DevPluginService sInstance = new DevPluginService();
private final PublishSubject<State> mConnectionState = PublishSubject.create();
private final DevPluginResponseHandler mResponseHandler;
private final HashMap<String, JsonWebSocket.Bytes> mBytes = new HashMap<>();
private final HashMap<String, JsonObject> mRequiredBytesCommands = new HashMap<>();
private final Handler mHandler = new Handler(Looper.getMainLooper());
private volatile JsonWebSocket mSocket;
public static DevPluginService getInstance() {
return sInstance;
@SuppressWarnings("unused")
public static class Port {
static int PC_CLIENT = 27139;
static int PC_SERVER = 6347;
static int AJ_CLIENT = -1;
static int AJ_SERVER = 9317;
}
@SuppressWarnings("unused")
public static class Version {
static int CLIENT = 2;
static int SERVER = 3;
}
public static final String TYPE_HELLO = "hello";
public static final String TYPE_BYTES_COMMAND = "bytes_command";
public static final int HANDSHAKE_TIMEOUT = JsonSocket.HANDSHAKE_TIMEOUT;
private static final DevPluginService sInstance = new DevPluginService();
public final DevPluginResponseHandler mResponseHandler;
public final Handler mHandler = new Handler(Looper.getMainLooper());
private volatile JsonSocketClient mJsonSocketClient;
private volatile JsonSocketServer mJsonSocketServer;
private volatile ServerSocket mAJServerSocket;
public DevPluginService() {
File cache = new File(GlobalAppContext.get().getCacheDir(), "remote_project");
mResponseHandler = new DevPluginResponseHandler(cache);
}
@AnyThread
public boolean isConnected() {
return mSocket != null && !mSocket.isClosed();
@Nullable
public JsonSocketClient getJsonSocketClient() {
return mJsonSocketClient;
}
@Nullable
public JsonSocketServer getJsonSocketServer() {
return mJsonSocketServer;
}
@AnyThread
public boolean isDisconnected() {
return mSocket == null || mSocket.isClosed();
}
@AnyThread
public void disconnectIfNeeded() {
if (isDisconnected())
return;
disconnect();
}
@AnyThread
public void disconnect() {
mSocket.close();
mSocket = null;
}
public Observable<State> connectionState() {
return mConnectionState;
}
@AnyThread
public Observable<JsonWebSocket> connectToServer(String host) {
int port = PORT;
public Observable<JsonSocketClient> connectToRemoteServer(String host) {
int port = Port.PC_SERVER;
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);
}
mConnectionState.onNext(new State(State.CONNECTING));
return socket(ip, port)
.observeOn(AndroidSchedulers.mainThread())
.doOnError(this::onSocketError);
}
@AnyThread
private Observable<JsonWebSocket> socket(String ip, int port) {
OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS)
.build();
String url = ip + ":" + port;
if (!url.startsWith("ws://") && !url.startsWith("wss://")) {
url = "ws://" + url;
}
return Observable.just(new JsonWebSocket(client, new Request.Builder()
.url(url)
.build()))
.doOnNext(socket -> {
mSocket = socket;
subscribeMessage(socket);
sayHelloToServer(socket);
return Observable
.just(new JsonSocketClient(ip, port))
.observeOn(Schedulers.newThread())
.doOnNext(jsonSocketClient -> {
try {
mJsonSocketClient = jsonSocketClient;
if (ThreadTool.wait(jsonSocketClient::isSocketReady, HANDSHAKE_TIMEOUT)) {
jsonSocketClient
.subscribeMessage()
.monitorMessage()
.sayHello();
} else {
jsonSocketClient.onHandshakeTimeout();
}
} catch (IOException e) {
jsonSocketClient.onSocketError(e);
}
});
}
@SuppressLint("CheckResult")
private void subscribeMessage(JsonWebSocket socket) {
socket.data()
.observeOn(AndroidSchedulers.mainThread())
.doOnComplete(() -> mConnectionState.onNext(new State(State.DISCONNECTED)))
.subscribe(data -> onSocketData(socket, data), this::onSocketError);
socket.bytes()
.doOnComplete(() -> mConnectionState.onNext(new State(State.DISCONNECTED)))
.subscribe(data -> onSocketData(socket, data), this::onSocketError);
}
@MainThread
private void onSocketError(Throwable e) {
e.printStackTrace();
if (mSocket != null) {
mConnectionState.onNext(new State(State.DISCONNECTED, e));
mSocket.close();
mSocket = null;
}
}
@MainThread
private void onSocketData(JsonWebSocket jsonWebSocket, JsonElement element) {
if (!element.isJsonObject()) {
Log.w(LOG_TAG, "onSocketData: not json object: " + element);
return;
}
try {
JsonObject obj = element.getAsJsonObject();
JsonElement typeElement = obj.get("type");
if (typeElement == null || !typeElement.isJsonPrimitive()) {
return;
}
String type = typeElement.getAsString();
if (type.equals(TYPE_HELLO)) {
onServerHello(jsonWebSocket, obj);
return;
}
if (TYPE_BYTES_COMMAND.equals(type)) {
String md5 = obj.get("md5").getAsString();
JsonWebSocket.Bytes bytes = mBytes.remove(md5);
if (bytes != null) {
handleBytes(obj, bytes);
} else {
mRequiredBytesCommands.put(md5, obj);
}
return;
}
mResponseHandler.handle(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
@SuppressLint("CheckResult")
private void handleBytes(JsonObject obj, JsonWebSocket.Bytes bytes) {
mResponseHandler.handleBytes(obj, bytes)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(dir -> {
obj.get("data").getAsJsonObject().add("dir", new JsonPrimitive(dir.getPath()));
mResponseHandler.handle(obj);
@AnyThread
public Observable<JsonSocketServer> enableLocalServer() {
return Observable
.just(new JsonSocketServer(Port.AJ_SERVER))
.observeOn(Schedulers.newThread())
.doOnNext(jsonSocketServer -> {
try {
mJsonSocketServer = jsonSocketServer;
mAJServerSocket = jsonSocketServer.getServerSocket();
if (mAJServerSocket != null) {
jsonSocketServer
.setStateConnected()
.setSocket(mAJServerSocket.accept())
.subscribeMessage()
.monitorMessage()
.sayHello();
} else {
jsonSocketServer.onHandshakeTimeout();
}
} catch (IOException e) {
jsonSocketServer.onSocketError(e);
}
});
}
@WorkerThread
private void onSocketData(JsonWebSocket jsonWebSocket, JsonWebSocket.Bytes bytes) {
JsonObject command = mRequiredBytesCommands.remove(bytes.md5);
if (command != null) {
handleBytes(command, bytes);
} else {
mBytes.put(bytes.md5, bytes);
public static void setState(PublishSubject<State> cxn, int state) {
cxn.onNext(new State(state));
}
public static void setState(PublishSubject<State> cxn, int state, Throwable e) {
cxn.onNext(new State(state, e));
}
@AnyThread
// FIXME by SuperMonster003 on Dec 29, 2021
// ! Would print double (may be even more times) the amount of
// ! messages on VSCode when multi connection were established.
public void print(String log) {
if (mJsonSocketClient != null) {
mJsonSocketClient.writeLog(log);
}
if (mJsonSocketServer != null) {
mJsonSocketServer.writeLog(log);
}
}
@WorkerThread
private void sayHelloToServer(JsonWebSocket socket) {
writeMap(socket, TYPE_HELLO, new MapBuilder<String, Object>()
.put("device_name", Build.BRAND + " " + Build.MODEL)
.put("client_version", CLIENT_VERSION)
.put("app_version", BuildConfig.VERSION_NAME)
.put("app_version_code", BuildConfig.VERSION_CODE)
.build());
mHandler.postDelayed(() -> {
if (mSocket != socket && !socket.isClosed()) {
onHandshakeTimeout(socket);
}
}, HANDSHAKE_TIMEOUT);
}
@MainThread
private void onHandshakeTimeout(JsonWebSocket socket) {
Log.i(LOG_TAG, "onHandshakeTimeout");
mConnectionState.onNext(new State(State.DISCONNECTED, new SocketTimeoutException("handshake timeout")));
socket.close();
}
@MainThread
private void onServerHello(JsonWebSocket jsonWebSocket, JsonObject message) {
Log.i(LOG_TAG, "onServerHello: " + message);
mSocket = jsonWebSocket;
mConnectionState.onNext(new State(State.CONNECTED));
}
@AnyThread
private static boolean write(JsonWebSocket socket, String type, JsonObject data) {
JsonObject json = new JsonObject();
json.addProperty("type", type);
json.add("data", data);
return socket.write(json);
}
@AnyThread
private static boolean writePair(JsonWebSocket socket, String type, Pair<String, String> pair) {
JsonObject data = new JsonObject();
data.addProperty(pair.first, pair.second);
return write(socket, type, data);
}
@AnyThread
private static boolean writeMap(JsonWebSocket socket, String type, Map<String, ?> map) {
JsonObject data = new JsonObject();
for (Map.Entry<String, ?> entry : map.entrySet()) {
Object value = entry.getValue();
if (value instanceof String) {
data.addProperty(entry.getKey(), (String) value);
} else if (value instanceof Character) {
data.addProperty(entry.getKey(), (Character) value);
} else if (value instanceof Number) {
data.addProperty(entry.getKey(), (Number) value);
} else if (value instanceof Boolean) {
data.addProperty(entry.getKey(), (Boolean) value);
} else if (value instanceof JsonElement) {
data.add(entry.getKey(), (JsonElement) value);
} else {
throw new IllegalArgumentException("cannot put value " + value + " into json");
}
}
return write(socket, type, data);
}
@SuppressLint("CheckResult")
@AnyThread
public void log(String log) {
if (!isConnected())
return;
writePair(mSocket, "log", new Pair<>("log", log));
}
}
}

View File

@@ -0,0 +1,283 @@
package org.autojs.autojs.pluginclient;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.stream.JsonReader;
import com.stardust.app.GlobalAppContext;
import com.stardust.autojs.runtime.api.Device;
import com.stardust.util.MapBuilder;
import org.autojs.autojs.BuildConfig;
import org.autojs.autojs.tool.IOTool;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.subjects.PublishSubject;
import okio.ByteString;
abstract public class JsonSocket extends Socket {
private final String TAG = "JsonSocket";
public static final int HEADER_SIZE = 8;
public static final int HANDSHAKE_TIMEOUT = 5 * 1000;
public static final String TYPE_HELLO = DevPluginService.TYPE_HELLO;
public static final String TYPE_BYTES_COMMAND = DevPluginService.TYPE_BYTES_COMMAND;
public static class Bytes {
public final String md5;
public final ByteString byteString;
public final long timestamp;
public Bytes(String md5, ByteString byteString) {
this.md5 = md5;
this.byteString = byteString;
this.timestamp = System.currentTimeMillis();
}
}
@SuppressWarnings("unused")
public static class Type {
public static int TEXT = 1;
public static int BINARY = 2;
public static int GZIP_TEXT = 3;
public static int GZIP_BINARY = 4;
}
public final android.os.Handler mHandler = new Handler(Looper.getMainLooper());
public final DevPluginService devPlugin = DevPluginService.getInstance();
public abstract void switchOff() throws IOException;
public abstract boolean isSocketReady();
public abstract Socket getSocket();
public abstract JsonSocket setSocket(Socket socket);
public abstract JsonSocket monitorMessage();
public abstract JsonSocket subscribeMessage();
public abstract JsonSocket setStateConnected();
public abstract PublishSubject<JsonElement> getJsonElementPublishSubject();
public abstract PublishSubject<Bytes> getBytesPublishSubject();
public void sayHello() {
writeMap(TYPE_HELLO, new MapBuilder<String, Object>()
.put("device_name", Build.BRAND + " " + Build.MODEL)
.put("app_version", BuildConfig.VERSION_NAME)
.put("app_version_code", BuildConfig.VERSION_CODE)
.put("server_version", DevPluginService.Version.SERVER)
.put("device_id", new Device(GlobalAppContext.get()).getAndroidId())
.build());
}
public void onMessage(JsonSocket jsonSocket, String text) {
Log.d(TAG, "onMessage: text = " + text);
dispatchJson(jsonSocket, text);
}
public void onMessage(JsonSocket jsonSocket, ByteString bytes) {
Log.d(TAG, "onMessage: ByteString = " + bytes.toString());
jsonSocket.getBytesPublishSubject().onNext(new Bytes(bytes.md5().hex(), bytes));
}
private void onMessageDispatch(JsonSocket jsonSocket, String str) throws IOException {
Log.d(TAG, "Input total str: " + str);
Log.d(TAG, "Input total length: " + str.length());
String header = str.substring(0, HEADER_SIZE);
Log.d(TAG, "Input data length: " + new Buffer(header.getBytes()).readInt32BE(0));
Log.d(TAG, "Input data type: " + new Buffer(header.getBytes()).readInt32BE(4));
String message = str.substring(HEADER_SIZE);
Log.d(TAG, "Input message length: " + message.length());
Log.d(TAG, "Input message: " + message);
// Log.d(TAG, "Input message gunzip: " + gunzip(str));
onMessage(jsonSocket, message);
}
@SuppressWarnings("SameParameterValue")
private void writeMap(String type, Map<String, ?> map) {
JsonObject data = new JsonObject();
for (Map.Entry<String, ?> entry : map.entrySet()) {
Object value = entry.getValue();
if (value instanceof String) {
data.addProperty(entry.getKey(), (String) value);
} else if (value instanceof Character) {
data.addProperty(entry.getKey(), (Character) value);
} else if (value instanceof Number) {
data.addProperty(entry.getKey(), (Number) value);
} else if (value instanceof Boolean) {
data.addProperty(entry.getKey(), (Boolean) value);
} else if (value instanceof JsonElement) {
data.add(entry.getKey(), (JsonElement) value);
} else {
throw new IllegalArgumentException("cannot put value " + value + " into json");
}
}
writeData(type, data);
}
@SuppressWarnings("SameParameterValue")
@AnyThread
public void writePair(String type, Pair<String, String> pair) {
JsonObject data = new JsonObject();
data.addProperty(pair.first, pair.second);
writeData(type, data);
}
public void writeLog(String log) {
if (isSocketReady()) {
writePair("log", new Pair<>("log", log));
}
}
private void writeData(String type, JsonObject data) {
JsonObject json = new JsonObject();
json.addProperty("type", type);
json.add("data", data);
writeMessage(json);
}
private void writeMessage(JsonElement element) {
String json = element.toString();
Log.d(TAG, "writeMessage: length = " + json.length() + ", json = " + element);
try {
writeMessageWithType(getSocket(), json, Type.TEXT);
} catch (IOException e) {
e.printStackTrace();
}
}
private void writeMessageWithType(Socket socket, String message, int messageType) throws IOException {
if (socket != null) {
byte[] jsonBytes = getJsonBytes(message);
byte[] headerBytes = getHeaderBytes(new int[]{jsonBytes.length, messageType});
OutputStream os = socket.getOutputStream();
BufferedOutputStream writer = new BufferedOutputStream(os);
writer.write(headerBytes);
writer.write(jsonBytes);
writer.flush();
}
}
public void monitorMessage(Socket socket, JsonSocket jsonSocket) {
new Thread(() -> {
InputStream inputStream = null;
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
try {
inputStream = socket.getInputStream();
inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(inputStreamReader);
final StringBuilder stringBuilder = new StringBuilder();
String readLine;
Log.d(TAG, "bufferedReader is reading lines...");
while ((readLine = bufferedReader.readLine()) != null && !socket.isClosed()) {
Log.d(TAG, "Reading line...");
stringBuilder.append(readLine);
Log.d(TAG, "read line length: " + stringBuilder.toString().length());
onMessageDispatch(jsonSocket, stringBuilder.toString());
stringBuilder.setLength(0);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
IOTool.close(bufferedReader);
IOTool.close(inputStreamReader);
IOTool.close(inputStream);
try {
jsonSocket.switchOff();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
public void setState(PublishSubject<DevPluginService.State> cxn, int state) {
cxn.onNext(new DevPluginService.State(state));
}
public void setState(PublishSubject<DevPluginService.State> cxn, int state, Throwable e) {
cxn.onNext(new DevPluginService.State(state, e));
}
private void dispatchJson(@NonNull JsonSocket jsonSocket, String json) {
try {
Log.d(TAG, "JSON to parse: " + json);
JsonReader reader = new JsonReader(new StringReader(json));
reader.setLenient(true);
JsonElement element = JsonParser.parseReader(reader);
jsonSocket.getJsonElementPublishSubject().onNext(element);
} catch (JsonParseException e) {
e.printStackTrace();
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@SuppressLint("CheckResult")
public void handleBytes(JsonObject jsonObject, JsonSocket.Bytes bytes) {
devPlugin.mResponseHandler
.handleBytes(jsonObject, bytes)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(dir -> {
jsonObject
.get("data")
.getAsJsonObject()
.add("dir", new JsonPrimitive(dir.getPath()));
devPlugin.mResponseHandler.handle(jsonObject);
});
}
private byte[] getJsonBytes(@NonNull String json) {
return json.getBytes(StandardCharsets.UTF_8);
}
@NonNull
private byte[] getHeaderBytes(@NonNull int[] data) {
// byte order is big endian
// use Buffer#readInt32BE for a socket server in Node.js
ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES * data.length);
for (int i : data) {
// int, 4 bytes
buffer.putInt(i);
}
return buffer.array();
}
}

View File

@@ -0,0 +1,205 @@
package org.autojs.autojs.pluginclient;
import android.annotation.SuppressLint;
import android.util.Log;
import androidx.annotation.MainThread;
import androidx.annotation.WorkerThread;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.net.Socket;
import java.util.HashMap;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.subjects.PublishSubject;
public class JsonSocketClient extends JsonSocket {
private static final String TAG = JsonSocketClient.class.getSimpleName();
public static final PublishSubject<DevPluginService.State> cxnState = PublishSubject.create();
private final PublishSubject<JsonElement> mJsonElementPublishSubject = PublishSubject.create();
private final PublishSubject<Bytes> mBytesPublishSubject = PublishSubject.create();
private final HashMap<String, Bytes> mBytes = new HashMap<>();
private final HashMap<String, JsonObject> mRequiredBytesCommands = new HashMap<>();
private Socket mSocket;
// @Constructor
public JsonSocketClient(String host, int port) {
new Thread(() -> {
try {
setStateConnecting();
mSocket = new Socket(host, port);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
public boolean isSocketReady() {
return mSocket != null && mSocket.isConnected();
}
@Override
public Socket getSocket() {
return mSocket;
}
@Override
public JsonSocket setSocket(Socket socket) {
mSocket = socket;
return this;
}
@Override
public PublishSubject<JsonElement> getJsonElementPublishSubject() {
return mJsonElementPublishSubject;
}
@Override
public PublishSubject<Bytes> getBytesPublishSubject() {
return mBytesPublishSubject;
}
@Override
public void switchOff() throws IOException {
close();
setStateDisconnected();
}
public void close() throws IOException {
Log.w(TAG, "closing socket...");
mJsonElementPublishSubject.onComplete();
if (mSocket != null) {
mSocket.close();
mSocket = null;
}
}
@Override
public void sayHello() {
super.sayHello();
mHandler.postDelayed(() -> {
if (!isSocketReady()) {
try {
onHandshakeTimeout();
} catch (IOException e) {
e.printStackTrace();
}
}
}, HANDSHAKE_TIMEOUT);
}
private void onHello(JsonObject message) {
Log.i(TAG, "onHello: " + message);
setStateConnected();
}
@MainThread
private void onSocketData(JsonElement element) {
Log.d(TAG, "onSocketData...");
try {
if (!element.isJsonObject()) {
onSocketError(new Error("Not a JSON object"));
return;
}
JsonObject obj = element.getAsJsonObject();
JsonElement typeElement = obj.get("type");
if (typeElement == null || !typeElement.isJsonPrimitive()) {
return;
}
String type = typeElement.getAsString();
Log.d(TAG, "json type: " + type);
switch (type) {
case TYPE_HELLO -> onHello(obj);
case TYPE_BYTES_COMMAND -> {
String md5 = obj.get("md5").getAsString();
JsonSocket.Bytes bytes = mBytes.remove(md5);
if (bytes != null) {
handleBytes(obj, bytes);
} else {
mRequiredBytesCommands.put(md5, obj);
}
}
default -> devPlugin.mResponseHandler.handle(obj);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@WorkerThread
private void onSocketData(JsonSocket.Bytes bytes) {
Log.d(TAG, "onSocketData bytes");
JsonObject command = mRequiredBytesCommands.remove(bytes.md5);
if (command != null) {
handleBytes(command, bytes);
} else {
mBytes.put(bytes.md5, bytes);
}
}
@MainThread
public void onSocketError(Throwable e) throws IOException {
Log.w(TAG, "onSocketError");
e.printStackTrace();
setStateDisconnected(e);
close();
}
@MainThread
public void onHandshakeTimeout() throws IOException {
Log.i(TAG, "onHandshakeTimeout");
// setStateDisconnected(new SocketTimeoutException("handshake timeout"));
setStateDisconnected();
close();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@SuppressLint("CheckResult")
@Override
public JsonSocket subscribeMessage() {
mJsonElementPublishSubject
.observeOn(AndroidSchedulers.mainThread())
.doOnComplete(this::setStateDisconnected)
.subscribe(this::onSocketData, this::onSocketError);
mBytesPublishSubject
.doOnComplete(this::setStateDisconnected)
.subscribe(this::onSocketData, this::onSocketError);
return this;
}
public JsonSocket monitorMessage() {
super.monitorMessage(mSocket, this);
return this;
}
public JsonSocket setStateConnected() {
setState(cxnState, DevPluginService.State.CONNECTED);
return this;
}
public JsonSocket setStateConnecting() {
setState(cxnState, DevPluginService.State.CONNECTING);
return this;
}
public JsonSocket setStateDisconnected() {
setState(cxnState, DevPluginService.State.DISCONNECTED);
return this;
}
public JsonSocket setStateDisconnected(Throwable e) {
setState(cxnState, DevPluginService.State.DISCONNECTED, e);
return this;
}
}

View File

@@ -0,0 +1,212 @@
package org.autojs.autojs.pluginclient;
import android.annotation.SuppressLint;
import android.util.Log;
import androidx.annotation.MainThread;
import androidx.annotation.WorkerThread;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.subjects.PublishSubject;
public class JsonSocketServer extends JsonSocket {
private static final String TAG = JsonSocketServer.class.getSimpleName();
public static final PublishSubject<DevPluginService.State> cxnState = PublishSubject.create();
private final PublishSubject<JsonElement> mJsonElementPublishSubject = PublishSubject.create();
private final PublishSubject<Bytes> mBytesPublishSubject = PublishSubject.create();
private final HashMap<String, Bytes> mBytes = new HashMap<>();
private final HashMap<String, JsonObject> mRequiredBytesCommands = new HashMap<>();
private Socket mSocket;
private ServerSocket mServerSocket;
// @Constructor
public JsonSocketServer(int port) {
try {
setStateConnecting();
mServerSocket = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@SuppressLint("CheckResult")
@Override
public JsonSocket subscribeMessage() {
mJsonElementPublishSubject
.observeOn(AndroidSchedulers.mainThread())
.doOnComplete(this::setStateDisconnected)
.subscribe(this::onSocketData, this::onSocketError);
mBytesPublishSubject
.doOnComplete(this::setStateDisconnected)
.subscribe(this::onSocketData, this::onSocketError);
return this;
}
public boolean isSocketReady() {
return mSocket != null && !mSocket.isClosed();
}
public boolean isServerSocketReady() {
return mServerSocket != null && !mServerSocket.isClosed();
}
@Override
public Socket getSocket() {
return mSocket;
}
public ServerSocket getServerSocket() {
return mServerSocket;
}
@Override
public JsonSocket setSocket(Socket socket) {
mSocket = socket;
return this;
}
@Override
public PublishSubject<JsonElement> getJsonElementPublishSubject() {
return mJsonElementPublishSubject;
}
@Override
public PublishSubject<Bytes> getBytesPublishSubject() {
return mBytesPublishSubject;
}
@Override
public void switchOff() throws IOException {
if (isServerSocketReady()) {
mServerSocket.close();
mServerSocket = null;
}
close();
setStateDisconnected();
}
public void close() throws IOException {
if (isSocketReady()) {
mSocket.close();
mSocket = null;
}
}
@Override
public void sayHello() {
super.sayHello();
mHandler.postDelayed(() -> {
if (!isServerSocketReady()) {
try {
onHandshakeTimeout();
} catch (IOException e) {
e.printStackTrace();
}
}
}, HANDSHAKE_TIMEOUT);
}
public JsonSocket monitorMessage() {
super.monitorMessage(mSocket, this);
return this;
}
@MainThread
private void onSocketData(JsonElement element) {
Log.d(TAG, "onSocketData...");
try {
if (!element.isJsonObject()) {
onSocketError(new Error("Not a JSON object"));
return;
}
JsonObject obj = element.getAsJsonObject();
JsonElement typeElement = obj.get("type");
if (typeElement == null || !typeElement.isJsonPrimitive()) {
return;
}
String type = typeElement.getAsString();
Log.d(TAG, "json type: " + type);
switch (type) {
case TYPE_HELLO -> setStateConnected();
case TYPE_BYTES_COMMAND -> {
String md5 = obj.get("md5").getAsString();
Bytes bytes = mBytes.remove(md5);
if (bytes != null) {
handleBytes(obj, bytes);
} else {
mRequiredBytesCommands.put(md5, obj);
}
}
default -> devPlugin.mResponseHandler.handle(obj);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@WorkerThread
private void onSocketData(Bytes bytes) {
Log.d(TAG, "onSocketData bytes");
JsonObject command = mRequiredBytesCommands.remove(bytes.md5);
if (command != null) {
handleBytes(command, bytes);
} else {
mBytes.put(bytes.md5, bytes);
}
}
@MainThread
public void onSocketError(Throwable e) throws IOException {
e.printStackTrace();
if (isServerSocketReady()) {
setStateDisconnected(e);
switchOff();
}
}
@MainThread
public void onHandshakeTimeout() throws IOException {
Log.i(TAG, "onHandshakeTimeout");
// setStateDisconnected(new SocketTimeoutException("handshake timeout"));
setStateDisconnected();
switchOff();
}
public JsonSocket setStateConnected() {
setState(cxnState, DevPluginService.State.CONNECTED);
return this;
}
public JsonSocket setStateConnecting() {
setState(cxnState, DevPluginService.State.CONNECTING);
return this;
}
public JsonSocket setStateDisconnected() {
setState(cxnState, DevPluginService.State.DISCONNECTED);
return this;
}
public JsonSocket setStateDisconnected(Throwable e) {
setState(cxnState, DevPluginService.State.DISCONNECTED, e);
return this;
}
}

View File

@@ -1,125 +0,0 @@
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 com.google.gson.stream.JsonReader;
import java.io.StringReader;
import androidx.annotation.Nullable;
import io.reactivex.Observable;
import io.reactivex.subjects.PublishSubject;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
public class JsonWebSocket extends WebSocketListener {
public static class Bytes {
public final String md5;
public final ByteString byteString;
public final long timestamp;
public Bytes(String md5, ByteString byteString) {
this.md5 = md5;
this.byteString = byteString;
this.timestamp = System.currentTimeMillis();
}
}
private static final String LOG_TAG = "JsonWebSocket";
private final WebSocket mWebSocket;
private final JsonParser mJsonParser = new JsonParser();
private final PublishSubject<JsonElement> mJsonElementPublishSubject = PublishSubject.create();
private final PublishSubject<Bytes> mBytesPublishSubject = PublishSubject.create();
private volatile boolean mClosed = false;
public JsonWebSocket(OkHttpClient client, Request request) {
mWebSocket = client.newWebSocket(request, this);
}
@Override
public void onMessage(WebSocket webSocket, String text) {
Log.d(LOG_TAG, "onMessage: text = " + text);
dispatchJson(text);
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
Log.d(LOG_TAG, "onMessage: ByteString = " + bytes.toString());
mBytesPublishSubject.onNext(new Bytes(bytes.md5().hex(), bytes));
}
public Observable<JsonElement> data() {
return mJsonElementPublishSubject;
}
public Observable<Bytes> bytes(){
return mBytesPublishSubject;
}
public boolean write(JsonElement element) {
String json = element.toString();
Log.d(LOG_TAG, "write: length = " + json.length() + ", json = " + element);
return mWebSocket.send(json);
}
public void close() {
mJsonElementPublishSubject.onComplete();
mClosed = true;
mWebSocket.close(1000, "close");
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
Log.d(LOG_TAG, "onFailure: code = " + code + ", reason = " + reason);
close();
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) {
Log.d(LOG_TAG, "onFailure: response = " + response, t);
close(t);
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
Log.d(LOG_TAG, "onOpen: response = " + response);
}
private void close(Throwable e) {
if (mClosed) {
return;
}
mJsonElementPublishSubject.onError(e);
mClosed = true;
mWebSocket.close(1011, "remote exception: " + e.getMessage());
}
private void dispatchJson(String json) {
try {
JsonReader reader = new JsonReader(new StringReader(json));
reader.setLenient(true);
JsonElement element = mJsonParser.parse(reader);
mJsonElementPublishSubject.onNext(element);
} catch (JsonParseException e) {
e.printStackTrace();
}
}
public boolean isClosed() {
return mClosed;
}
}

View File

@@ -0,0 +1,81 @@
package org.autojs.autojs.tool;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class IOTool {
private static final String TAG = IOTool.class.getSimpleName();
public static void close(Closeable io) {
try {
if (io != null) {
io.close();
}
} catch (IOException e) {
Log.w(TAG, "ex: " + e);
}
}
public static void close(Closeable io, boolean exceptionMatters) throws IOException {
try {
if (io != null) {
io.close();
}
} catch (IOException e) {
if (exceptionMatters) {
throw e;
}
}
}
public static byte[] gzip(String str) {
ByteArrayOutputStream out = null;
GZIPOutputStream gzip = null;
try {
out = new ByteArrayOutputStream();
gzip = new GZIPOutputStream(out);
gzip.write(str.getBytes(StandardCharsets.UTF_8));
gzip.finish();
return out.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
close(out);
close(gzip);
}
return null;
}
public static String gunzip(byte[] bytes) {
ByteArrayOutputStream out = null;
GZIPInputStream gzip = null;
try {
out = new ByteArrayOutputStream();
gzip = new GZIPInputStream(new ByteArrayInputStream(bytes));
int res;
byte[] buf = new byte[1024];
while ((res = gzip.read(buf)) != -1) {
out.write(buf, 0, res);
}
out.flush();
return out.toString(String.valueOf(StandardCharsets.UTF_8));
} catch (Exception e) {
e.printStackTrace();
} finally {
close(out);
close(gzip);
}
return "";
}
}

View File

@@ -0,0 +1,32 @@
package org.autojs.autojs.tool;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
public class ThreadTool {
public static boolean wait(Supplier<Boolean> condition, int timeout) throws InterruptedException {
AtomicBoolean result = new AtomicBoolean(false);
Thread thread = new Thread(() -> {
while (!condition.get()) {
try {
//noinspection BusyWait
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
result.set(true);
});
thread.start();
thread.join(timeout);
if (thread.isAlive()) {
thread.interrupt();
}
return result.get();
}
public static boolean wait(Supplier<Boolean> condition) throws InterruptedException {
return wait(condition, 10 * 1000);
}
}

View File

@@ -5,6 +5,7 @@ import android.content.Context;
import android.net.Uri;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar;
import android.util.AttributeSet;
import android.webkit.ValueCallback;
@@ -96,7 +97,7 @@ public class CommunityWebView extends EWebView {
Scripts.INSTANCE.run(file);
}, error -> {
error.printStackTrace();
Snackbar.make(CommunityWebView.this, R.string.text_download_failed, Toast.LENGTH_SHORT).show();
Snackbar.make(CommunityWebView.this, R.string.text_download_failed, BaseTransientBottomBar.LENGTH_SHORT).show();
});
}

View File

@@ -37,6 +37,8 @@ import org.autojs.autojs.Pref;
import org.autojs.autojs.R;
import org.autojs.autojs.external.foreground.ForegroundService;
import org.autojs.autojs.pluginclient.DevPluginService;
import org.autojs.autojs.pluginclient.JsonSocketClient;
import org.autojs.autojs.pluginclient.JsonSocketServer;
import org.autojs.autojs.tool.AccessibilityServiceTool;
import org.autojs.autojs.tool.Observers;
import org.autojs.autojs.tool.RootTool;
@@ -49,6 +51,7 @@ import org.autojs.autojs.ui.settings.SettingsActivity;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
@@ -58,10 +61,9 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
/**
* Created by Stardust on Jan 30, 2017.
* Modified by SuperMonster003 on Nov 16, 2021.
* Modified by SuperMonster003 as of Nov 16, 2021.
*/
@SuppressLint("NonConstantResourceId")
@SuppressWarnings("ResultOfMethodCallIgnored")
@@ -79,6 +81,11 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
private final DrawerMenuItem mAccessibilityServiceItem = new DrawerMenuItem(R.drawable.ic_accessibility_black_48dp, R.string.text_accessibility_service, 0, this::enableOrDisableAccessibilityService);
private final DrawerMenuItem mForegroundServiceItem = new DrawerMenuItem(R.drawable.ic_service_green, R.string.text_foreground_service, R.string.key_foreground_service, this::toggleForegroundService);
private final DrawerMenuItem mFloatingWindowItem = new DrawerMenuItem(R.drawable.ic_robot_64, R.string.text_floating_window, 0, this::showOrDismissFloatingWindow);
private final DrawerMenuItem mClientModeItem = new DrawerMenuItem(R.drawable.ic_computer_black_48dp, R.string.text_client_mode, 0, this::toggleRemoteServerCxn);
private final DrawerMenuItem mServerModeItem = new DrawerMenuItem(R.drawable.ic_smartphone_black_48dp, R.string.text_server_mode, 0, this::toggleLocalServerCxn);
private final DrawerMenuItem mNotificationPermissionItem = new DrawerMenuItem(R.drawable.ic_ali_notification, R.string.text_notification_permission, 0, this::goToNotificationServiceSettings);
private final DrawerMenuItem mUsageStatsPermissionItem = new DrawerMenuItem(R.drawable.ic_assessment_black_48dp, R.string.text_usage_stats_permission, 0, this::goToUsageStatsSettings);
private final DrawerMenuItem mIgnoreBatteryOptimizationsItem = new DrawerMenuItem(R.drawable.ic_battery_std_black_48dp, R.string.text_ignore_battery_optimizations, 0, this::toggleIgnoreBatteryOptimizations);
@@ -86,36 +93,55 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
private final DrawerMenuItem mWriteSystemSettingsItem = new DrawerMenuItem(R.drawable.ic_settings_black_48dp, R.string.text_write_system_settings, 0, this::goToWriteSystemSettings);
private final DrawerMenuItem mWriteSecuritySettingsItem = new DrawerMenuItem(R.drawable.ic_security_black_48dp, R.string.text_write_secure_settings, 0, this::toggleWriteSecureSettings);
private final DrawerMenuItem mFloatingWindowItem = new DrawerMenuItem(R.drawable.ic_robot_64, R.string.text_floating_window, 0, this::showOrDismissFloatingWindow);
private final DrawerMenuItem mConnectionItem = new DrawerMenuItem(R.drawable.ic_computer_black_48dp, R.string.debug, 0, this::connectOrDisconnectToRemote);
private final DevPluginService devPlugin = DevPluginService.getInstance();
private DrawerMenuAdapter mDrawerMenuAdapter;
private Disposable mConnectionStateDisposable;
private Disposable mClientConnectionStateDisposable;
private Disposable mServerConnectionStateDisposable;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mConnectionStateDisposable = DevPluginService.getInstance().connectionState()
mClientConnectionStateDisposable = JsonSocketClient.cxnState
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> {
setChecked(mConnectionItem, state.getState() == DevPluginService.State.CONNECTED);
setProgress(mConnectionItem, state.getState() == DevPluginService.State.CONNECTING);
if (state.getException() != null) {
showMessage(state.getException().getMessage());
}
});
.subscribe(state -> setItemState(mClientModeItem, state));
mServerConnectionStateDisposable = JsonSocketServer.cxnState
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> setItemState(mServerModeItem, state));
EventBus.getDefault().register(this);
}
@Override
public void onResume() {
super.onResume();
syncSwitchState();
}
@Override
public void onDestroy() {
super.onDestroy();
mClientConnectionStateDisposable.dispose();
mServerConnectionStateDisposable.dispose();
EventBus.getDefault().unregister(this);
}
@AfterViews
void setUpViews() {
public void setUpViews() {
ThemeColorManager.addViewBackground(mHeaderView);
initMenuItems();
if (Pref.isFloatingMenuShown()) {
FloatyWindowManger.showCircularMenuIfNeeded();
setChecked(mFloatingWindowItem, true);
}
setChecked(mConnectionItem, DevPluginService.getInstance().isConnected());
JsonSocketClient jsonSocketClient = devPlugin.getJsonSocketClient();
if (jsonSocketClient != null) {
setChecked(mClientModeItem, jsonSocketClient.isConnected());
}
if (Pref.isForegroundServiceEnabled()) {
ForegroundService.start(GlobalAppContext.get());
setChecked(mForegroundServiceItem, true);
@@ -128,6 +154,13 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
mAccessibilityServiceItem,
mForegroundServiceItem,
new DrawerMenuGroup(R.string.text_tools),
mFloatingWindowItem,
new DrawerMenuGroup(R.string.text_connect_to_pc),
mClientModeItem,
mServerModeItem,
new DrawerMenuGroup(R.string.text_permission),
mNotificationPermissionItem,
mUsageStatsPermissionItem,
@@ -136,10 +169,6 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
mWriteSystemSettingsItem,
mWriteSecuritySettingsItem,
new DrawerMenuGroup(R.string.text_tools),
mFloatingWindowItem,
mConnectionItem,
new DrawerMenuGroup(R.string.text_appearance),
new DrawerMenuItem(R.drawable.ic_night_mode, R.string.text_night_mode, R.string.key_night_mode, this::toggleNightMode),
new DrawerMenuItem(R.drawable.ic_personalize, R.string.text_theme_color, this::openThemeColorSettings)
@@ -148,8 +177,7 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
mDrawerMenu.setLayoutManager(new LinearLayoutManager(getContext()));
}
void enableOrDisableAccessibilityService(DrawerMenuItemViewHolder holder) {
public void enableOrDisableAccessibilityService(DrawerMenuItemViewHolder holder) {
boolean isAccessibilityServiceEnabled = isAccessibilityServiceEnabled();
boolean checked = holder.getSwitchCompat().isChecked();
if (checked && !isAccessibilityServiceEnabled) {
@@ -159,7 +187,7 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
}
}
void goToNotificationServiceSettings(DrawerMenuItemViewHolder holder) {
public void goToNotificationServiceSettings(DrawerMenuItemViewHolder holder) {
boolean enabled = NotificationListenerService.Companion.getInstance() != null;
boolean checked = holder.getSwitchCompat().isChecked();
if ((checked && !enabled) || (!checked && enabled)) {
@@ -167,7 +195,7 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
}
}
void goToUsageStatsSettings(DrawerMenuItemViewHolder holder) {
public void goToUsageStatsSettings(DrawerMenuItemViewHolder holder) {
Context context = getContext();
boolean enabled = false;
if (context != null) {
@@ -196,7 +224,7 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
.show();
}
void showOrDismissFloatingWindow(DrawerMenuItemViewHolder holder) {
public void showOrDismissFloatingWindow(DrawerMenuItemViewHolder holder) {
boolean isFloatingWindowShowing = FloatyWindowManger.isCircularMenuShowing();
boolean checked = holder.getSwitchCompat().isChecked();
if (getActivity() != null && !getActivity().isFinishing()) {
@@ -211,7 +239,7 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
}
@SuppressLint("BatteryLife")
void toggleIgnoreBatteryOptimizations(DrawerMenuItemViewHolder holder) {
public void toggleIgnoreBatteryOptimizations(DrawerMenuItemViewHolder holder) {
Context context = getContext();
try {
Intent intent = new Intent();
@@ -235,15 +263,15 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
}
}
void openThemeColorSettings(DrawerMenuItemViewHolder holder) {
public void openThemeColorSettings(DrawerMenuItemViewHolder holder) {
SettingsActivity.selectThemeColor(getActivity());
}
void toggleNightMode(DrawerMenuItemViewHolder holder) {
public void toggleNightMode(DrawerMenuItemViewHolder holder) {
((BaseActivity) requireActivity()).setNightModeEnabled(holder.getSwitchCompat().isChecked());
}
void goToDisplayOverOtherAppsSettings(DrawerMenuItemViewHolder holder) {
public void goToDisplayOverOtherAppsSettings(DrawerMenuItemViewHolder holder) {
boolean checked = holder.getSwitchCompat().isChecked();
Context context = getContext();
if (checked != FloatingPermission.canDrawOverlays(context)) {
@@ -251,7 +279,7 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
}
}
void goToWriteSystemSettings(DrawerMenuItemViewHolder holder) {
public void goToWriteSystemSettings(DrawerMenuItemViewHolder holder) {
boolean checked = holder.getSwitchCompat().isChecked();
if (checked != Settings.System.canWrite(getContext())) {
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
@@ -331,7 +359,7 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
if (context == null) {
return;
}
final int SNACKBAR_DURATION = 1000;
final int SNACK_BAR_DURATION = 1000;
String scriptAction = state ? "grant" : "revoke";
String script = "adb shell pm " + scriptAction + " " + context.getPackageName() + " " + WRITE_SECURE_SETTINGS_PERMISSION;
@@ -343,7 +371,7 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
View view = dialog.getView();
int resultRes = hasWriteSecureSettingsAccess() ? R.string.text_granted : R.string.text_not_granted;
if (view != null) {
Snackbar.make(view, resultRes, SNACKBAR_DURATION).show();
Snackbar.make(view, resultRes, SNACK_BAR_DURATION).show();
} else {
Toast.makeText(context, resultRes, Toast.LENGTH_SHORT).show();
}
@@ -356,7 +384,7 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
View view = dialog.getView();
int textRes = R.string.text_command_already_copied_to_clip;
if (view != null) {
Snackbar.make(view, textRes, SNACKBAR_DURATION).show();
Snackbar.make(view, textRes, SNACK_BAR_DURATION).show();
} else {
Toast.makeText(context, textRes, Toast.LENGTH_SHORT).show();
}
@@ -380,16 +408,36 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
}
void connectOrDisconnectToRemote(DrawerMenuItemViewHolder holder) {
private void toggleRemoteServerCxn(DrawerMenuItemViewHolder holder) throws IOException {
JsonSocketClient jsonSocketClient = devPlugin.getJsonSocketClient();
boolean disconnected = jsonSocketClient == null || !jsonSocketClient.isSocketReady();
boolean checked = holder.getSwitchCompat().isChecked();
boolean connected = DevPluginService.getInstance().isConnected();
if (checked && !connected) {
inputRemoteHost();
} else if (!checked && connected) {
DevPluginService.getInstance().disconnectIfNeeded();
if (checked) {
if (disconnected) {
inputRemoteHost();
}
} else {
if (jsonSocketClient != null) {
jsonSocketClient.switchOff();
}
}
}
@SuppressLint("CheckResult")
private void toggleLocalServerCxn(DrawerMenuItemViewHolder holder) throws IOException {
JsonSocketServer jsonSocketServer = devPlugin.getJsonSocketServer();
boolean checked = holder.getSwitchCompat().isChecked();
if (checked) {
devPlugin.enableLocalServer()
.subscribe(Observers.emptyConsumer(), this::onAJServerConnectException);
} else {
if (jsonSocketServer != null) {
jsonSocketServer.switchOff();
}
}
}
private void toggleForegroundService(DrawerMenuItemViewHolder holder) {
boolean checked = holder.getSwitchCompat().isChecked();
@@ -400,37 +448,46 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
}
}
@SuppressLint("CheckResult")
private void inputRemoteHost() {
Context activity = getActivity();
String host = Pref.getServerAddressOrDefault(WifiTool.getRouterIp(Objects.requireNonNull(activity)));
new MaterialDialog.Builder(activity)
.title(R.string.text_server_address)
.input("", host, (dialog, input) -> {
.title(R.string.text_pc_server_address)
.input(getInputHint(), host, (dialog, input) -> {
Pref.saveServerAddress(input.toString());
DevPluginService.getInstance().connectToServer(input.toString())
.subscribe(Observers.emptyConsumer(), this::onConnectException);
devPlugin.connectToRemoteServer(input.toString())
.subscribe(Observers.emptyConsumer(), this::onPCServerConnectException);
})
.neutralText(R.string.text_help)
.onNeutral((dialog, which) -> {
setChecked(mConnectionItem, false);
setChecked(mClientModeItem, false);
IntentUtil.browse(activity, URL_DEV_PLUGIN);
})
.cancelListener(dialog -> setChecked(mConnectionItem, false))
.cancelListener(dialog -> setChecked(mClientModeItem, false))
.show();
}
private void onConnectException(Throwable e) {
setChecked(mConnectionItem, false);
Toast.makeText(GlobalAppContext.get(), getString(R.string.error_connect_to_remote, e.getMessage()),
private String getInputHint() {
Context context = getContext();
if (context != null) {
return context.getString(R.string.text_pc_server_address);
}
return "Input a server address";
}
private void onPCServerConnectException(Throwable e) {
setChecked(mClientModeItem, false);
Toast.makeText(getContext(),
getString(R.string.error_connect_to_remote, e.getMessage()),
Toast.LENGTH_LONG).show();
}
@Override
public void onResume() {
super.onResume();
syncSwitchState();
private void onAJServerConnectException(Throwable e) {
setChecked(mServerModeItem, false);
Toast.makeText(getContext(),
getString(R.string.error_enable_server, e.getMessage()),
Toast.LENGTH_LONG).show();
}
private void syncSwitchState() {
@@ -456,6 +513,10 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
setChecked(mWriteSecuritySettingsItem, hasWriteSecureSettingsAccess());
}
private boolean isAccessibilityServiceEnabled() {
return AccessibilityServiceTool.isAccessibilityServiceEnabled(getActivity());
}
private void enableAccessibilityService() {
if (Pref.shouldEnableAccessibilityServiceByRoot() && RootTool.isRootAvailable()) {
enableAccessibilityServiceByRoot();
@@ -485,27 +546,18 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
});
}
@SuppressWarnings("unused")
@Subscribe
public void onCircularMenuStateChange(CircularMenu.StateChangeEvent event) {
setChecked(mFloatingWindowItem, event.getCurrentState() != CircularMenu.STATE_CLOSED);
}
@Override
public void onDestroy() {
super.onDestroy();
mConnectionStateDisposable.dispose();
EventBus.getDefault().unregister(this);
}
private void showMessage(CharSequence text) {
if (getContext() == null)
return;
Toast.makeText(getContext(), text, Toast.LENGTH_SHORT).show();
}
private void setProgress(DrawerMenuItem item, boolean progress) {
item.setProgress(progress);
mDrawerMenuAdapter.notifyItemChanged(item);
@@ -516,7 +568,11 @@ public class DrawerFragment extends androidx.fragment.app.Fragment {
mDrawerMenuAdapter.notifyItemChanged(item);
}
private boolean isAccessibilityServiceEnabled() {
return AccessibilityServiceTool.isAccessibilityServiceEnabled(getActivity());
private void setItemState(DrawerMenuItem item, DevPluginService.State state) {
setChecked(item, state.getState() == DevPluginService.State.CONNECTED);
setProgress(item, state.getState() == DevPluginService.State.CONNECTING);
if (state.getException() != null) {
showMessage(state.getException().getMessage());
}
}
}

View File

@@ -1,5 +1,7 @@
package org.autojs.autojs.ui.main.drawer;
import java.io.IOException;
/**
* Created by Stardust on 2017/8/25.
*/
@@ -7,17 +9,17 @@ public class DrawerMenuItem {
public interface Action {
void onClick(DrawerMenuItemViewHolder holder);
void onClick(DrawerMenuItemViewHolder holder) throws IOException;
}
private int mIcon;
private int mTitle;
private final int mIcon;
private final int mTitle;
private final Action mAction;
private boolean mAntiShake;
private boolean mSwitchEnabled;
private int mPrefKey;
private Action mAction;
private boolean mSwitchChecked;
private boolean mOnProgress;
private boolean mSwitchEnabled;
private boolean mSwitchChecked;
private int mPrefKey;
private int mNotificationCount;
public DrawerMenuItem(int icon, int title, Action action) {
@@ -81,7 +83,7 @@ public class DrawerMenuItem {
return mPrefKey;
}
public void performAction(DrawerMenuItemViewHolder holder) {
public void performAction(DrawerMenuItemViewHolder holder) throws IOException {
if (mAction != null)
mAction.onClick(holder);
}

View File

@@ -1,24 +1,23 @@
package org.autojs.autojs.ui.main.drawer;
import android.content.pm.PackageManager;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.autojs.autojs.R;
import org.autojs.autojs.ui.widget.BindableViewHolder;
import org.autojs.autojs.ui.widget.PrefSwitch;
import org.autojs.autojs.ui.widget.SwitchCompat;
import java.io.IOException;
import butterknife.BindView;
import butterknife.ButterKnife;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
/**
* Created by Stardust on 2017/12/10.
*/
@@ -48,12 +47,22 @@ public class DrawerMenuItemViewHolder extends BindableViewHolder<DrawerMenuItem>
public DrawerMenuItemViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
mSwitchCompat.setOnCheckedChangeListener((buttonView, isChecked) -> onClick());
mSwitchCompat.setOnCheckedChangeListener((buttonView, isChecked) -> {
try {
onClick();
} catch (IOException e) {
e.printStackTrace();
}
});
itemView.setOnClickListener(v -> {
if (mSwitchCompat.getVisibility() == VISIBLE) {
mSwitchCompat.toggle();
} else {
onClick();
try {
onClick();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@@ -93,7 +102,7 @@ public class DrawerMenuItemViewHolder extends BindableViewHolder<DrawerMenuItem>
}
}
private void onClick() {
private void onClick() throws IOException {
mDrawerMenuItem.setChecked(mSwitchCompat.isChecked());
if (mAntiShake && (System.currentTimeMillis() - mLastClickMillis < CLICK_TIMEOUT)) {
// Toast.makeText(itemView.getContext(), R.string.text_click_too_frequently, Toast.LENGTH_SHORT).show();

View File

@@ -51,7 +51,7 @@
<string name="text_floating_window">Floating Window</string>
<string name="text_error_report">Bug Report</string>
<string name="text_press_again_to_exit">Press again to exit</string>
<string name="text_already_stop_n_scripts">%d script(s) is(are) stopped</string>
<string name="text_already_stop_n_scripts">%d script(s) stopped</string>
<string name="text_start_running">Running</string>
<string name="text_open_by_other_apps">Open by other apps</string>
<string name="text_rename">Rename</string>
@@ -170,14 +170,17 @@
<string name="summary_guard_mode">Prevent automation of scripts when Auto.js in the front</string>
<string name="text_layout_inspector_is_dumping" tools:ignore="TypographyEllipsis">Inspecting layout...</string>
<string name="text_force_stop">Force stop</string>
<string name="text_execution_finished" formatted="false">\\n------------\\n[%s]Finishedspent %f seconds.</string>
<string name="text_execution_finished" formatted="false">[%s] finished in %s seconds.\n</string>
<string name="text_about_me_and_repo">About app and developer</string>
<string name="text_attribute">Attribute</string>
<string name="text_value">Value</string>
<string name="text_show_widget_information">View info</string>
<string name="text_show_layout_hierarchy">View in layout bounds\' view</string>
<string name="default_value_script_dir_path">/Scripts/</string>
<string name="debug">Connect to PC</string>
<string name="text_connect_to_pc">Connect to PC</string>
<string name="text_client_mode">Client mode</string>
<string name="text_server_mode">Server mode</string>
<string name="text_pc_server_address">PC server address</string>
<string name="text_night_mode">Dark mode</string>
<string name="text_stable_mode">Stable mode</string>
<string name="text_foreground_service">Foreground service</string>
@@ -259,4 +262,6 @@
<string name="no_root_access_for_record">Auto.js has no root access to record a script. Continue?</string>
<string name="text_insist_on_record">Record</string>
<string name="text_quit">Quit</string>
<string name="error_connect_to_remote">Can\'t connect to the remote server: %s</string>
<string name="error_enable_server">Can\'t enable the AutoJs6 server: %s</string>
</resources>

View File

@@ -51,7 +51,7 @@
<string name="text_floating_window">悬浮窗</string>
<string name="text_error_report">错误报告</string>
<string name="text_press_again_to_exit">再按一次退出程序</string>
<string name="text_already_stop_n_scripts">已停止%d个正在运行的脚本</string>
<string name="text_already_stop_n_scripts">已停止 %d 个正在运行的脚本</string>
<string name="text_start_running">开始运行</string>
<string name="text_open_by_other_apps">用其他应用打开</string>
<string name="text_rename">重命名</string>
@@ -142,13 +142,15 @@
<string name="key_guard_mode" translatable="false">key_guard_mode</string>
<string name="text_layout_inspector_is_dumping">布局分析中</string>
<string name="text_force_stop">强制停止</string>
<string name="text_execution_finished" formatted="false">\n------------\n[%s]运行结束,用时%f秒</string>
<string name="text_execution_finished" formatted="false">[%s] 运行结束 (用时 %s 秒)\n</string>
<string name="text_again"></string>
<string name="text_again_and_again">又双</string>
<string name="text_again_and_again_again">又双叒</string>
<string name="text_again_and_again_again_again">又双叒叕</string>
<string name="debug">连接到计算机</string>
<string name="text_server_address">服务器地址</string>
<string name="text_connect_to_pc">连接到计算机</string>
<string name="text_client_mode">客户端模式</string>
<string name="text_server_mode">服务端模式</string>
<string name="text_pc_server_address">PC 服务端地址</string>
<string name="text_about_me_and_repo">关于项目与开发者</string>
<string name="text_attribute">属性</string>
<string name="text_value"></string>
@@ -357,6 +359,7 @@
<string name="text_close">关闭</string>
<string name="text_execute">执行</string>
<string name="error_connect_to_remote">连接失败: %s</string>
<string name="error_enable_server">AutoJs6 服务启用失败: %s</string>
<string name="text_are_you_sure_to_delete">确定要删除%s吗</string>
<string name="text_run_on_broadcast">广播触发任务</string>
<string name="text_search_java_class">搜索Java包/类</string>

View File

@@ -58,8 +58,8 @@
<string name="text_press_again_to_exit">Press again to exit</string>
<string name="text_common_function">常用函数</string>
<string name="sorry_for_crash">很抱歉(ಥ _ ಥ)程序遇到未知错误,即将停止运行\n错误代码</string>
<string name="text_no_running_script">No running script</string>
<string name="text_already_stop_n_scripts">已停止%d个正在运行的脚本</string>
<string name="text_no_running_scripts">No running script</string>
<string name="text_already_stop_n_scripts">已停止 %d 个正在运行的脚本</string>
<string name="text_start_running">Running</string>
<string name="text_open_by_other_apps">Open by other apps</string>
<string name="text_rename">Rename</string>