diff --git a/README.md b/README.md index 75a4699f..9b747f02 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ * 支持将脚本文件或项目打包为 APK 文件 * 支持利用 Root 权限扩展功能 (屏幕点击/滑动/录制/Shell) * 支持作为 Tasker 插件使用 +* 支持与 VSCode 连接并进行桌面开发 (需要 [AutoJs6-VSCode-Ext](https://github.com/SuperMonster003/AutoJs6-VSCode-Ext) 插件) ****** @@ -98,24 +99,23 @@ [comment]: <> "Version history only shows last 3 versions" -# v6.0.1 Alpha2 +# v6.0.1 -###### 2021/12/07 +###### 2022/01/01 -* `修复` global.sleep及global.toast方法绑定错误 - -# v6.0.1 Alpha - -###### 2021/12/05 - -* `新增` 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 @@ -190,11 +190,11 @@ ****** -* [Auto.js](https://github.com/hyb1996/Auto.js-VSCode-Extension) { author: [hyb1996](https://github.com/hyb1996) } - - `适用于 VS Code 的桌面开发插件` +* [AutoJs6-VSCode-Ext](https://github.com/SuperMonster003/AutoJs6-VSCode-Ext) { author: [SuperMonster003](https://github.com/SuperMonster003) } + - `适用于 VSCode 的桌面开发插件 (二次开发项目)` * [AutoX](https://github.com/kkevsekk1/AutoX) { author: [kkevsekk1](https://github.com/kkevsekk1) } - - `Auto.js 二次开发项目` + - `安卓平台 JavaScript 自动化工具 (二次开发项目)` -* [Auto.js-TypeScript-Declarations](https://github.com/SuperMonster003/Auto.js-TypeScript-Declarations) { author: [SuperMonster003](https://github.com/SuperMonster003) } - - `Auto.js 声明文件 (.d.ts)` +[//]: # (* [Auto.js-TypeScript-Declarations](https://github.com/SuperMonster003/Auto.js-TypeScript-Declarations) { author: [SuperMonster003](https://github.com/SuperMonster003) }) +[//]: # ( - `Auto.js 声明文件 (.d.ts)`) diff --git a/app/build.gradle b/app/build.gradle index 8f2b3c2e..21f48c8e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,9 +5,9 @@ plugins { id "kotlin-kapt" } -def AnnotationsVer = "4.8.0" -def SupportLibsVer = "28.0.0" -def ButterKnifeVer = "10.2.3" +def annotationsVer = "4.8.0" +def butterKnifeVer = "10.2.3" +def leakcanaryVer = "2.7" def properties = new Properties() def propFile = new File("sign.properties") @@ -15,21 +15,15 @@ def isPropFileExists = propFile.exists() isPropFileExists && properties.load(new FileInputStream(propFile)) -configurations.all { - resolutionStrategy { - force "com.android.support:support-v4:${SupportLibsVer}" - } -} - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { jvmTarget = JavaVersion.VERSION_16 } } -tasks.withType(JavaCompile) { - options.compilerArgs << '-Xlint:deprecation' << '-Xlint:unchecked' -} +// tasks.withType(JavaCompile) { +// options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked" +// } android { signingConfigs { @@ -52,7 +46,7 @@ android { javaCompileOptions { annotationProcessorOptions { arguments = [ - 'resourcePackageName': applicationId, + "resourcePackageName": applicationId, "androidManifestFile": "$projectDir/src/main/AndroidManifest.xml".toString() ] } @@ -66,6 +60,10 @@ android { if (isPropFileExists) { signingConfig signingConfigs.release } + // Disable Crashlytics reports. + ext.enableCrashlytics = false + // Disables PNG crunching for the release build type. + crunchPngs false } release { shrinkResources false @@ -118,7 +116,7 @@ android { applicationVariants.all { variant -> variant.mergeAssetsProvider.configure { doLast { - delete(fileTree(dir: outputDir, includes: ['declarations/**'])) + delete(fileTree(dir: outputDir, includes: ["declarations/**"])) } } } @@ -129,18 +127,22 @@ repositories { } dependencies { - testImplementation "junit:junit:4.13.2" + // JUnit + testImplementation "junit:junit:${junitVer}" + + // LeakCanary + debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakcanaryVer}" // Kotlin - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2-native-mt" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0-native-mt" // Android Annotations - kapt "org.androidannotations:androidannotations:${AnnotationsVer}" - implementation "org.androidannotations:androidannotations-api:${AnnotationsVer}" + kapt "org.androidannotations:androidannotations:${annotationsVer}" + implementation "org.androidannotations:androidannotations-api:${annotationsVer}" // ButterKnife - kapt "com.jakewharton:butterknife-compiler:${ButterKnifeVer}" - implementation "com.jakewharton:butterknife:${ButterKnifeVer}" + kapt "com.jakewharton:butterknife-compiler:${butterKnifeVer}" + implementation "com.jakewharton:butterknife:${butterKnifeVer}" // Android supports implementation "androidx.appcompat:appcompat:1.4.0" @@ -212,13 +214,10 @@ dependencies { // Android Job implementation "com.evernote:android-job:1.4.2" - // Leakcanary - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' - // Extracted from com.github.hyb1996:MutableTheme:1.0.0 - api 'com.jrummyapps:colorpicker:2.1.7' - implementation 'androidx.recyclerview:recyclerview:1.2.1' - implementation 'com.github.ozodrukh:CircularReveal:2.0.1' + implementation "com.jrummyapps:colorpicker:2.1.7" + implementation "androidx.recyclerview:recyclerview:1.2.1" + implementation "com.github.ozodrukh:CircularReveal:2.0.1" // Bugly implementation project(":libs:com.tencent.bugly.crashreport-2.6.6") diff --git a/app/src/main/assets/doc/CHANGELOG.md b/app/src/main/assets/doc/CHANGELOG.md index 2d4230de..eb524821 100644 --- a/app/src/main/assets/doc/CHANGELOG.md +++ b/app/src/main/assets/doc/CHANGELOG.md @@ -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 diff --git a/app/src/main/java/org/autojs/autojs/autojs/AutoJs.java b/app/src/main/java/org/autojs/autojs/autojs/AutoJs.java index cb2160e0..ec3dd525 100644 --- a/app/src/main/java/org/autojs/autojs/autojs/AutoJs.java +++ b/app/src/main/java/org/autojs/autojs/autojs/AutoJs.java @@ -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; } }; diff --git a/app/src/main/java/org/autojs/autojs/autojs/ScriptExecutionGlobalListener.java b/app/src/main/java/org/autojs/autojs/autojs/ScriptExecutionGlobalListener.java index bdd62629..1c24b723 100644 --- a/app/src/main/java/org/autojs/autojs/autojs/ScriptExecutionGlobalListener.java +++ b/app/src/main/java/org/autojs/autojs/autojs/ScriptExecutionGlobalListener.java @@ -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 diff --git a/app/src/main/java/org/autojs/autojs/model/explorer/ExplorerFileItem.java b/app/src/main/java/org/autojs/autojs/model/explorer/ExplorerFileItem.java index e042bc1b..4732e66a 100644 --- a/app/src/main/java/org/autojs/autojs/model/explorer/ExplorerFileItem.java +++ b/app/src/main/java/org/autojs/autojs/model/explorer/ExplorerFileItem.java @@ -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() + "{" + diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/Buffer.java b/app/src/main/java/org/autojs/autojs/pluginclient/Buffer.java new file mode 100644 index 00000000..284a2063 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/pluginclient/Buffer.java @@ -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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginResponseHandler.java b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginResponseHandler.java index 189df3a0..527bfa3e 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginResponseHandler.java +++ b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginResponseHandler.java @@ -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 mScriptExecutions = new HashMap<>(); + private final HashMap 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 handleBytes(JsonObject data, JsonWebSocket.Bytes bytes) { + public Observable 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(); diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.java b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.java index 1da3449b..c42694a7 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.java +++ b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.java @@ -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 mConnectionState = PublishSubject.create(); - private final DevPluginResponseHandler mResponseHandler; - private final HashMap mBytes = new HashMap<>(); - private final HashMap 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 connectionState() { - return mConnectionState; - } - - @AnyThread - public Observable connectToServer(String host) { - int port = PORT; + public Observable 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 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 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 cxn, int state) { + cxn.onNext(new State(state)); + } + + public static void setState(PublishSubject 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() - .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 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 map) { - JsonObject data = new JsonObject(); - for (Map.Entry 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)); - } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java new file mode 100644 index 00000000..286c3676 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java @@ -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 getJsonElementPublishSubject(); + + public abstract PublishSubject getBytesPublishSubject(); + + public void sayHello() { + writeMap(TYPE_HELLO, new MapBuilder() + .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 map) { + JsonObject data = new JsonObject(); + for (Map.Entry 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 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 cxn, int state) { + cxn.onNext(new DevPluginService.State(state)); + } + + public void setState(PublishSubject 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(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.java b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.java new file mode 100644 index 00000000..29ad03eb --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.java @@ -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 cxnState = PublishSubject.create(); + + private final PublishSubject mJsonElementPublishSubject = PublishSubject.create(); + private final PublishSubject mBytesPublishSubject = PublishSubject.create(); + + private final HashMap mBytes = new HashMap<>(); + private final HashMap 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 getJsonElementPublishSubject() { + return mJsonElementPublishSubject; + } + + @Override + public PublishSubject 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; + } + +} diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketServer.java b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketServer.java new file mode 100644 index 00000000..25bdba73 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketServer.java @@ -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 cxnState = PublishSubject.create(); + + private final PublishSubject mJsonElementPublishSubject = PublishSubject.create(); + private final PublishSubject mBytesPublishSubject = PublishSubject.create(); + + private final HashMap mBytes = new HashMap<>(); + private final HashMap 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 getJsonElementPublishSubject() { + return mJsonElementPublishSubject; + } + + @Override + public PublishSubject 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; + } + +} diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/JsonWebSocket.java b/app/src/main/java/org/autojs/autojs/pluginclient/JsonWebSocket.java deleted file mode 100644 index 111074c8..00000000 --- a/app/src/main/java/org/autojs/autojs/pluginclient/JsonWebSocket.java +++ /dev/null @@ -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 mJsonElementPublishSubject = PublishSubject.create(); - private final PublishSubject 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 data() { - return mJsonElementPublishSubject; - } - - public Observable 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; - } - -} diff --git a/app/src/main/java/org/autojs/autojs/tool/IOTool.java b/app/src/main/java/org/autojs/autojs/tool/IOTool.java new file mode 100644 index 00000000..e59cb786 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/tool/IOTool.java @@ -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 ""; + } +} diff --git a/app/src/main/java/org/autojs/autojs/tool/ThreadTool.java b/app/src/main/java/org/autojs/autojs/tool/ThreadTool.java new file mode 100644 index 00000000..6f97d2c2 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/tool/ThreadTool.java @@ -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 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 condition) throws InterruptedException { + return wait(condition, 10 * 1000); + } +} diff --git a/app/src/main/java/org/autojs/autojs/ui/main/community/CommunityWebView.java b/app/src/main/java/org/autojs/autojs/ui/main/community/CommunityWebView.java index a2a96a50..70d9a30b 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/community/CommunityWebView.java +++ b/app/src/main/java/org/autojs/autojs/ui/main/community/CommunityWebView.java @@ -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(); }); } diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.java b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.java index c9af70ea..f3fd626a 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.java +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.java @@ -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()); + } } } diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItem.java b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItem.java index 4b91932b..da378e48 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItem.java +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItem.java @@ -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); } diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemViewHolder.java b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemViewHolder.java index eae6c1d8..1dfbb473 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemViewHolder.java +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemViewHolder.java @@ -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 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 } } - 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(); diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 7173dec9..200887c0 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -51,7 +51,7 @@ Floating Window Bug Report Press again to exit - %d script(s) is(are) stopped + %d script(s) stopped Running Open by other apps Rename @@ -170,14 +170,17 @@ Prevent automation of scripts when Auto.js in the front Inspecting layout... Force stop - \\n------------\\n[%s]Finished,spent %f seconds. + [%s] finished in %s seconds.\n About app and developer Attribute Value View info View in layout bounds\' view /Scripts/ - Connect to PC + Connect to PC + Client mode + Server mode + PC server address Dark mode Stable mode Foreground service @@ -259,4 +262,6 @@ Auto.js has no root access to record a script. Continue? Record Quit + Can\'t connect to the remote server: %s + Can\'t enable the AutoJs6 server: %s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9c9900e..3428fe0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,7 +51,7 @@ 悬浮窗 错误报告 再按一次退出程序 - 已停止%d个正在运行的脚本 + 已停止 %d 个正在运行的脚本 开始运行 用其他应用打开 重命名 @@ -142,13 +142,15 @@ key_guard_mode 布局分析中 强制停止 - \n------------\n[%s]运行结束,用时%f秒 + [%s] 运行结束 (用时 %s 秒)\n 又双 又双叒 又双叒叕 - 连接到计算机 - 服务器地址 + 连接到计算机 + 客户端模式 + 服务端模式 + PC 服务端地址 关于项目与开发者 属性 @@ -357,6 +359,7 @@ 关闭 执行 连接失败: %s + AutoJs6 服务启用失败: %s 确定要删除%s吗 广播触发任务 搜索Java包/类 diff --git a/app/src/main/strings.xml b/app/src/main/strings.xml index f49f91e0..d2c4516a 100644 --- a/app/src/main/strings.xml +++ b/app/src/main/strings.xml @@ -58,8 +58,8 @@ Press again to exit 常用函数 很抱歉(ಥ _ ಥ)程序遇到未知错误,即将停止运行\n错误代码: - No running script - 已停止%d个正在运行的脚本 + No running script + 已停止 %d 个正在运行的脚本 Running Open by other apps Rename diff --git a/autojs/build.gradle b/autojs/build.gradle index 51650087..dcff52d8 100644 --- a/autojs/build.gradle +++ b/autojs/build.gradle @@ -9,9 +9,9 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } } -tasks.withType(JavaCompile) { - options.compilerArgs << '-Xlint:deprecation' << '-Xlint:unchecked' -} +// tasks.withType(JavaCompile) { +// options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked" +// } android { compileSdkVersion versions.compile @@ -24,7 +24,7 @@ android { buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } compileOptions { @@ -41,37 +41,22 @@ repositories { } dependencies { - testImplementation 'junit:junit:4.13.2' + testImplementation "junit:junit:${junitVer}" - api 'org.greenrobot:eventbus:3.2.0' + implementation "net.lingala.zip4j:zip4j:2.9.1" - api 'net.lingala.zip4j:zip4j:2.9.1' - - api 'com.afollestad.material-dialogs:core:0.9.6.0' - api 'com.google.android.material:material:1.6.0-alpha01' - - api 'com.github.hyb1996:EnhancedFloaty:0.31' - - api 'com.makeramen:roundedimageview:2.3.0' + implementation "com.afollestad.material-dialogs:core:0.9.6.0" + implementation "com.google.android.material:material:1.6.0-alpha01" // OkHttp - api 'com.squareup.okhttp3:okhttp:5.0.0-alpha.3' - - // JDeferred - api 'org.jdeferred:jdeferred-android-aar:1.2.6' - - // RootShell - api 'com.github.Stericson:RootShell:1.6' + implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.3" // Gson - api 'com.google.code.gson:gson:2.8.9' + implementation "com.google.code.gson:gson:2.8.9" // Log4j - api group: 'de.mindpipe.android', name: 'android-logging-log4j', version: '1.0.3' - api group: 'log4j', name: 'log4j', version: '1.2.17' - - // Preference - api 'androidx.preference:preference-ktx:1.1.1' + implementation group: "de.mindpipe.android", name: "android-logging-log4j", version: "1.0.3" + implementation group: "log4j", name: "log4j", version: "1.2.17" // Terminal emulator api project(":libs:jackpal.androidterm.libtermexec-1.0") @@ -80,8 +65,8 @@ dependencies { api project(":libs:com.android.dx-1.7.0") api project(":libs:org.mozilla.rhino-1.7.14") - api project(':libs:org.opencv-4.5.4') + api project(":libs:org.opencv-4.5.4") - api project(':common') - api project(':automator') + api project(":common") + api project(":automator") } \ No newline at end of file diff --git a/autojs/src/main/AndroidManifest.xml b/autojs/src/main/AndroidManifest.xml index e0480539..6fe4cae0 100644 --- a/autojs/src/main/AndroidManifest.xml +++ b/autojs/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ xmlns:tools="http://schemas.android.com/tools" package="com.stardust.autojs"> + diff --git a/autojs/src/main/assets/init.js b/autojs/src/main/assets/init.js index 8da5bfd6..fd5ea0aa 100644 --- a/autojs/src/main/assets/init.js +++ b/autojs/src/main/assets/init.js @@ -63,7 +63,7 @@ runtime.init(); (function (scope) { var modules = ['app', 'automator', 'console', 'dialogs', 'io', 'selector', 'shell', 'web', 'ui', 'images', 'threads', 'events', 'engines', 'RootAutomator', 'http', 'storages', 'floaty', - 'sensors', 'media', 'plugins', 'continuation']; + 'sensors', 'media', 'plugins', 'continuation', '$base64']; var len = modules.length; for (var i = 0; i < len; i++) { var m = modules[i]; @@ -78,6 +78,11 @@ runtime.init(); Canvas = com.stardust.autojs.core.graphics.ScriptCanvas; Image = com.stardust.autojs.core.image.ImageWrapper; + OkHttpClient = Packages.okhttp3.OkHttpClient; + MutableOkHttp = com.stardust.autojs.core.http.MutableOkHttp; + Intent = android.content.Intent; + BroadcastReceiver = com.stardust.autojs.core.content.BroadcastReceiver; + // 重定向require以便支持相对路径和npm模块 Module = require('jvm-npm.js'); require = Module.require; diff --git a/autojs/src/main/assets/modules/__$base64__.js b/autojs/src/main/assets/modules/__$base64__.js new file mode 100644 index 00000000..1d553e2d --- /dev/null +++ b/autojs/src/main/assets/modules/__$base64__.js @@ -0,0 +1,27 @@ +module.exports = function (runtime, global) { + const Base64 = android.util.Base64; + + const $base64 = () => void 0; + + let _ = { + charsetNames: [ + // charset names updated up to android sdk 27 + 'us-ascii', 'iso-8859-1', 'utf-8', 'utf-16be', 'utf-16le', 'utf-16', + ], + isValidEncoding(encode) { + return this.charsetNames.includes(encode); + }, + }; + + // noinspection JSValidateTypes + $base64.encode = (str, encoding) => _.isValidEncoding(encoding) + ? Base64.encodeToString(new java.lang.String(str).getBytes(encoding), Base64.NO_WRAP) + : Base64.encodeToString(new java.lang.String(str).getBytes(), Base64.NO_WRAP); + + // noinspection JSValidateTypes + $base64.decode = (str, encoding) => _.isValidEncoding(encoding) + ? String(new java.lang.String(Base64.decode(str, Base64.NO_WRAP), encoding)) + : String(new java.lang.String(Base64.decode(str, Base64.NO_WRAP))); + + return $base64; +}; \ No newline at end of file diff --git a/autojs/src/main/assets/modules/__images__.js b/autojs/src/main/assets/modules/__images__.js index d2a2e3db..7ed90868 100644 --- a/autojs/src/main/assets/modules/__images__.js +++ b/autojs/src/main/assets/modules/__images__.js @@ -2,38 +2,40 @@ module.exports = function (runtime, scope) { const ResultAdapter = require("result_adapter"); - var MatchingResult = (function () { - var comparators = { - "left": (l, r) => l.point.x - r.point.x, - "top": (l, r) => l.point.y - r.point.y, - "right": (l, r) => r.point.x - l.point.x, - "bottom": (l, r) => r.point.y - l.point.y - } + const MatchingResult = (function () { + let comparators = { + 'left': (l, r) => l.point.x - r.point.x, + 'top': (l, r) => l.point.y - r.point.y, + 'right': (l, r) => r.point.x - l.point.x, + 'bottom': (l, r) => r.point.y - l.point.y, + }; + function MatchingResult(list) { if (Array.isArray(list)) { this.matches = list; } else { this.matches = runtime.bridges.bridges.toArray(list); } - this.__defineGetter__("points", () => { + this.__defineGetter__('points', () => { if (typeof (this.__points__) == 'undefined') { this.__points__ = this.matches.map(m => m.point); } return this.__points__; }); } + MatchingResult.prototype.first = function () { if (this.matches.length == 0) { return null; } return this.matches[0]; - } + }; MatchingResult.prototype.last = function () { if (this.matches.length == 0) { return null; } return this.matches[this.matches.length - 1]; - } + }; MatchingResult.prototype.findMax = function (cmp) { if (this.matches.length == 0) { return null; @@ -45,32 +47,32 @@ module.exports = function (runtime, scope) { } }); return target; - } + }; MatchingResult.prototype.leftmost = function () { return this.findMax(comparators.left); - } + }; MatchingResult.prototype.topmost = function () { return this.findMax(comparators.top); - } + }; MatchingResult.prototype.rightmost = function () { return this.findMax(comparators.right); - } + }; MatchingResult.prototype.bottommost = function () { return this.findMax(comparators.bottom); - } + }; MatchingResult.prototype.worst = function () { return this.findMax((l, r) => l.similarity - r.similarity); - } + }; MatchingResult.prototype.best = function () { return this.findMax((l, r) => r.similarity - l.similarity); - } + }; MatchingResult.prototype.sortBy = function (cmp) { var comparatorFn = null; if (typeof (cmp) == 'string') { - cmp.split("-").forEach(direction => { + cmp.split('-').forEach(direction => { var buildInFn = comparators[direction]; if (!buildInFn) { - throw new Error("unknown direction '" + direction + "' in '" + cmp + "'"); + throw new Error('unknown direction \'' + direction + '\' in \'' + cmp + '\''); } (function (fn) { if (comparatorFn == null) { @@ -83,7 +85,7 @@ module.exports = function (runtime, scope) { return fn(l, r); } return cmpValue; - } + }; })(comparatorFn, fn); } })(buildInFn); @@ -94,7 +96,7 @@ module.exports = function (runtime, scope) { var clone = this.matches.slice(); clone.sort(comparatorFn); return new MatchingResult(clone); - } + }; return MatchingResult; })(); diff --git a/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java b/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java index db623ff6..97f48d17 100644 --- a/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java +++ b/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java @@ -25,6 +25,7 @@ import com.stardust.util.UiHandler; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; +import java.text.MessageFormat; import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -175,7 +176,7 @@ public class ScriptEngineService { @Subscribe public void onScriptExecution(ScriptExecutionEvent event) { if (event.getCode() == ScriptExecutionEvent.ON_START) { - mGlobalConsole.verbose(mContext.getString(R.string.text_start_running) + "[" + event.getMessage() + "]"); + mGlobalConsole.verbose(MessageFormat.format("{0} [{1}].", mContext.getString(R.string.text_start_running), event.getMessage())); } else if (event.getCode() == ScriptExecutionEvent.ON_EXCEPTION) { mUiHandler.toast(mContext.getString(R.string.text_error) + ": " + event.getMessage()); } diff --git a/autojs/src/main/java/com/stardust/autojs/core/console/GlobalConsole.java b/autojs/src/main/java/com/stardust/autojs/core/console/GlobalConsole.java index 77edbf6f..577e30b8 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/console/GlobalConsole.java +++ b/autojs/src/main/java/com/stardust/autojs/core/console/GlobalConsole.java @@ -1,5 +1,7 @@ package com.stardust.autojs.core.console; +import android.annotation.SuppressLint; + import com.stardust.util.UiHandler; import org.apache.log4j.Level; @@ -15,6 +17,7 @@ import java.util.Locale; * Created by Stardust on 2017/10/22. */ +@SuppressLint("ConstantLocale") public class GlobalConsole extends ConsoleImpl { private static final String LOG_tAG = "GlobalConsole"; private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()); @@ -37,7 +40,6 @@ public class GlobalConsole extends ConsoleImpl { private Priority toLog4jLevel(int level) { switch (level) { case android.util.Log.VERBOSE: - return Level.DEBUG; case android.util.Log.DEBUG: return Level.DEBUG; case android.util.Log.INFO: @@ -53,22 +55,15 @@ public class GlobalConsole extends ConsoleImpl { } private String getLevelChar(int level) { - switch (level) { - case android.util.Log.VERBOSE: - return "V"; - case android.util.Log.DEBUG: - return "D"; - case android.util.Log.INFO: - return "I"; - case android.util.Log.WARN: - return "W"; - case android.util.Log.ERROR: - return "E"; - case android.util.Log.ASSERT: - return "A"; - - } - return ""; + return switch (level) { + case android.util.Log.VERBOSE -> "V"; + case android.util.Log.DEBUG -> "D"; + case android.util.Log.INFO -> "I"; + case android.util.Log.WARN -> "W"; + case android.util.Log.ERROR -> "E"; + case android.util.Log.ASSERT -> "A"; + default -> ""; + }; } } diff --git a/autojs/src/main/java/com/stardust/autojs/core/looper/Timer.java b/autojs/src/main/java/com/stardust/autojs/core/looper/Timer.java index 1b386afa..fbc3bd79 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/looper/Timer.java +++ b/autojs/src/main/java/com/stardust/autojs/core/looper/Timer.java @@ -87,6 +87,9 @@ public class Timer { } } + public void post(Runnable r) { + + } public boolean clearInterval(int id) { return clearCallback(id); diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/Device.java b/autojs/src/main/java/com/stardust/autojs/runtime/api/Device.java index bf2608f4..4dab0a5f 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/api/Device.java +++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/Device.java @@ -15,8 +15,10 @@ import android.os.Build; import android.os.PowerManager; import android.os.Vibrator; import android.provider.Settings; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import android.telephony.TelephonyManager; import com.stardust.autojs.R; @@ -73,13 +75,8 @@ public class Device { public static final String securityPatch; static { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - baseOS = Build.VERSION.BASE_OS; - securityPatch = Build.VERSION.SECURITY_PATCH; - } else { - baseOS = null; - securityPatch = null; - } + baseOS = Build.VERSION.BASE_OS; + securityPatch = Build.VERSION.SECURITY_PATCH; } public static final String codename = Build.VERSION.CODENAME; @@ -87,7 +84,7 @@ public class Device { @SuppressLint("HardwareIds") public static final String serial = Build.SERIAL; - private Context mContext; + private final Context mContext; private PowerManager.WakeLock mWakeLock; private int mWakeLockFlag; @@ -285,16 +282,13 @@ public class Device { return; } SettingsCompat.manageWriteSettings(mContext); - throw new SecurityException(mContext.getString(R.string.no_write_settings_permissin)); + throw new SecurityException(mContext.getString(R.string.no_write_settings_permission)); } private void checkReadPhoneStatePermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (mContext.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) - != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException(mContext.getString(R.string.no_read_phone_state_permissin)); - } + if (mContext.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException(mContext.getString(R.string.no_read_phone_state_permissin)); } } @@ -323,7 +317,9 @@ public class Device { return getMacByFile(); } + @SuppressLint("MissingPermission") String mac = wifiInf.getMacAddress(); + if (FAKE_MAC_ADDRESS.equals(mac)) { mac = null; } diff --git a/autojs/src/main/res/values-en/strings.xml b/autojs/src/main/res/values-en/strings.xml index f57a76fd..37fa0047 100644 --- a/autojs/src/main/res/values-en/strings.xml +++ b/autojs/src/main/res/values-en/strings.xml @@ -4,9 +4,9 @@ Script Running Path is empty File does not exist - No file reading&writing permission - No running script - Stop %d script(s) + No file r/w permission + No running scripts + %d script(s) stopped Required Android OS version: OK Cancel @@ -14,7 +14,7 @@ No drawing overlay permission Auto.js Key observing is disabled, please enable in settings - No writing settings permission + No writing settings permission 通知服务未运行,请重新启用通知权限 diff --git a/autojs/src/main/res/values/strings.xml b/autojs/src/main/res/values/strings.xml index e5f862ca..0e656f50 100644 --- a/autojs/src/main/res/values/strings.xml +++ b/autojs/src/main/res/values/strings.xml @@ -5,8 +5,8 @@ 路径为空 文件不存在 无文件读写权限 - 没有正在运行的脚本 - 已停止%d个正在运行的脚本 + 没有正在运行的脚本 + 已停止 %d 个正在运行的脚本 本脚本需要此安卓版本以上才能运行: 确定 取消 @@ -15,7 +15,7 @@ 使脚本自动操作(点击、长按、滑动等)所需,若关闭则只能执行不涉及自动操作的脚本。 按键监听未启用,请在软件设置中开启 手势监听未启用,请在软件设置中开启 - 沒有修改系統设置权限 + 沒有修改系統设置权限 通知服务未运行,请重新启用通知权限 本脚本需要Auto.js版本号%d以上才能运行 打开侧拉菜单 diff --git a/automator/build.gradle b/automator/build.gradle index d166c37d..a74f5da8 100644 --- a/automator/build.gradle +++ b/automator/build.gradle @@ -1,5 +1,5 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +apply plugin: "com.android.library" +apply plugin: "kotlin-android" tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { @@ -27,7 +27,7 @@ android { buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } } @@ -37,7 +37,7 @@ repositories { } dependencies { - testImplementation 'junit:junit:4.13.2' - api 'androidx.appcompat:appcompat:1.4.0' - api project(':common') + testImplementation "junit:junit:${junitVer}" + implementation "androidx.appcompat:appcompat:1.4.0" + api project(":common") } diff --git a/automator/src/test/java/com/stardust/automator/filter/DfsFilterTest.kt b/automator/src/test/java/com/stardust/automator/filter/DfsFilterTest.kt index 7a644d5c..c4c96b17 100644 --- a/automator/src/test/java/com/stardust/automator/filter/DfsFilterTest.kt +++ b/automator/src/test/java/com/stardust/automator/filter/DfsFilterTest.kt @@ -28,7 +28,7 @@ class DfsFilterTest { fun filter() { val filter = RandomFilter() val root = TestUiObject(10) - val list = DFS(filter).search(root) + val list = DFS.search(root, filter) for (uiObject in list) { if (root !== uiObject) uiObject.recycle() diff --git a/build.gradle b/build.gradle index 365e3af6..21032041 100644 --- a/build.gradle +++ b/build.gradle @@ -4,25 +4,22 @@ import org.apache.groovy.json.internal.LazyMap // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.6.0" + ext.kotlinVer = "1.6.10" + ext.junitVer = "4.13.2" repositories { mavenCentral() google() - maven { url "https://maven.aliyun.com/repository/apache-snapshots" } maven { url "https://maven.aliyun.com/repository/central" } maven { url "https://maven.aliyun.com/repository/google" } maven { url "https://maven.aliyun.com/repository/gradle-plugin" } - maven { url "https://maven.aliyun.com/repository/grails-core" } maven { url "https://maven.aliyun.com/repository/jcenter" } maven { url "https://maven.aliyun.com/repository/public" } - maven { url "https://maven.aliyun.com/repository/spring" } - maven { url "https://maven.aliyun.com/repository/spring-plugin" } maven { url "https://repo.huaweicloud.com/repository/maven" } } dependencies { - classpath 'com.android.tools.build:gradle:7.2.0-alpha05' + classpath "com.android.tools.build:gradle:7.2.0-alpha06" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVer}" classpath "com.jakewharton:butterknife-gradle-plugin:10.2.1" classpath "org.codehaus.groovy:groovy-json:3.0.8" @@ -34,15 +31,11 @@ allprojects { maven { url "https://jitpack.io" } maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } mavenCentral() - maven { url "https://maven.aliyun.com/repository/apache-snapshots" } maven { url "https://maven.aliyun.com/repository/central" } maven { url "https://maven.aliyun.com/repository/google" } maven { url "https://maven.aliyun.com/repository/gradle-plugin" } - maven { url "https://maven.aliyun.com/repository/grails-core" } maven { url "https://maven.aliyun.com/repository/jcenter" } maven { url "https://maven.aliyun.com/repository/public" } - maven { url "https://maven.aliyun.com/repository/spring" } - maven { url "https://maven.aliyun.com/repository/spring-plugin" } maven { url "https://repo.huaweicloud.com/repository/maven" } google() } diff --git a/common/build.gradle b/common/build.gradle index eb7668ed..ee08221e 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,9 +1,9 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +apply plugin: "com.android.library" +apply plugin: "kotlin-android" -tasks.withType(JavaCompile) { - options.compilerArgs << '-Xlint:deprecation' -} +// tasks.withType(JavaCompile) { +// options.compilerArgs << "-Xlint:deprecation" +// } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { @@ -24,7 +24,7 @@ android { buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } lintOptions { @@ -37,8 +37,33 @@ android { } dependencies { - testImplementation 'junit:junit:4.13.2' - api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - api 'androidx.annotation:annotation:1.3.0' - api 'com.github.hyb1996:settingscompat:1.1.5' + testImplementation "junit:junit:${junitVer}" + + // Kotlin + api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVer}" + + // RoundedImageView + api "com.makeramen:roundedimageview:2.3.0" + + // EventBus + api "org.greenrobot:eventbus:3.3.1" + + // Annotation + api "androidx.annotation:annotation:1.3.0" + + // Preference + api "androidx.preference:preference-ktx:1.1.1" + + // RootShell + api "com.github.Stericson:RootShell:1.6" + + // JDeferred + api "org.jdeferred:jdeferred-android-aar:1.2.6" + + // Auto.js + api "com.github.hyb1996:settingscompat:1.1.5" + api "com.github.hyb1996:EnhancedFloaty:0.31" + + // Rhino + api project(":libs:org.mozilla.rhino-1.7.14") } diff --git a/gradle.properties b/gradle.properties index c317e028..06ed9ca1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,8 +12,11 @@ # org.gradle.parallel=true #Thu Nov 11 16:38:18 CST 2021 -org.gradle.jvmargs=-Xms2g -Xmx2g -Dkotlin.daemon.jvm.options\="-Xmx2g" -Dfile.encoding\=UTF-8 +org.gradle.jvmargs=-Xms2g -Xmx2g -Dkotlin.daemon.jvm.options\="-Xmx2g" -Dfile.encoding\=UTF-8 -XX:+UseParallelGC android.useAndroidX=true android.enableJetifier=true -android.injected.studio.version.check=false \ No newline at end of file +android.injected.studio.version.check=false + +org.gradle.daemon=true +org.gradle.parallel=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7f8f9af9..35475a35 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Nov 26 16:35:39 CST 2021 +#Thu Dec 30 20:07:37 CST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME \ No newline at end of file +zipStoreBase=GRADLE_USER_HOME diff --git a/inrt/build.gradle b/inrt/build.gradle index dae35709..8c5428f4 100644 --- a/inrt/build.gradle +++ b/inrt/build.gradle @@ -1,5 +1,5 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' +apply plugin: "com.android.application" +apply plugin: "kotlin-android" android { compileSdkVersion versions.compile @@ -15,8 +15,8 @@ android { } lintOptions { abortOnError false - disable 'MissingTranslation' - disable 'ExtraTranslation' + disable "MissingTranslation" + disable "ExtraTranslation" } compileOptions { sourceCompatibility JavaVersion.VERSION_16 @@ -46,51 +46,51 @@ android { buildTypes { debug { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } } def buildApkPluginForAbi(File pluginProjectDir, String abi) { copy { - from file('..\\app\\release\\') - into new File(pluginProjectDir, 'app\\src\\main\\assets') + from file("..\\app\\release\\") + into new File(pluginProjectDir, "app\\src\\main\\assets") def fileName = "inrt-" + abi + "-release.apk" include fileName - rename fileName, 'template.apk' + rename fileName, "template.apk" } exec { workingDir pluginProjectDir - commandLine 'gradlew.bat', 'assembleRelease' + commandLine "gradlew.bat", "assembleRelease" } copy { - from new File(pluginProjectDir, 'app\\build\\outputs\\apk\\release') - into file('..\\common\\release') - def fileName = '打包插件-' + versions.appVersionName + '-release.apk' + from new File(pluginProjectDir, "app\\build\\outputs\\apk\\release") + into file("..\\common\\release") + def fileName = "打包插件-" + versions.appVersionName + "-release.apk" include fileName - rename fileName, '打包插件-' + abi + '-' + versions.appVersionName + '-release.apk' + rename fileName, "打包插件-" + abi + "-" + versions.appVersionName + "-release.apk" } } task buildApkPlugin { doLast { - def pluginProjectDirPath = '..\\..\\AutoJsApkBuilderPlugin' + def pluginProjectDirPath = "..\\..\\AutoJsApkBuilderPlugin" def pluginProjectDir = file(pluginProjectDirPath) if (!pluginProjectDir.exists() || !pluginProjectDir.isDirectory()) { - println 'pluginProjectDir not exists' + println "pluginProjectDir not exists" return } - buildApkPluginForAbi(pluginProjectDir, 'armeabi-v7a') + buildApkPluginForAbi(pluginProjectDir, "armeabi-v7a") } } tasks.whenTaskAdded { task -> - if (task.name == 'assembleRelease') { - task.finalizedBy 'buildApkPlugin' + if (task.name == "assembleRelease") { + task.finalizedBy "buildApkPlugin" } } @@ -105,14 +105,14 @@ repositories { } dependencies { - testImplementation 'junit:junit:4.13.2' + testImplementation "junit:junit:${junitVer}" - implementation 'com.github.bumptech.glide:glide:4.12.0' + implementation "com.github.bumptech.glide:glide:4.12.0" implementation project(":libs:org.mozilla.rhino-1.7.14") implementation project(":libs:org.opencv-4.5.4") - implementation project(':automator') - implementation project(':common') - implementation project(':autojs') + implementation project(":automator") + implementation project(":common") + implementation project(":autojs") } diff --git a/inrt/src/main/res/values/strings.xml b/inrt/src/main/res/values/strings.xml index 0e6da344..c7ada45d 100644 --- a/inrt/src/main/res/values/strings.xml +++ b/inrt/src/main/res/values/strings.xml @@ -19,6 +19,6 @@ 打开应用后直接运行脚本 其他 Powered by Auto.js - \n------------\n[%s]运行结束,用时%f秒 + [%s] 运行结束 (用时 %f 秒) diff --git a/libs/org.mozilla.rhino-1.7.14/build.gradle b/libs/org.mozilla.rhino-1.7.14/build.gradle index 984d5207..650d5236 100644 --- a/libs/org.mozilla.rhino-1.7.14/build.gradle +++ b/libs/org.mozilla.rhino-1.7.14/build.gradle @@ -5,4 +5,4 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } configurations.maybeCreate("default") -artifacts.add("default", file("rhino-1.7.14-SNAPSHOT.jar")) \ No newline at end of file +artifacts.add("default", file("rhino-1.7.14.jar")) \ No newline at end of file diff --git a/libs/org.mozilla.rhino-1.7.14/rhino-1.7.14-SNAPSHOT.jar b/libs/org.mozilla.rhino-1.7.14/rhino-1.7.14.jar similarity index 88% rename from libs/org.mozilla.rhino-1.7.14/rhino-1.7.14-SNAPSHOT.jar rename to libs/org.mozilla.rhino-1.7.14/rhino-1.7.14.jar index 01995203..63532d7d 100644 Binary files a/libs/org.mozilla.rhino-1.7.14/rhino-1.7.14-SNAPSHOT.jar and b/libs/org.mozilla.rhino-1.7.14/rhino-1.7.14.jar differ diff --git a/project-versions.json b/project-versions.json index b1ba7f55..2daa05bb 100644 --- a/project-versions.json +++ b/project-versions.json @@ -1,7 +1,7 @@ { - "appVersionCode": 654, - "appVersionName": "6.0.0", - "appSinceDate": "Dec 1, 2021", + "appVersionCode": 693, + "appVersionName": "6.0.1", + "appSinceDate": "Jan 1, 2022", "target": 28, "mini": 24, "compile": 31