From e01f010e46c237f6211fb1663473009807c90057 Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Fri, 9 Jan 2026 23:51:51 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha14=20-=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=A8=A1=E5=BC=8F=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E6=97=B6,=20Activity=20=E9=87=8D=E5=BB=BA=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=20VSCode=20=E6=8E=A7=E5=88=B6=E5=8F=B0?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E8=BE=93=E5=87=BA=E6=97=A5=E5=BF=97=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20(issue=20#385);=20=E6=B7=B1=E5=BA=A6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20VSCode=20=E6=8F=92=E4=BB=B6=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changelog/lang_ar.json | 2 +- .changelog/lang_en.json | 2 +- .changelog/lang_es.json | 2 +- .changelog/lang_fr.json | 2 +- .changelog/lang_ja.json | 2 +- .changelog/lang_ko.json | 2 +- .changelog/lang_ru.json | 2 +- .changelog/lang_zh-Hans.json | 8 +- .changelog/lang_zh-Hant-HK.json | 2 +- .changelog/lang_zh-Hant-TW.json | 2 +- app/src/main/java/org/autojs/autojs/AutoJs.kt | 15 +- .../autojs/app/tool/AbstractJsonSocketTool.kt | 3 + .../autojs/app/tool/JsonSocketClientTool.kt | 78 ++--- .../autojs/app/tool/JsonSocketServerTool.kt | 19 +- .../autojs/pluginclient/DevPluginService.kt | 209 +++++++++---- .../autojs/pluginclient/JsonSocket.java | 289 +++++++----------- .../autojs/pluginclient/JsonSocketClient.kt | 165 +++++++--- .../autojs/pluginclient/JsonSocketServer.kt | 30 +- .../autojs/service/AccessibilityService.kt | 4 +- .../autojs/ui/main/drawer/DrawerFragment.kt | 99 +++--- .../main/drawer/DrawerMenuItemCustomHelper.kt | 5 +- .../ui/main/drawer/DrawerMenuItemHelper.kt | 2 +- .../ui/main/drawer/PermissionItemHelper.kt | 5 +- .../ui/main/drawer/RunnableItemHelper.kt | 5 +- .../ui/main/drawer/ServiceItemHelper.kt | 5 +- .../ui/main/drawer/ShowableItemHelper.kt | 5 +- .../autojs/ui/main/drawer/SocketItemHelper.kt | 15 +- .../org/autojs/autojs/util/NetworkUtils.kt | 8 +- app/src/main/res/values-ar/strings.xml | 2 + app/src/main/res/values-en/strings.xml | 2 + app/src/main/res/values-es/strings.xml | 2 + app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values-ja/strings.xml | 2 + app/src/main/res/values-ko/strings.xml | 2 + app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values-zh-rHK/strings.xml | 2 + app/src/main/res/values-zh-rTW/strings.xml | 2 + app/src/main/res/values-zh/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + version.properties | 10 +- 40 files changed, 599 insertions(+), 420 deletions(-) diff --git a/.changelog/lang_ar.json b/.changelog/lang_ar.json index 648f2137..5da250e1 100644 --- a/.changelog/lang_ar.json +++ b/.changelog/lang_ar.json @@ -277,7 +277,7 @@ "دعم التعرف الذكي على موقع الإدراج عند استيراد فئات وحزم Java في محرر الأكواد", "دعم استخدام المسارات كمعلمات الصور مباشرة في وحدة images", "دعم المعلمات في استيراد الحزم باستخدام importPackage", - "دعم التعرف الذكي على عناوين IP في وضع الخادم من الحافظة ودعم التحويل الذكي باستخدام مفتاح المسافة", + "في وضع العميل، يدعم عنوان IP الاستيراد من الحافظة مع تعرّف ذكي، ويدعم التحويل الذكي لإدخال مفتاح المسافة", "دعم إنشاء الملفات برقم تسلسلي افتراضي ومناسب مع اختيار تلقائي للبادئة في مدير الملفات", "توفر رسائل خطأ مفصلة عند تشغيل مشروع معين في مدير الملفات _[`issue #268`](http://issues.autojs6.com/268)_", "دعم المزيد من أنواع الملفات ودعم عرض الرموز المقابلة (يدعم أكثر من 800 نوع من الملفات) في مدير الملفات", diff --git a/.changelog/lang_en.json b/.changelog/lang_en.json index f52829bc..a0a27c47 100644 --- a/.changelog/lang_en.json +++ b/.changelog/lang_en.json @@ -277,7 +277,7 @@ "Code editor supports intelligently determining insertion location when importing Java classes and package names", "images module supports using paths directly as image parameters", "importPackage supports string parameters", - "Server mode IP address supports clipboard import with intelligent recognition and smart conversion with space key", + "In client mode, the IP address supports smart recognition when importing from the clipboard and also supports intelligent conversion of space-key input", "File manager supports default prefix selection when creating new files and automatically generates appropriate numeric suffix", "File manager specifically informs on exception message when running project _[`issue #268`](http://issues.autojs6.com/268)_", "File manager supports more types and displays corresponding icon symbols (supports over 800 file types)", diff --git a/.changelog/lang_es.json b/.changelog/lang_es.json index 1228d8c7..b76db8b3 100644 --- a/.changelog/lang_es.json +++ b/.changelog/lang_es.json @@ -277,7 +277,7 @@ "El editor de código admite la determinación inteligente de la ubicación de inserción al importar clases Java y nombres de paquetes", "El módulo images admite el uso de rutas directamente como parámetros de imagen", "importPackage admite parámetros de cadena", - "La dirección IP del modo servidor admite la importación del portapapeles con reconocimiento inteligente y conversión inteligente con la tecla de espacio", + "En el modo cliente, la dirección IP admite la importación desde el portapapeles con reconocimiento inteligente y permite la conversión inteligente de la tecla de espacio", "El administrador de archivos admite la selección de prefijos predeterminados al crear nuevos archivos y genera automáticamente el sufijo numérico apropiado", "El administrador de archivos informa específicamente sobre el mensaje de excepción al ejecutar el proyecto _[`issue #268`](http://issues.autojs6.com/268)_", "El administrador de archivos admite más tipos y muestra símbolos de icono correspondientes (admite más de 800 tipos de archivos)", diff --git a/.changelog/lang_fr.json b/.changelog/lang_fr.json index 22e90b84..dcbfd0c5 100644 --- a/.changelog/lang_fr.json +++ b/.changelog/lang_fr.json @@ -277,7 +277,7 @@ "L'éditeur de code prend en charge la détermination intelligente de l'emplacement d'insertion lors de l'importation de classes Java et de noms de paquets", "Le module images prend en charge l'utilisation des chemins directement comme paramètres d'image", "importPackage prend en charge les paramètres de chaîne", - "L'adresse IP du mode serveur prend en charge l'importation du presse-papiers avec reconnaissance intelligente et conversion intelligente avec la touche espace", + "En mode client, l'adresse IP prend en charge l'importation depuis le presse-papiers avec reconnaissance intelligente et permet une conversion intelligente de la touche espace", "Le gestionnaire de fichiers prend en charge la sélection de préfixes par défaut lors de la création de nouveaux fichiers et génère automatiquement un suffixe numérique approprié", "Le gestionnaire de fichiers informe spécifiquement sur le message d'exception lors de l'exécution du projet _[`issue #268`](http://issues.autojs6.com/268)_", "Le gestionnaire de fichiers prend en charge plus de types et affiche les symboles d'icône correspondants (prend en charge plus de 800 types de fichiers)", diff --git a/.changelog/lang_ja.json b/.changelog/lang_ja.json index a725654b..391adc8b 100644 --- a/.changelog/lang_ja.json +++ b/.changelog/lang_ja.json @@ -277,7 +277,7 @@ "コードエディタで Java クラスやパッケージ名をインポートする位置のインテリジェント判断をサポート", "images モジュールが画像パラメータとしてパスの直接使用をサポート", "importPackage が文字列パラメータをサポート", - "サーバーモードの IP アドレスがクリップボードインポートをスマートに認識し, スペースキーのスマート変換をサポート", + "クライアントモードでは, IP アドレスがクリップボードからの取り込みに対応し, インテリジェントに認識します. さらに, スペースキー入力の自動変換にも対応します", "ファイルマネージャが新しいファイルを作成する際にデフォルトのプレフィックス選択と適切な数字サフィックスの自動生成をサポート", "ファイル管理プロジェクトの実行時の例外メッセージを具体化 _[`issue #268`](http://issues.autojs6.com/268)_", "ファイルマネージャがより多くの種類をサポートし, 対応するアイコンシンボルを表示 (800 種類以上のファイルタイプをサポート)", diff --git a/.changelog/lang_ko.json b/.changelog/lang_ko.json index 6526e73a..063a1e91 100644 --- a/.changelog/lang_ko.json +++ b/.changelog/lang_ko.json @@ -277,7 +277,7 @@ "코드 편집기에서 Java 클래스나 패키지명을 가져오는 위치의 지능형 판단을 지원", "images 모듈이 이미지 매개변수로 경로의 직접 사용을 지원", "importPackage 가 문자열 매개변수를 지원", - "서버 모드의 IP 주소가 클립보드 가져오기를 스마트하게 인식하고, 스페이스 키 스마트 변환을 지원", + "클라이언트 모드에서 IP 주소는 클립보드 가져오기를 지능적으로 인식하며, 공백 키 입력을 지능적으로 변환하는 기능도 지원합니다", "파일 관리자가 새 파일을 생성할 때 기본 접두사 선택 및 적절한 숫자 접미사의 자동 생성을 지원", "파일 관리 프로젝트 실행 시 예외 메시지 구체화 _[`issue #268`](http://issues.autojs6.com/268)_", "파일 관리자가 더 많은 유형을 지원하고, 해당 아이콘 심볼을 표시 (800 이상의 파일 타입 지원)", diff --git a/.changelog/lang_ru.json b/.changelog/lang_ru.json index 098e236b..09f42b98 100644 --- a/.changelog/lang_ru.json +++ b/.changelog/lang_ru.json @@ -277,7 +277,7 @@ "Редактор кода поддерживает интеллектуальное определение места вставки при импорте Java классов и пакетов имен", "Модуль images поддерживает использование путей напрямую в качестве параметров изображений", "importPackage поддерживает строковые параметры", - "IP-адрес в режиме сервера поддерживает импорт из буфера обмена с интеллектуальным определением и конверсией с пробелом", + "В режиме клиента IP-адрес поддерживает умное распознавание при вставке из буфера обмена и интеллектуальное преобразование ввода через пробел", "Файловый менеджер поддерживает выбор префиксов по умолчанию при создании новых файлов и автоматически генерирует подходящее числовое расширение", "Файловый менеджер четко уведомляет об ошибке при выполнении проекта _[`issue #268`](http://issues.autojs6.com/268)_", "Файловый менеджер поддерживает больше типов и отображает соответствующие значки (поддерживает более 800 типов файлов)", diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index 05558824..69021987 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -1,7 +1,7 @@ { "$data": { "v6.7.0": { - "released_date": "2026/01/08", + "released_date": "2026/01/09", "feature": [ "插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)", "cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))", @@ -81,6 +81,9 @@ "文件管理器删除项目文件夹后 UI 未能自动刷新的问题", "使用 \"三按钮\" 手势导航时导航栏前景色可能与夜间模式关联异常的问题", "APK 文件类型信息对话框可能无法获取应用名称及 SDK 信息的问题", + "客户端模式连接时, 旋转屏幕及切换语言等触发 Activity 重建的操作导致 AutoJs6 总是重建连接的问题", + "服务端模式连接时, 旋转屏幕及切换语言等触发 Activity 重建的操作导致 VSCode 控制台无法输出日志的问题 _[`issue #385`](http://issues.autojs6.com/385)_", + "连接 VSCode 插件时, 多种方式同时连接可能导致日志打印数量成倍增加的问题", "构建工具启用 isCleanup[Paddle/Rapid]Ocr 配置选项时无法正常完成 Rebuild Project 任务的问题" ], "improvement": [ @@ -101,6 +104,7 @@ "崩溃报告页面支持双指缩放调整字体大小并添加常用功能按钮", "应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_", "Rhino 引擎在泛型签名解析失败时回退为原始反射类型以增强低版本安卓系统的反射方法可用性", + "服务端模式连接时支持显示已建立连接的客户端数量", "使用 LiveData 及 SharedFlow 替代已弃用的 LocalBroadcastManager", "Gradle 构建脚本提升 7z 格式文件的解压效率", "Gradle 构建脚本支持获取详细的 Android Studio IDE 版本 (如 \"2025.1.4.7\")", @@ -414,7 +418,7 @@ "代码编辑器导入 Java 类和包名时支持智能判断插入位置", "images 模块支持直接使用路径作为图像参数", "importPackage 支持字符串参数", - "服务端模式 IP 地址支持剪切板导入智能识别且支持空格按键智能转换", + "客户端模式 IP 地址支持剪切板导入智能识别且支持空格按键智能转换", "文件管理器新建文件时支持默认前缀选择并自动生成合适的数字后缀", "文件管理器运行项目时具体化异常消息提示 _[`issue #268`](http://issues.autojs6.com/268)_", "文件管理器支持更多类型并支持显示对应的图标符号 (支持 800 多种文件类型)", diff --git a/.changelog/lang_zh-Hant-HK.json b/.changelog/lang_zh-Hant-HK.json index 1ad7f4e7..04de50af 100644 --- a/.changelog/lang_zh-Hant-HK.json +++ b/.changelog/lang_zh-Hant-HK.json @@ -277,7 +277,7 @@ "代碼編輯器導入 Java 類和包名時支持智能判斷插入位置", "images 模塊支持直接使用路徑作為圖像參數", "importPackage 支持字符串參數", - "服務端模式 IP 地址支持剪切板導入智能識別且支持空格按鍵智能轉換", + "客户端模式 IP 地址支持剪切板導入智能識別且支持空格按鍵智能轉換", "文件管理器新建文件時支持默認前綴選擇並自動生成合適的數字後綴", "文件管理器運行項目時具體化異常消息提示 _[`issue #268`](http://issues.autojs6.com/268)_", "文件管理器支持更多類型並支持顯示對應的圖標符號 (支持 800 多種文件類型)", diff --git a/.changelog/lang_zh-Hant-TW.json b/.changelog/lang_zh-Hant-TW.json index 9fe49306..33541151 100644 --- a/.changelog/lang_zh-Hant-TW.json +++ b/.changelog/lang_zh-Hant-TW.json @@ -277,7 +277,7 @@ "程式碼編輯器匯入 Java 類和包名時支援智慧判斷插入位置", "images 模組支援直接使用路徑作為影象引數", "importPackage 支援字串引數", - "服務端模式 IP 地址支援剪下板匯入智慧識別且支援空格按鍵智慧轉換", + "客戶端模式 IP 地址支援剪下板匯入智慧識別且支援空格按鍵智慧轉換", "檔案管理器新建檔案時支援預設字首選擇並自動生成合適的數字字尾", "檔案管理器執行專案時具體化異常訊息提示 _[`issue #268`](http://issues.autojs6.com/268)_", "檔案管理器支援更多型別並支援顯示對應的圖示符號 (支援 800 多種檔案型別)", diff --git a/app/src/main/java/org/autojs/autojs/AutoJs.kt b/app/src/main/java/org/autojs/autojs/AutoJs.kt index 8c9844cc..860bcb1a 100644 --- a/app/src/main/java/org/autojs/autojs/AutoJs.kt +++ b/app/src/main/java/org/autojs/autojs/AutoJs.kt @@ -26,7 +26,9 @@ import org.autojs.autojs.ui.floating.FullScreenFloatyWindow import org.autojs.autojs.ui.floating.layoutinspector.LayoutBoundsFloatyWindow import org.autojs.autojs.ui.floating.layoutinspector.LayoutHierarchyFloatyWindow import org.autojs.autojs.util.RhinoUtils.isBackgroundThread -import java.util.concurrent.Executors +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit import org.autojs.autojs.inrt.autojs.AutoJs as AutoJsInrt /** @@ -38,7 +40,16 @@ import org.autojs.autojs.inrt.autojs.AutoJs as AutoJsInrt open class AutoJs(appContext: Application) : AbstractAutoJs(appContext) { // @Thank to Zen2H - private val mPrintExecutor = Executors.newSingleThreadExecutor() + // Use bounded queue to prevent log flooding from blocking the whole channel. + // zh-CN: 使用有界队列避免日志洪泛导致整个通道被阻塞. + private val mPrintExecutor = ThreadPoolExecutor( + 1, + 1, + 0L, + TimeUnit.MILLISECONDS, + ArrayBlockingQueue(2048), + ThreadPoolExecutor.DiscardOldestPolicy(), + ) private val mA11yTool = AccessibilityTool(appContext) diff --git a/app/src/main/java/org/autojs/autojs/app/tool/AbstractJsonSocketTool.kt b/app/src/main/java/org/autojs/autojs/app/tool/AbstractJsonSocketTool.kt index 9fc2c6e4..4de96697 100644 --- a/app/src/main/java/org/autojs/autojs/app/tool/AbstractJsonSocketTool.kt +++ b/app/src/main/java/org/autojs/autojs/app/tool/AbstractJsonSocketTool.kt @@ -9,6 +9,7 @@ import org.autojs.autojs.ui.main.drawer.SocketItemHelper /** * Created by SuperMonster003 on Jun 24, 2022. + * Modified by SuperMonster003 as of Jan 9, 2026. */ abstract class AbstractJsonSocketTool(final override val context: Context) : SocketItemHelper { @@ -23,6 +24,8 @@ abstract class AbstractJsonSocketTool(final override val context: Context) : Soc @JvmField protected var onConnectionDialogDismissed = DialogInterface.OnDismissListener { } + internal abstract fun connectIfNotNormallyClosed() + fun setOnConnectionException(consumer: Consumer) = also { onConnectionException = consumer } fun setStateDisposable(disposable: Disposable?) = also { stateDisposable = disposable } diff --git a/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketClientTool.kt b/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketClientTool.kt index e7b3f37a..89aa0804 100644 --- a/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketClientTool.kt +++ b/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketClientTool.kt @@ -10,19 +10,16 @@ import org.autojs.autojs.app.DialogUtils import org.autojs.autojs.core.pref.Pref import org.autojs.autojs.extension.MaterialDialogExtensions.widgetThemeColor import org.autojs.autojs.pluginclient.JsonSocketClient -import org.autojs.autojs.ui.main.drawer.DrawerMenuDisposableItem import org.autojs.autojs.util.Observers import org.autojs.autojs.util.ViewUtils import org.autojs.autojs6.R class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { - private var mClientModeItem: DrawerMenuDisposableItem? = null - override val isConnected get() = devPlugin.isJsonSocketClientConnected - private var isNormallyClosed + override var isNormallyClosed get() = devPlugin.isClientSocketNormallyClosed set(state) { devPlugin.isClientSocketNormallyClosed = state @@ -30,29 +27,17 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { override val isInMainThread = true - override fun connect(): Boolean { + override fun connect() { inputRemoteHost(isAutoConnect = !isNormallyClosed) - // @Hint by SuperMonster003 on Nov 9, 2023. - // ! Method with showing a dialog always returns false. - // ! zh-CN: 含对话框显示的方法总是返回 false 值. - return false } - internal fun connectIfNotNormallyClosed() { + override fun connectIfNotNormallyClosed() { if (!isNormallyClosed) connect() } - internal fun setClientModeItem(clientModeItem: DrawerMenuDisposableItem) { - mClientModeItem = clientModeItem - } - - override fun disconnect(): Boolean { - mClientModeItem?.subtitle = null - val result = runCatching { - devPlugin.jsonSocketClient?.switchOff() - }.isSuccess + override fun disconnect() { + devPlugin.jsonSocketClient?.switchOff() isNormallyClosed = true - return result } override fun dispose() { @@ -64,7 +49,7 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { val host = Pref.getServerAddress() if (isAutoConnect) { devPlugin - .connectToRemoteServer(context, host, mClientModeItem, true) + .connectToRemoteServer(context, host, true) .subscribe(Observers.emptyConsumer(), Observers.emptyConsumer()) return } @@ -87,26 +72,25 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { connectToRemoteServer(dialog) } .itemsLongCallback { dHistories, _, _, text -> - false.also { - MaterialDialog.Builder(context) - .title(R.string.text_prompt) - .content(R.string.text_confirm_to_delete) - .negativeText(R.string.dialog_button_cancel) - .neutralColorRes(R.color.dialog_button_default) - .positiveText(R.string.dialog_button_confirm) - .positiveColorRes(R.color.dialog_button_caution) - .onPositive { ds, _ -> - ds.dismiss() - JsonSocketClient.removeFromHistories(text.toString()) - dHistories.items?.let { - it.remove(text) - dHistories.notifyItemsChanged() - DialogUtils.toggleContentViewByItems(dHistories) - DialogUtils.toggleActionButtonAbilityByItems(dHistories, DialogAction.NEUTRAL) - } + MaterialDialog.Builder(context) + .title(R.string.text_prompt) + .content(R.string.text_confirm_to_delete) + .negativeText(R.string.dialog_button_cancel) + .neutralColorRes(R.color.dialog_button_default) + .positiveText(R.string.dialog_button_confirm) + .positiveColorRes(R.color.dialog_button_caution) + .onPositive { ds, _ -> + ds.dismiss() + JsonSocketClient.removeFromHistories(text.toString()) + dHistories.items?.let { + it.remove(text) + dHistories.notifyItemsChanged() + DialogUtils.toggleContentViewByItems(dHistories) + DialogUtils.toggleActionButtonAbilityByItems(dHistories, DialogAction.NEUTRAL) } - .show() - } + } + .show() + return@itemsLongCallback false } .neutralText(R.string.dialog_button_clear_items) .neutralColorRes(R.color.dialog_button_warn) @@ -247,7 +231,7 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { } dialog.dismiss() devPlugin - .connectToRemoteServer(context, input, mClientModeItem) + .connectToRemoteServer(context, input) .subscribe({ Pref.setServerAddress(input) }, onConnectionException) } @@ -259,16 +243,16 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { return Regex(regex).matches(nearest.toString()) && Regex(regex).matches(source) } - private fun triggerRepeatedCharacter(dialog: MaterialDialog, source: CharSequence, nearest: Char): Boolean { - if (isRepeatedCharacter(source, nearest, REGEX_DOT)) { + private fun triggerRepeatedCharacter(dialog: MaterialDialog, source: CharSequence, nearest: Char) = when { + isRepeatedCharacter(source, nearest, REGEX_DOT) -> { showSnack(dialog, R.string.error_repeated_dot_symbol) - return true + true } - if (isRepeatedCharacter(source, nearest, REGEX_COLON)) { + isRepeatedCharacter(source, nearest, REGEX_COLON) -> { showSnack(dialog, R.string.error_repeated_colon_symbol) - return true + true } - return false + else -> false } companion object { diff --git a/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketServerTool.kt b/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketServerTool.kt index a322027a..11ff4bf0 100644 --- a/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketServerTool.kt +++ b/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketServerTool.kt @@ -4,21 +4,20 @@ import android.annotation.SuppressLint import android.content.Context import org.autojs.autojs.util.Observers import org.autojs.autojs.util.ViewUtils -import java.io.IOException class JsonSocketServerTool(context: Context) : AbstractJsonSocketTool(context) { override val isConnected get() = devPlugin.isServerSocketConnected - private var isNormallyClosed + override var isNormallyClosed get() = devPlugin.isServerSocketNormallyClosed set(state) { devPlugin.isServerSocketNormallyClosed = state } @SuppressLint("CheckResult") - override fun connect(): Boolean { + override fun connect() { devPlugin .enableLocalServer() .subscribe(Observers.emptyConsumer()) { @@ -27,23 +26,15 @@ class JsonSocketServerTool(context: Context) : AbstractJsonSocketTool(context) { onConnectionException.accept(it) } isNormallyClosed = false - return true } - internal fun connectIfNotNormallyClosed() { + override fun connectIfNotNormallyClosed() { if (!isNormallyClosed) connect() } - override fun disconnect(): Boolean { - val result = try { - devPlugin.jsonSocketServer?.switchOff() - true - } catch (e: IOException) { - e.printStackTrace() - false - } + override fun disconnect() { + devPlugin.disconnectJsonSocketServer() isNormallyClosed = true - return result } override fun dispose() { diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt index 4cfd74b1..f25521c5 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt +++ b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt @@ -4,24 +4,34 @@ import android.content.Context import androidx.annotation.AnyThread import io.reactivex.Observable import io.reactivex.schedulers.Schedulers -import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject import org.autojs.autojs.annotation.ScriptInterface import org.autojs.autojs.pluginclient.JsonSocket.HANDSHAKE_TIMEOUT import org.autojs.autojs.runtime.ScriptRuntime -import org.autojs.autojs.ui.main.drawer.DrawerMenuDisposableItem +import org.autojs.autojs.util.NetworkUtils.DEFAULT_IP_ADDRESS import org.autojs.autojs.util.ThreadUtils import java.io.File import java.io.IOException import java.net.ServerSocket +import java.net.SocketException +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean /** * Created by Stardust on May 11, 2017. - * Modified by SuperMonster003 as of Jan 1, 2022. + * Modified by SuperMonster003 as of Jan 9, 2026. * Transformed by SuperMonster003 on Jul 1, 2023. */ class DevPluginService(val context: Context) { - class State @JvmOverloads constructor(val state: Int, val exception: Throwable? = null) { + class State @JvmOverloads constructor(private val state: Int, val exception: Throwable? = null) { + + fun isDisconnected() = state == DISCONNECTED + + fun isConnecting() = state == CONNECTING + + fun isConnected() = state == CONNECTED companion object { const val DISCONNECTED = 0 @@ -32,14 +42,18 @@ class DevPluginService(val context: Context) { } object Port { - const val PC_SERVER = 6347 const val AJ_SERVER = 7347 - } val responseHandler = DevPluginResponseHandler(context, File(context.cacheDir, "remote_project")) + // Publish current server-side connection count. + // zh-CN: 发布当前服务端连接数量. + val serverConnectionCount = BehaviorSubject.createDefault(0) + + val clientConnectionIpAddress = BehaviorSubject.createDefault(DEFAULT_IP_ADDRESS) + @get:ScriptInterface @Volatile var jsonSocketClient: JsonSocketClient? = null @@ -50,14 +64,11 @@ class DevPluginService(val context: Context) { var jsonSocketServer: JsonSocketServer? = null private set - @Volatile - private var mServerSocket: ServerSocket? = null - val isJsonSocketClientConnected - get() = jsonSocketClient != null && jsonSocketClient!!.isSocketReady + get() = jsonSocketClient?.isSocketReady == true val isServerSocketConnected - get() = mServerSocket != null && !mServerSocket!!.isClosed + get() = mServerSocket?.isClosed == false var isClientSocketNormallyClosed get() = JsonSocketClient.isClientSocketNormallyClosed @@ -71,6 +82,17 @@ class DevPluginService(val context: Context) { JsonSocketServer.isServerSocketNormallyClosed = state } + @Volatile + private var mServerSocket: ServerSocket? = null + + // Track all active server-side client connections. + // zh-CN: 跟踪所有服务端已建立的客户端连接. + private val mServerConnections = ConcurrentHashMap.newKeySet() + + // Prevent duplicate server startup. + // zh-CN: 防止服务端重复启动. + private val mEnableLocalServerStarted = AtomicBoolean(false) + fun disconnectJsonSocketClient() { try { jsonSocketClient?.switchOff() @@ -81,17 +103,36 @@ class DevPluginService(val context: Context) { fun disconnectJsonSocketServer() { try { + // Close all active client connections first. + // zh-CN: 先关闭所有已连接客户端. + mServerConnections.toList().forEach { runCatching { it.close() } } + mServerConnections.clear() + serverConnectionCount.onNext(0) + + // Then close listening socket. + // zh-CN: 然后关闭监听 socket. jsonSocketServer?.switchOff() + + // Publish disconnected state for UI. + // zh-CN: 发布断开状态用于 UI 刷新. + JsonSocketServer.cxnState.onNext(State(State.DISCONNECTED)) } catch (e: IOException) { e.printStackTrace() } } @AnyThread - fun connectToRemoteServer(context: Context, host: String, clientModeItem: DrawerMenuDisposableItem?) = connectToRemoteServer(context, host, clientModeItem, false) + fun connectToRemoteServer( + context: Context, + host: String, + ignoreExceptions: Boolean = false, + ): Observable { + // Reuse existing connection to avoid reconnect on Activity recreation. + // zh-CN: 复用现有连接, 避免 Activity 重建时重复连接. + if (isJsonSocketClientConnected) { + return Observable.just(jsonSocketClient) + } - @AnyThread - fun connectToRemoteServer(context: Context, host: String, clientModeItem: DrawerMenuDisposableItem?, ignoreExceptions: Boolean): Observable { try { var port = Port.PC_SERVER var ip = host @@ -100,38 +141,46 @@ class DevPluginService(val context: Context) { port = host.substring(i + 1).toInt() ip = host.substring(0, i) } + + // Show connecting subtitle immediately. + // zh-CN: 立即显示正在连接的 subtitle. + clientConnectionIpAddress.onNext("$ip ..."); + return Observable .just(JsonSocketClient(this, context, ip, port)) .observeOn(Schedulers.newThread()) - .doOnNext { jsonSocketClient -> + .doOnNext { client -> try { - clientModeItem?.subtitle = "$ip ..." - this.jsonSocketClient = jsonSocketClient - if (ThreadUtils.wait({ jsonSocketClient.isSocketReady }, HANDSHAKE_TIMEOUT)) { - clientModeItem?.subtitle = ip - this.isClientSocketNormallyClosed = false - JsonSocketClient.addIntoHistories(ip) - jsonSocketClient + this.jsonSocketClient = client + + // Mark this instance as active before it can emit state. + // zh-CN: 在该实例可能发出状态前, 将其标记为活跃实例. + JsonSocketClient.activeInstanceId = client.instanceId + + if (ThreadUtils.wait({ client.isSocketReady }, HANDSHAKE_TIMEOUT)) { + // Do NOT mark as connected here. Wait for hello validation in JsonSocketClient. + // zh-CN: 不要在此处标记为已连接, 等待 JsonSocketClient 的 hello 校验通过. + client .subscribeMessage() .monitorMessage() .sayHello() } else { - if (jsonSocketClient.isExtensionVersionCheckFailed || jsonSocketClient.hasErrorMessageOnHello) { + if (client.isExtensionVersionCheckFailed || client.hasErrorMessageOnHello) { JsonSocketClient.addIntoHistories(ip) - jsonSocketClient.switchOff() + client.switchOff() return@doOnNext } if (!ignoreExceptions) { - jsonSocketClient.onHandshakeTimeout() + client.onHandshakeTimeout() } else { - jsonSocketClient.switchOff() + client.switchOff() } } } catch (e: IOException) { if (!ignoreExceptions) { - jsonSocketClient.onSocketError(e) + client.onSocketError(e) } else { - jsonSocketClient.switchOff() + client.switchOff() } } } @@ -147,58 +196,98 @@ class DevPluginService(val context: Context) { @AnyThread fun enableLocalServer(): Observable { return Observable - .just(JsonSocketServer(this, Port.AJ_SERVER)) - .observeOn(Schedulers.newThread()) - .doOnNext { jsonSocketServer: JsonSocketServer -> - do { - try { - this.jsonSocketServer = jsonSocketServer.apply { - mServerSocket = JsonSocketServer.serverSocket - } - if (mServerSocket != null) { - jsonSocketServer - .setStateConnected() - .setSocket(mServerSocket!!.accept()) - .subscribeMessage() - .monitorMessage() - .sayHello() - } - } catch (e: Exception) { - e.printStackTrace() - val msg = e.message ?: "" - if (!msg.contains(Regex("Socket closed"))) { - jsonSocketServer.onSocketError(e) - } + .create { emitter -> + + // Ensure only one accept loop runs at the same time. + // zh-CN: 确保同一时间只有一个 accept 循环在运行. + if (!mEnableLocalServerStarted.compareAndSet(false, true)) { + jsonSocketServer?.let { emitter.onNext(it) } + emitter.onComplete() + return@create + } + + JsonSocketServer(this, Port.AJ_SERVER).also { + // Listening started. + // zh-CN: 监听已启动. + this.jsonSocketServer = it + it.setStateConnected() + emitter.onNext(it) + } + + try { + while (true) { + val ss = JsonSocketServer.serverSocket + mServerSocket = ss + if (ss == null || ss.isClosed) break + try { - jsonSocketServer.socket?.close() - } catch (e: Exception) { - e.printStackTrace() + val acceptedSocket = ss.accept() + // Create a dedicated connection instance per client. + // zh-CN: 为每个客户端创建独立连接实例. + val conn = JsonSocketServer(this, Port.AJ_SERVER).apply { + setSocket(acceptedSocket) + subscribeMessage() + monitorMessage() + sayHello() + } + + mServerConnections.add(conn) + serverConnectionCount.onNext(mServerConnections.size) + + emitter.onNext(conn) + } catch (e: SocketException) { + // Treat "Socket closed" as a normal shutdown path. + // zh-CN: 将 "Socket closed" 视为正常关闭流程. + if ((e.message ?: "").contains("Socket closed", true)) { + break + } + emitter.onError(e) + return@create } } - } while (mServerSocket != null) + + emitter.onComplete() + } finally { + // Allow restarting after loop exits. + // zh-CN: 循环退出后允许再次启动. + mEnableLocalServerStarted.set(false) + } } + // Run blocking accept loop on IO scheduler to avoid ANR. + // zh-CN: 在 IO 调度器运行阻塞 accept 循环, 避免 ANR. + .subscribeOn(Schedulers.io()) + } + + internal fun onServerClientDisconnected(conn: JsonSocketServer) { + // Remove disconnected connection and update count. + // zh-CN: 移除已断开连接并更新计数. + mServerConnections.remove(conn) + serverConnectionCount.onNext(mServerConnections.size) } - // FIXME by SuperMonster003 as of Dec 29, 2021. - // ! Would print double (may be even more times) the amount of - // ! messages on VSCode when multi connection were established. @AnyThread fun print(log: String?) { jsonSocketClient?.writeLog(log) - jsonSocketServer?.writeLog(log) + + // Broadcast logs to all active server-side connections. + // zh-CN: 向所有服务端已连接客户端广播日志. + mServerConnections.forEach { it.writeLog(log) } } companion object { + const val TYPE_HELLO = "hello" const val TYPE_COMMAND = "command" const val TYPE_BYTES_COMMAND = "bytes_command" - fun setState(cxn: PublishSubject, state: Int) { + + fun setState(cxn: Subject, state: Int) { cxn.onNext(State(state)) } - fun setState(cxn: PublishSubject, state: Int, e: Throwable?) { + fun setState(cxn: Subject, state: Int, e: Throwable?) { cxn.onNext(State(state, e)) } + } } \ 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 index a556c888..1c5235d6 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java +++ b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java @@ -1,8 +1,5 @@ package org.autojs.autojs.pluginclient; -import static java.nio.charset.StandardCharsets.ISO_8859_1; -import static java.nio.charset.StandardCharsets.UTF_8; - import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; @@ -19,22 +16,25 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.stream.JsonReader; - +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.subjects.PublishSubject; +import io.reactivex.subjects.Subject; +import okio.ByteString; import org.autojs.autojs.runtime.api.Device; import org.autojs.autojs.tool.MapBuilder; import org.autojs.autojs6.BuildConfig; import org.autojs.autojs6.R; import org.mozilla.javascript.NativeObject; +import java.io.BufferedInputStream; 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.ByteOrder; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.Arrays; @@ -44,64 +44,59 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.subjects.PublishSubject; -import okio.ByteString; +import static java.nio.charset.StandardCharsets.UTF_8; abstract public class JsonSocket extends Socket { - private static final String TAG = "JsonSocket"; - - protected static final HashMap sBytes = new HashMap<>(); - - protected static final HashMap sRequiredBytesCommands = new HashMap<>(); - private PublishSubject mJsonElementPublishSubject; - private PublishSubject mBytesPublishSubject; - - public E getLast(Collection c) { - E last = null; - for (E e : c) last = e; - return last; - } - - private Fragment mFragment = null; - - private final Context mContext; - private final DevPluginService mService; - - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - public static final int HEADER_SIZE = 16; + public static final int HEADER_SIZE = 8; public static final int HANDSHAKE_TIMEOUT = 6400; public static final String TYPE_HELLO = DevPluginService.TYPE_HELLO; public static final String TYPE_COMMAND = DevPluginService.TYPE_COMMAND; 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; + protected static final HashMap sBytes = new HashMap<>(); + protected static final HashMap sRequiredBytesCommands = new HashMap<>(); - public Bytes(String md5, ByteString byteString) { - this.md5 = md5; - this.byteString = byteString; - this.timestamp = System.currentTimeMillis(); - } - } - - public static class Type { - public static int JSON = 1; - public static int BYTES = 2; - } + private static final String TAG = "JsonSocket"; public final android.os.Handler mHandler = new Handler(Looper.getMainLooper()); + // Serialize writes to prevent interleaving between threads. + // zh-CN: 串行化写入, 避免多线程导致帧交错. + private final Object mWriteLock = new Object(); + + private final Context mContext; + private final DevPluginService mService; + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + private PublishSubject mJsonElementPublishSubject; + private PublishSubject mBytesPublishSubject; + public JsonSocket(DevPluginService service) { mService = service; mContext = service.getContext(); } + private static void readFully(InputStream in, byte[] buffer, int offset, int length) throws IOException { + int read; + int total = 0; + while (total < length) { + read = in.read(buffer, offset + total, length - total); + if (read < 0) { + throw new IOException("Stream ended unexpectedly"); + } + total += read; + } + } + + public E getLast(Collection c) { + E last = null; + for (E e : c) last = e; + return last; + } + protected Context getContext() { return mContext; } @@ -124,12 +119,14 @@ abstract public class JsonSocket extends Socket { @SuppressLint("CheckResult") public JsonSocket subscribeMessage() { mJsonElementPublishSubject = PublishSubject.create(); - mJsonElementPublishSubject.observeOn(AndroidSchedulers.mainThread()); - mJsonElementPublishSubject.subscribe(this::onSocketData, this::onSocketError); + mJsonElementPublishSubject + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onSocketData, this::onSocketError); mBytesPublishSubject = PublishSubject.create(); - mBytesPublishSubject.observeOn(AndroidSchedulers.mainThread()); - mBytesPublishSubject.subscribe(this::onSocketData, this::onSocketError); + mBytesPublishSubject + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onSocketData, this::onSocketError); return this; } @@ -182,72 +179,6 @@ abstract public class JsonSocket extends Socket { } } - private void onMessageDispatch(char[] bytes) { - Log.d(TAG, "Received message total bytes: " + Arrays.toString(bytes)); - Log.d(TAG, "Received message total length: " + bytes.length); - - char[] overload; - - if (mFragment != null) { - overload = mFragment.splice(bytes); - } else { - String header = new String(Arrays.copyOfRange(bytes, 0, HEADER_SIZE)); - - int dataSize = parseHeaderInt(header, 0, HEADER_SIZE - 2); - Log.d(TAG, "Data length from header: " + dataSize); - if (dataSize < 0) { - Log.e(TAG, "Invalid data length, ignored"); - return; - } - - int dataType = parseHeaderInt(header, HEADER_SIZE - 2, 2); - Log.d(TAG, "Data type from header: " + dataType); - if (dataType < 0) { - Log.e(TAG, "Unknown data type, ignored"); - return; - } - - mFragment = new Fragment(dataSize, dataType); - if (HEADER_SIZE > bytes.length) { - Log.e(TAG, "Bytes length (" + bytes.length + ") is less than header size (" + HEADER_SIZE + "), ignored"); - return; - } - overload = mFragment.splice(Arrays.copyOfRange(bytes, HEADER_SIZE, bytes.length)); - } - - if (!mFragment.isRestored()) { - return; - } - char[] restored = mFragment.getRestoredBytes(); - int type = mFragment.getAimDataType(); - mFragment = null; - - if (type == Type.JSON) { - onMessage(new String(charsToBytes(restored, UTF_8), UTF_8)); - } else if (type == Type.BYTES) { - onMessage(ByteString.of(charsToBytes(restored, ISO_8859_1))); - } else { - Log.e(TAG, "Unknown data type (" + type + ") for message dispatching"); - return; - } - - if (overload != null) { - onMessageDispatch(overload); - } - } - - private static int parseHeaderInt(String header, int offset, int length) { - try { - String s = new String(header.getBytes(UTF_8), offset, length).replaceAll("\\D", ""); - if (!s.isEmpty()) { - return Integer.parseInt(s); - } - } catch (Exception e) { - e.printStackTrace(); - } - return -1; - } - @SuppressWarnings("SameParameterValue") private void writeMap(String type, Map map) { JsonObject data = new JsonObject(); @@ -305,6 +236,9 @@ abstract public class JsonSocket extends Socket { writeMessageWithType(getSocket(), json, Type.JSON); } catch (IOException e) { e.printStackTrace(); + // Fail fast on write errors to avoid half-dead connections. + // zh-CN: 写入出错时快速失败, 避免连接处于"半死不活"状态. + onSocketError(e); } } @@ -313,12 +247,14 @@ abstract public class JsonSocket extends Socket { byte[] jsonBytes = getJsonBytes(message); byte[] headerBytes = getHeaderBytes(new int[]{jsonBytes.length, messageType}); - OutputStream os = socket.getOutputStream(); - BufferedOutputStream writer = new BufferedOutputStream(os); + synchronized (mWriteLock) { + OutputStream os = socket.getOutputStream(); + BufferedOutputStream writer = new BufferedOutputStream(os); - writer.write(headerBytes); - writer.write(jsonBytes); - writer.flush(); + writer.write(headerBytes); + writer.write(jsonBytes); + writer.flush(); + } } } @@ -333,22 +269,52 @@ abstract public class JsonSocket extends Socket { // ! 这将导致产生的 MD5 散列值与在 Node.js 环境中得到的 MD5 散列值不匹配. executorService.execute(() -> { - // try (AutoCloseable) { ... } - // @Thank to Zen2H + // Use byte stream to read framed binary protocol reliably. + // zh-CN: 使用字节流读取分帧二进制协议, 提升可靠性. try (InputStream inputStream = socket.getInputStream(); - InputStreamReader inputStreamReader = new InputStreamReader(inputStream, UTF_8); - BufferedReader bufferedReader = new BufferedReader(inputStreamReader) + BufferedInputStream bis = new BufferedInputStream(inputStream) ) { - char[] buff = new char[2048]; - int k; - Log.d(TAG, "bufferedReader is reading..."); - while ((k = bufferedReader.read(buff, 0, buff.length)) > -1 && !socket.isClosed()) { - Log.d(TAG, "read length: " + k); - onMessageDispatch(Arrays.copyOfRange(buff, 0, k)); + + byte[] header = new byte[HEADER_SIZE]; + + while (!socket.isClosed()) { + readFully(bis, header, 0, HEADER_SIZE); + + ByteBuffer hb = ByteBuffer.wrap(header).order(ByteOrder.BIG_ENDIAN); + int dataSize = hb.getInt(); + int dataType = hb.getInt(); + + Log.d(TAG, "Data length from header: " + dataSize); + Log.d(TAG, "Data type from header: " + dataType); + + if (dataSize < 0) { + throw new IOException("Invalid data length: " + dataSize); + } + + byte[] payload = new byte[dataSize]; + readFully(bis, payload, 0, dataSize); + + if (dataType == Type.JSON) { + onMessage(new String(payload, UTF_8)); + } else if (dataType == Type.BYTES) { + onMessage(ByteString.of(payload)); + } else { + throw new IOException("Unknown data type: " + dataType); + } } } catch (IOException e) { e.printStackTrace(); + // Treat "Socket closed" as a normal shutdown path. + // zh-CN: 将 "Socket closed" 视为正常关闭流程. + String message = e.getMessage(); + if (message != null && ( + message.toLowerCase().contains("socket closed") || + message.toLowerCase().contains("stream ended unexpectedly") + )) { + return; + } + onSocketError(e); } finally { if (jsonSocket instanceof JsonSocketClient) { try { @@ -370,11 +336,11 @@ abstract public class JsonSocket extends Socket { return bytes; } - public void setState(PublishSubject cxn, int state) { + public void setState(Subject cxn, int state) { cxn.onNext(new DevPluginService.State(state)); } - public void setState(PublishSubject cxn, int state, Throwable e) { + public void setState(Subject cxn, int state, Throwable e) { cxn.onNext(new DevPluginService.State(state, e)); } @@ -424,54 +390,21 @@ abstract public class JsonSocket extends Socket { return buffer.array(); } - private static class Fragment { + public static class Bytes { + public final String md5; + public final ByteString byteString; + public final long timestamp; - private final int mAimLength; - private final int mAimDataType; - - private char[] bytes = new char[0]; - private int currentLength = 0; - - public Fragment(int aimLength, int aimDataType) { - mAimLength = aimLength; - mAimDataType = aimDataType; - } - - public boolean isRestored() { - return currentLength >= mAimLength; - } - - public char[] splice(char[] charBytes) { - int tempLength = currentLength + charBytes.length; - if (tempLength <= mAimLength) { - this.bytes = joinBytes(this.bytes, charBytes); - currentLength = currentLength + charBytes.length; - Log.d(TAG, "currentLength: " + currentLength + "/" + mAimLength); - return null; - } - int overloadLength = tempLength - mAimLength; - Log.w(TAG, "charBytes overloaded: " + overloadLength); - char[] leftPart = Arrays.copyOfRange(charBytes, 0, charBytes.length - overloadLength); - char[] rightPart = Arrays.copyOfRange(charBytes, charBytes.length - overloadLength, charBytes.length); - - splice(leftPart); - return rightPart; - } - - public char[] getRestoredBytes() { - return bytes; - } - - public int getAimDataType() { - return mAimDataType; - } - - private static char[] joinBytes(final char[] array1, char[] array2) { - char[] joinedArray = Arrays.copyOf(array1, array1.length + array2.length); - System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); - return joinedArray; + public Bytes(String md5, ByteString byteString) { + this.md5 = md5; + this.byteString = byteString; + this.timestamp = System.currentTimeMillis(); } + } + public static class Type { + public static int JSON = 1; + public static int BYTES = 2; } } \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt index 0eb4744e..f4022679 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt +++ b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt @@ -10,11 +10,11 @@ import com.afollestad.materialdialogs.MaterialDialog import com.google.gson.JsonElement import com.google.gson.JsonObject import io.github.g00fy2.versioncompare.Version -import io.reactivex.subjects.PublishSubject -import org.autojs.autojs.AutoJs +import io.reactivex.subjects.BehaviorSubject import org.autojs.autojs.core.pref.Pref import org.autojs.autojs.core.pref.Pref.getBoolean import org.autojs.autojs.core.pref.Pref.putBoolean +import org.autojs.autojs.util.NetworkUtils import org.autojs.autojs.util.StringUtils.key import org.autojs.autojs.util.ViewUtils import org.autojs.autojs6.BuildConfig @@ -24,7 +24,7 @@ import java.net.Socket import java.net.SocketTimeoutException import java.util.concurrent.Executors -class JsonSocketClient(service: DevPluginService?, private val ctx: Context, host: String?, port: Int) : JsonSocket(service) { +class JsonSocketClient(service: DevPluginService?, private val ctx: Context, private val host: String?, port: Int) : JsonSocket(service) { private val jsonSocketExecutor = Executors.newSingleThreadExecutor() @@ -33,7 +33,28 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos var isExtensionVersionCheckFailed = false var hasErrorMessageOnHello = false + // Unique id for guarding against stale state emission from old instances. + // zh-CN: 用于防止旧实例发出的状态污染 UI 的唯一 id. + internal val instanceId: Long = System.nanoTime() + + private val handshakeTimeoutRunnable = Runnable { + if (isExtensionVersionCheckFailed || hasErrorMessageOnHello) { + return@Runnable + } + if (!isSocketReady && !isClientSocketNormallyClosed) { + try { + onHandshakeTimeout() + } catch (e: IOException) { + e.printStackTrace() + } + } + } + init { + // Mark this instance as active as early as possible so CONNECTING state is visible. + // zh-CN: 尽可能早地将此实例标记为 active, 以便 CONNECTING 状态可见. + activeInstanceId = instanceId + jsonSocketExecutor.submit { try { setStateConnecting() @@ -46,7 +67,13 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos } } - override fun isSocketReady() = mSocket?.isConnected == true + override fun isSocketReady(): Boolean { + val s = mSocket ?: return false + return s.isConnected && + !s.isClosed && + !s.isInputShutdown && + !s.isOutputShutdown + } override fun getSocket() = mSocket @@ -61,26 +88,27 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos @Throws(IOException::class) override fun close() { Log.w(TAG, "closing socket...") + cancelHandshakeTimeout() setStateDisconnected() mSocket?.apply { close() } mSocket = null jsonSocketExecutor.shutdown() + + // Clear subtitle on close. + // zh-CN: 关闭连接时清空 subtitle. + service.clientConnectionIpAddress.onNext(NetworkUtils.DEFAULT_IP_ADDRESS) } override fun sayHello() { super.sayHello() - mHandler.postDelayed({ - if (isExtensionVersionCheckFailed || hasErrorMessageOnHello) { - return@postDelayed - } - if (!isSocketReady && !isClientSocketNormallyClosed) { - try { - onHandshakeTimeout() - } catch (e: IOException) { - e.printStackTrace() - } - } - }, HANDSHAKE_TIMEOUT.toLong()) + cancelHandshakeTimeout() + mHandler.postDelayed(handshakeTimeoutRunnable, HANDSHAKE_TIMEOUT.toLong()) + } + + private fun cancelHandshakeTimeout() { + // Cancel pending handshake timeout to avoid stale callbacks. + // zh-CN: 取消挂起的握手超时回调, 避免旧回调误触发. + mHandler.removeCallbacks(handshakeTimeoutRunnable) } override fun getContext() = ctx @@ -93,41 +121,79 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos if (data != null && data.isJsonObject) { val errorMessage = data.asJsonObject["errorMessage"] if (errorMessage != null && errorMessage.isJsonPrimitive) { - ViewUtils.showToast(context, errorMessage.asString, true) + val msg = errorMessage.asString + + if (ctx is Activity) { + ctx.runOnUiThread { + MaterialDialog.Builder(ctx) + .title(ctx.getString(R.string.text_connection_cannot_be_established)) + .content(msg) + .positiveText(R.string.dialog_button_dismiss) + .positiveColorRes(R.color.dialog_button_default) + .build() + .also { + mHandler.post { it.show() } + } + } + } else { + ViewUtils.showToast(ctx, msg, true) + } + hasErrorMessageOnHello = true + + // Mark as disconnected when server rejects handshake (e.g. version mismatch). + // zh-CN: 当服务端拒绝握手 (例如版本不匹配) 时, 标记为已断开. + service.clientConnectionIpAddress.onNext(NetworkUtils.DEFAULT_IP_ADDRESS) + setStateDisconnected(IllegalStateException(errorMessage.asString)) + + try { + switchOff() + } catch (e: IOException) { + e.printStackTrace() + } + return } + val extensionVersion = data.asJsonObject["extensionVersion"] if (extensionVersion != null && extensionVersion.isJsonPrimitive) { currentVersion = extensionVersion.asString if (Version(currentVersion).isAtLeast(requiredVersion)) { + // Handshake accepted. + // zh-CN: 握手通过. + host?.let { service.clientConnectionIpAddress.onNext(it) } + service.isClientSocketNormallyClosed = false setStateConnected() return } isExtensionVersionCheckFailed = true } } + + // Fallback: version check failed or invalid hello. + // zh-CN: 兜底: 版本校验失败或 hello 异常. + service.clientConnectionIpAddress.onNext(NetworkUtils.DEFAULT_IP_ADDRESS) + setStateDisconnected(IllegalStateException("Handshake rejected")) + try { switchOff() } catch (e: IOException) { e.printStackTrace() } - val activity = AutoJs.instance.appUtils.currentActivity ?: context - val msg = """ - ${activity.getString(R.string.text_vsc_ext_version_not_meet_requirement)}. + ${ctx.getString(R.string.text_vsc_ext_version_not_meet_requirement)}. - ${activity.getString(R.string.text_min_version)}: $requiredVersion - ${activity.getString(R.string.text_current_version)}: ${currentVersion ?: "${activity.getString(R.string.text_lower_than)} $requiredVersion"} + ${ctx.getString(R.string.text_min_version)}: $requiredVersion + ${ctx.getString(R.string.text_current_version)}: ${currentVersion ?: "${ctx.getString(R.string.text_lower_than)} $requiredVersion"} - ${activity.getString(R.string.text_repo_url_of_vscode_vsc_ext)}: - ${activity.getString(R.string.url_github_autojs6_vscode_extension_repo)} + ${ctx.getString(R.string.text_repo_url_of_vscode_vsc_ext)}: + ${ctx.getString(R.string.url_github_autojs6_vscode_extension_repo)} """.trimIndent() - if (activity is Activity) { - activity.runOnUiThread { - MaterialDialog.Builder(activity) - .title(activity.getString(R.string.text_connection_cannot_be_established)) + if (ctx is Activity) { + ctx.runOnUiThread { + MaterialDialog.Builder(ctx) + .title(ctx.getString(R.string.text_connection_cannot_be_established)) .content(msg) .positiveText(R.string.dialog_button_dismiss) .positiveColorRes(R.color.dialog_button_default) @@ -142,10 +208,10 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos } } else { val toastMsg = """ - ${activity.getString(R.string.text_min_version_of_vscode_vsc_ext)}: + ${ctx.getString(R.string.text_min_version_of_vscode_vsc_ext)}: $requiredVersion """.trimIndent() - ViewUtils.showToast(activity, toastMsg, true) + ViewUtils.showToast(ctx, toastMsg, true) } } @@ -207,19 +273,40 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos @Throws(IOException::class) fun onHandshakeTimeout() { Log.i(TAG, "onHandshakeTimeout") - setStateDisconnected(SocketTimeoutException(context.getString(R.string.error_handshake_timed_out, HANDSHAKE_TIMEOUT))) + setStateDisconnected(SocketTimeoutException(ctx.getString(R.string.error_handshake_timed_out, HANDSHAKE_TIMEOUT))) switchOff() } override fun monitorMessage() = also { super.monitorMessage(mSocket, this) } - override fun setStateConnected() = also { setState(cxnState, DevPluginService.State.CONNECTED) } + override fun setStateConnected() = also { + cancelHandshakeTimeout() + emitStateIfActive(DevPluginService.State.CONNECTED) + } - private fun setStateConnecting() = also { setState(cxnState, DevPluginService.State.CONNECTING) } + private fun setStateConnecting() = also { + emitStateIfActive(DevPluginService.State.CONNECTING) + } - private fun setStateDisconnected() = also { setState(cxnState, DevPluginService.State.DISCONNECTED) } + private fun setStateDisconnected() = also { + cancelHandshakeTimeout() + emitStateIfActive(DevPluginService.State.DISCONNECTED) + } - private fun setStateDisconnected(e: Throwable?) = also { setState(cxnState, DevPluginService.State.DISCONNECTED, e) } + private fun setStateDisconnected(e: Throwable?) = also { + cancelHandshakeTimeout() + emitStateIfActive(DevPluginService.State.DISCONNECTED, e) + } + + private fun emitStateIfActive(state: Int, e: Throwable? = null) { + // Only the active client instance is allowed to publish state to the global subject. + // zh-CN: 仅允许当前活跃的客户端实例向全局 subject 发布状态. + if (activeInstanceId != instanceId) return + when (e) { + null -> setState(cxnState, state) + else -> setState(cxnState, state, e) + } + } companion object { @@ -241,7 +328,13 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos serverAddressHistories = linkedSetOf() } - val cxnState = PublishSubject.create() + // Track current active instance for guarding state emission. + // zh-CN: 记录当前活跃实例, 用于防止旧实例状态污染. + @Volatile + internal var activeInstanceId: Long = 0L + + val cxnState: BehaviorSubject = + BehaviorSubject.createDefault(DevPluginService.State(DevPluginService.State.DISCONNECTED)) var isClientSocketNormallyClosed get() = getBoolean(key(R.string.key_client_socket_normally_closed), true) diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketServer.kt b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketServer.kt index 6eb4402f..f9e1270d 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketServer.kt +++ b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketServer.kt @@ -4,11 +4,10 @@ import android.util.Log import androidx.annotation.MainThread import androidx.annotation.WorkerThread import com.google.gson.JsonElement -import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.BehaviorSubject import org.autojs.autojs.core.pref.Pref.getBoolean import org.autojs.autojs.core.pref.Pref.putBoolean import org.autojs.autojs.util.StringUtils.key -import org.autojs.autojs.util.ViewUtils import org.autojs.autojs6.R import java.io.IOException import java.net.ServerSocket @@ -29,7 +28,13 @@ class JsonSocketServer(service: DevPluginService?, port: Int) : JsonSocket(servi } } - override fun isSocketReady() = mSocket != null && !mSocket!!.isClosed + override fun isSocketReady(): Boolean { + val s = mSocket ?: return false + return s.isConnected && + !s.isClosed && + !s.isInputShutdown && + !s.isOutputShutdown + } private val isServerSocketSetUp get() = serverSocket != null && !serverSocket!!.isClosed @@ -105,8 +110,20 @@ class JsonSocketServer(service: DevPluginService?, port: Int) : JsonSocket(servi @MainThread public override fun onSocketError(e: Throwable) { + Log.w(TAG, "onSocketError") e.printStackTrace() - ViewUtils.showToast(context, e.message) + + // Close client socket and keep listening socket alive. + // zh-CN: 关闭客户端 socket, 保持监听 socket 存活. + try { + close() + } catch (ex: Exception) { + ex.printStackTrace() + } + + // Notify service to update connection count. + // zh-CN: 通知 service 更新连接计数. + service.onServerClientDisconnected(this) } override fun setStateConnected() = also { setState(cxnState, DevPluginService.State.CONNECTED) } @@ -119,7 +136,10 @@ class JsonSocketServer(service: DevPluginService?, port: Int) : JsonSocket(servi private val TAG = JsonSocketServer::class.java.simpleName - val cxnState = PublishSubject.create() + // Replay latest state to new subscribers (e.g. after language change / recreation). + // zh-CN: 向新订阅者回放最新状态 (例如切换语言/重建后). + val cxnState: BehaviorSubject = + BehaviorSubject.createDefault(DevPluginService.State(DevPluginService.State.DISCONNECTED)) var isServerSocketNormallyClosed get() = getBoolean(key(R.string.key_server_socket_normally_closed), true) diff --git a/app/src/main/java/org/autojs/autojs/service/AccessibilityService.kt b/app/src/main/java/org/autojs/autojs/service/AccessibilityService.kt index 9aa650b5..eefb802d 100644 --- a/app/src/main/java/org/autojs/autojs/service/AccessibilityService.kt +++ b/app/src/main/java/org/autojs/autojs/service/AccessibilityService.kt @@ -13,8 +13,8 @@ open class AccessibilityService(final override val context: Context) : ServiceIt override val isRunning get() = mA11yTool.hasService() || mA11yTool.isRunning() - override fun active(): Boolean { - return mA11yTool.restartService(true) + override fun active() { + mA11yTool.restartService(true) } override fun start(): Boolean { diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt index 7d2edb37..327a4d0b 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt @@ -9,7 +9,9 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers +import org.autojs.autojs.App import org.autojs.autojs.app.tool.FloatingButtonTool import org.autojs.autojs.app.tool.JsonSocketClientTool import org.autojs.autojs.app.tool.JsonSocketServerTool @@ -54,7 +56,7 @@ import kotlin.math.roundToInt /** * Created by Stardust on Jan 30, 2017. - * Modified by SuperMonster003 as of Nov 16, 2021. + * Modified by SuperMonster003 as of Jan 9, 2026. * Transformed by SuperMonster003 on Sep 19, 2022. */ open class DrawerFragment : Fragment() { @@ -185,42 +187,78 @@ open class DrawerFragment : Fragment() { R.string.key_floating_menu_shown, ) - JsonSocketClientTool(mContext).apply { - mClientModeItem = DrawerMenuDisposableItem(this, R.drawable.ic_computer_black_48dp, R.string.text_client_mode).also { - setClientModeItem(it) + JsonSocketClientTool(mActivity).apply { + val devPluginService = App.app.devPluginService + + val drawerItem = DrawerMenuDisposableItem(this, R.drawable.ic_computer_black_48dp, R.string.text_client_mode).also { + mClientModeItem = it } - setStateDisposable( - JsonSocketClient.cxnState - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - if (it.state == DevPluginService.State.DISCONNECTED) { - mClientModeItem.subtitle = null - } - consumeJsonSocketItemState(mClientModeItem, it) - }) + + val disposable = Observable + .combineLatest( + JsonSocketClient.cxnState, + devPluginService.clientConnectionIpAddress, + ) { state: DevPluginService.State, ip: String -> + Pair(state, ip) + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { (state, ip) -> + drawerItem.subtitle = when { + state.isDisconnected() -> null + else -> ip + } + drawerItem.setCheckedIfNeeded(state.isConnected()) + drawerItem.isProgress = state.isConnecting() + state.exception?.let { e -> + drawerItem.subtitle = null + ViewUtils.showToast(mContext, e.message) + } + } + setStateDisposable(disposable) setOnConnectionException { e: Throwable -> - mClientModeItem.setCheckedIfNeeded(false) + drawerItem.setCheckedIfNeeded(false) ViewUtils.showToast(context, getString(R.string.error_connect_to_remote, e.message), true) } - setOnConnectionDialogDismissed { mClientModeItem.setCheckedIfNeeded(false) } + setOnConnectionDialogDismissed { drawerItem.setCheckedIfNeeded(false) } connectIfNotNormallyClosed() } - JsonSocketServerTool(mContext).apply { - setStateDisposable( - JsonSocketServer.cxnState - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { state: DevPluginService.State -> - mServerModeItem.subtitle = takeIf { state.state == DevPluginService.State.CONNECTED }?.let { - NetworkUtils.getIpAddress() + JsonSocketServerTool(mActivity).apply { + val devPluginService = App.app.devPluginService + + val drawerItem = DrawerMenuDisposableItem(this, R.drawable.ic_smartphone_black_48dp, R.string.text_server_mode).also { + mServerModeItem = it + } + + val disposable = Observable + .combineLatest( + JsonSocketServer.cxnState, + devPluginService.serverConnectionCount, + ) { state: DevPluginService.State, count: Int -> + Pair(state, count) + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { (state, count) -> + drawerItem.subtitle = when { + state.isDisconnected() -> null + else -> NetworkUtils.getIpAddress().let { ip -> + when { + count > 0 -> "$ip [ ${context.getString(R.string.text_connected_count_with_colon, count)} ]" + else -> ip + } } - consumeJsonSocketItemState(mServerModeItem, state) - }) + } + drawerItem.setCheckedIfNeeded(!state.isDisconnected()) + drawerItem.isProgress = state.isConnecting() + state.exception?.let { e -> + ViewUtils.showToast(mContext, e.message) + } + } + setStateDisposable(disposable) setOnConnectionException { e: Throwable -> - mServerModeItem.setCheckedIfNeeded(false) + drawerItem.setCheckedIfNeeded(false) ViewUtils.showToast(context, getString(R.string.error_enable_server, e.message), true) } - mServerModeItem = DrawerMenuDisposableItem(this, R.drawable.ic_smartphone_black_48dp, R.string.text_server_mode) connectIfNotNormallyClosed() } @@ -530,15 +568,6 @@ open class DrawerFragment : Fragment() { mKeepScreenOnWhenInForegroundItem, ).forEach { it.sync() } - private fun consumeJsonSocketItemState(item: DrawerMenuToggleableItem, state: DevPluginService.State) { - item.setCheckedIfNeeded(state.state == DevPluginService.State.CONNECTED) - item.isProgress = state.state == DevPluginService.State.CONNECTING - state.exception?.let { e -> - item.subtitle = null - ViewUtils.showToast(mContext, e.message) - } - } - companion object { lateinit var drawerMenuAdapter: DrawerMenuAdapter diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemCustomHelper.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemCustomHelper.kt index 3b347f3d..ec5bddab 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemCustomHelper.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemCustomHelper.kt @@ -7,9 +7,8 @@ abstract class DrawerMenuItemCustomHelper internal constructor(private val mCont override val context: Context get() = mContext - override fun active(): Boolean { - if (isActive) return true - return toggle() + override fun active() { + if (!isActive) toggle() } } \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemHelper.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemHelper.kt index baf1f4eb..89eed017 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemHelper.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemHelper.kt @@ -15,7 +15,7 @@ interface DrawerMenuItemHelper { val isInMainThread: Boolean get() = false - fun active(): Boolean + fun active() fun toggle(): Boolean diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/PermissionItemHelper.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/PermissionItemHelper.kt index caa6eff3..8c89cc4b 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/PermissionItemHelper.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/PermissionItemHelper.kt @@ -5,9 +5,8 @@ interface PermissionItemHelper : DrawerMenuItemHelper { override val isActive get() = has() - override fun active(): Boolean { - if (has()) return true - return request() + override fun active() { + requestIfNeeded() } fun has(): Boolean diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/RunnableItemHelper.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/RunnableItemHelper.kt index 18e75bd5..2f0ab675 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/RunnableItemHelper.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/RunnableItemHelper.kt @@ -5,9 +5,8 @@ interface RunnableItemHelper : DrawerMenuItemHelper { override val isActive: Boolean get() = isRunning - override fun active(): Boolean { - if (isRunning) return true - return launch() + override fun active() { + launchIfNeeded() } val isRunning: Boolean diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/ServiceItemHelper.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/ServiceItemHelper.kt index 2889c734..0fe8523e 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/ServiceItemHelper.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/ServiceItemHelper.kt @@ -5,9 +5,8 @@ interface ServiceItemHelper : DrawerMenuItemHelper { override val isActive: Boolean get() = isRunning - override fun active(): Boolean { - if (isRunning) return true - return start() + override fun active() { + startIfNeeded() } val isRunning: Boolean diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/ShowableItemHelper.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/ShowableItemHelper.kt index bd432ce1..c9407949 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/ShowableItemHelper.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/ShowableItemHelper.kt @@ -5,9 +5,8 @@ interface ShowableItemHelper : DrawerMenuItemHelper { override val isActive: Boolean get() = isShowing - override fun active(): Boolean { - if (isShowing) return true - return show() + override fun active() { + showIfNeeded() } val isShowing: Boolean diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/SocketItemHelper.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/SocketItemHelper.kt index d04fe6df..09967dc2 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/SocketItemHelper.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/SocketItemHelper.kt @@ -5,14 +5,15 @@ interface SocketItemHelper : DrawerMenuItemHelper { override val isActive get() = isConnected - override fun active(): Boolean { - if (isConnected) return true - return connect() + override fun active() { + connectIfNeeded() } val isConnected: Boolean - fun connect(): Boolean + var isNormallyClosed: Boolean + + fun connect() fun dispose() @@ -20,7 +21,7 @@ interface SocketItemHelper : DrawerMenuItemHelper { if (!isConnected) connect() } - fun disconnect(): Boolean + fun disconnect() fun disconnectIfNeeded() { if (isConnected) disconnect() @@ -28,6 +29,8 @@ interface SocketItemHelper : DrawerMenuItemHelper { override fun toggle() = toggle(!isConnected) - override fun toggle(aimState: Boolean): Boolean = if (aimState) connect() else disconnect() + override fun toggle(aimState: Boolean): Boolean = runCatching { + if (aimState) connect() else disconnect() + }.isSuccess } \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/util/NetworkUtils.kt b/app/src/main/java/org/autojs/autojs/util/NetworkUtils.kt index 54d66a4b..7a97ca89 100644 --- a/app/src/main/java/org/autojs/autojs/util/NetworkUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/NetworkUtils.kt @@ -20,11 +20,13 @@ import java.util.Locale /** * Created by Stardust on Apr 9, 2017. - * Modified by SuperMonster003 as of Jun 3, 2022. + * Modified by SuperMonster003 as of Jan 9, 2026. */ @Suppress("DEPRECATION", "unused") object NetworkUtils { + const val DEFAULT_IP_ADDRESS = "0.0.0.0" + private val globalAppContext = GlobalAppContext.get() private val connectivityManager @@ -47,7 +49,7 @@ object NetworkUtils { } catch (e: UnknownHostException) { Log.e(NetworkUtils::class.java.simpleName, "Error getting Hotspot IP address ", e) null - } ?: "0.0.0.0" + } ?: DEFAULT_IP_ADDRESS private fun getIpAddressByte(): ByteArray { val manager = globalAppContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager @@ -185,7 +187,7 @@ object NetworkUtils { } catch (ignored: Exception) { // for now eat exceptions } - return "0.0.0.0" + return DEFAULT_IP_ADDRESS } @JvmStatic diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 4af6c1b9..ffab9d12 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1140,5 +1140,7 @@ تأخير البدء متقدم تعذّر حفظ إعدادات المشروع + متصل + متصل: %1$d diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 896640b6..c744096e 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1135,5 +1135,7 @@ Start delay Advanced Failed to save project config + Connected + Connected: %1$d diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index a9cf2a8e..570a2720 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1138,5 +1138,7 @@ Retraso de inicio Avanzado No se pudo guardar la configuración del proyecto + Conectado + Conectados: %1$d diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8d13f011..ef114b56 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1138,5 +1138,7 @@ Délai de démarrage Avancé Échec de l\'enregistrement de la configuration du projet + Connecté + Connectés : %1$d diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index fb5c43e6..8812480b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1139,5 +1139,7 @@ 開始遅延 詳細 プロジェクト設定の保存に失敗しました + 接続済み + 接続済み: %1$d diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 4ef08e7c..4ca9b1b7 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1140,5 +1140,7 @@ 시작 지연 고급 프로젝트 설정을 저장하지 못했습니다 + 연결됨 + 연결됨: %1$d diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ac241806..238bb05d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1138,5 +1138,7 @@ Задержка старта Доп. Не удалось сохранить конфигурацию проекта + Подключено + Подключено: %1$d diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index edd09403..813481d5 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1136,5 +1136,7 @@ 啓動延遲 高級設置 保存項目配置失敗 + 已連接 + 已連接: %1$d diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1cc4602f..60701b3d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1136,5 +1136,7 @@ 啟動延遲 高階設定 儲存專案配置失敗 + 已連線 + 已連線: %1$d diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 250f143c..84771c3b 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1136,5 +1136,7 @@ 启动延迟 高级设置 保存项目配置失败 + 已连接 + 已连接: %1$d diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cceddd88..bc1b761b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1393,5 +1393,7 @@ Start delay Advanced Failed to save project config + Connected + Connected: %1$d diff --git a/version.properties b/version.properties index bb4bd146..8db49ad5 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Thu Jan 08 01:30:58 CST 2026 -BUILD_TIME=1767807058123 +#Fri Jan 09 23:39:38 CST 2026 +BUILD_TIME=1767973178586 COMPILE_SDK_VERSION=36 IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_NDK_VERSION=26.1.10909125 @@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=36 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=3583 -VERSION_NAME=6.7.0 Alpha13 -VSCODE_EXT_REQUIRED_VERSION=1.0.8 +VERSION_BUILD=3591 +VERSION_NAME=6.7.0 Alpha14 +VSCODE_EXT_REQUIRED_VERSION=1.0.13