6.0.1 - 增加客户端/服务端连接方式 增加部分全局对象 部分插件及工具版本更新
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() + "{" +
|
||||
|
||||
138
app/src/main/java/org/autojs/autojs/pluginclient/Buffer.java
Normal file
138
app/src/main/java/org/autojs/autojs/pluginclient/Buffer.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
283
app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java
Normal file
283
app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
81
app/src/main/java/org/autojs/autojs/tool/IOTool.java
Normal file
81
app/src/main/java/org/autojs/autojs/tool/IOTool.java
Normal 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 "";
|
||||
}
|
||||
}
|
||||
32
app/src/main/java/org/autojs/autojs/tool/ThreadTool.java
Normal file
32
app/src/main/java/org/autojs/autojs/tool/ThreadTool.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]Finished,spent %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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user