6.7.0 - Alpha14 - 修复服务端模式连接时, Activity 重建操作导致 VSCode 控制台无法输出日志的问题 (issue #385); 深度优化 VSCode 插件连接相关代码逻辑
This commit is contained in:
@@ -277,7 +277,7 @@
|
|||||||
"دعم التعرف الذكي على موقع الإدراج عند استيراد فئات وحزم Java في محرر الأكواد",
|
"دعم التعرف الذكي على موقع الإدراج عند استيراد فئات وحزم Java في محرر الأكواد",
|
||||||
"دعم استخدام المسارات كمعلمات الصور مباشرة في وحدة images",
|
"دعم استخدام المسارات كمعلمات الصور مباشرة في وحدة images",
|
||||||
"دعم المعلمات في استيراد الحزم باستخدام importPackage",
|
"دعم المعلمات في استيراد الحزم باستخدام importPackage",
|
||||||
"دعم التعرف الذكي على عناوين IP في وضع الخادم من الحافظة ودعم التحويل الذكي باستخدام مفتاح المسافة",
|
"في وضع العميل، يدعم عنوان IP الاستيراد من الحافظة مع تعرّف ذكي، ويدعم التحويل الذكي لإدخال مفتاح المسافة",
|
||||||
"دعم إنشاء الملفات برقم تسلسلي افتراضي ومناسب مع اختيار تلقائي للبادئة في مدير الملفات",
|
"دعم إنشاء الملفات برقم تسلسلي افتراضي ومناسب مع اختيار تلقائي للبادئة في مدير الملفات",
|
||||||
"توفر رسائل خطأ مفصلة عند تشغيل مشروع معين في مدير الملفات _[`issue #268`](http://issues.autojs6.com/268)_",
|
"توفر رسائل خطأ مفصلة عند تشغيل مشروع معين في مدير الملفات _[`issue #268`](http://issues.autojs6.com/268)_",
|
||||||
"دعم المزيد من أنواع الملفات ودعم عرض الرموز المقابلة (يدعم أكثر من 800 نوع من الملفات) في مدير الملفات",
|
"دعم المزيد من أنواع الملفات ودعم عرض الرموز المقابلة (يدعم أكثر من 800 نوع من الملفات) في مدير الملفات",
|
||||||
|
|||||||
@@ -277,7 +277,7 @@
|
|||||||
"Code editor supports intelligently determining insertion location when importing Java classes and package names",
|
"Code editor supports intelligently determining insertion location when importing Java classes and package names",
|
||||||
"images module supports using paths directly as image parameters",
|
"images module supports using paths directly as image parameters",
|
||||||
"importPackage supports string 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 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 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)",
|
"File manager supports more types and displays corresponding icon symbols (supports over 800 file types)",
|
||||||
|
|||||||
@@ -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 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",
|
"El módulo images admite el uso de rutas directamente como parámetros de imagen",
|
||||||
"importPackage admite parámetros de cadena",
|
"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 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 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)",
|
"El administrador de archivos admite más tipos y muestra símbolos de icono correspondientes (admite más de 800 tipos de archivos)",
|
||||||
|
|||||||
@@ -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",
|
"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",
|
"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",
|
"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 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 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)",
|
"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)",
|
||||||
|
|||||||
@@ -277,7 +277,7 @@
|
|||||||
"コードエディタで Java クラスやパッケージ名をインポートする位置のインテリジェント判断をサポート",
|
"コードエディタで Java クラスやパッケージ名をインポートする位置のインテリジェント判断をサポート",
|
||||||
"images モジュールが画像パラメータとしてパスの直接使用をサポート",
|
"images モジュールが画像パラメータとしてパスの直接使用をサポート",
|
||||||
"importPackage が文字列パラメータをサポート",
|
"importPackage が文字列パラメータをサポート",
|
||||||
"サーバーモードの IP アドレスがクリップボードインポートをスマートに認識し, スペースキーのスマート変換をサポート",
|
"クライアントモードでは, IP アドレスがクリップボードからの取り込みに対応し, インテリジェントに認識します. さらに, スペースキー入力の自動変換にも対応します",
|
||||||
"ファイルマネージャが新しいファイルを作成する際にデフォルトのプレフィックス選択と適切な数字サフィックスの自動生成をサポート",
|
"ファイルマネージャが新しいファイルを作成する際にデフォルトのプレフィックス選択と適切な数字サフィックスの自動生成をサポート",
|
||||||
"ファイル管理プロジェクトの実行時の例外メッセージを具体化 _[`issue #268`](http://issues.autojs6.com/268)_",
|
"ファイル管理プロジェクトの実行時の例外メッセージを具体化 _[`issue #268`](http://issues.autojs6.com/268)_",
|
||||||
"ファイルマネージャがより多くの種類をサポートし, 対応するアイコンシンボルを表示 (800 種類以上のファイルタイプをサポート)",
|
"ファイルマネージャがより多くの種類をサポートし, 対応するアイコンシンボルを表示 (800 種類以上のファイルタイプをサポート)",
|
||||||
|
|||||||
@@ -277,7 +277,7 @@
|
|||||||
"코드 편집기에서 Java 클래스나 패키지명을 가져오는 위치의 지능형 판단을 지원",
|
"코드 편집기에서 Java 클래스나 패키지명을 가져오는 위치의 지능형 판단을 지원",
|
||||||
"images 모듈이 이미지 매개변수로 경로의 직접 사용을 지원",
|
"images 모듈이 이미지 매개변수로 경로의 직접 사용을 지원",
|
||||||
"importPackage 가 문자열 매개변수를 지원",
|
"importPackage 가 문자열 매개변수를 지원",
|
||||||
"서버 모드의 IP 주소가 클립보드 가져오기를 스마트하게 인식하고, 스페이스 키 스마트 변환을 지원",
|
"클라이언트 모드에서 IP 주소는 클립보드 가져오기를 지능적으로 인식하며, 공백 키 입력을 지능적으로 변환하는 기능도 지원합니다",
|
||||||
"파일 관리자가 새 파일을 생성할 때 기본 접두사 선택 및 적절한 숫자 접미사의 자동 생성을 지원",
|
"파일 관리자가 새 파일을 생성할 때 기본 접두사 선택 및 적절한 숫자 접미사의 자동 생성을 지원",
|
||||||
"파일 관리 프로젝트 실행 시 예외 메시지 구체화 _[`issue #268`](http://issues.autojs6.com/268)_",
|
"파일 관리 프로젝트 실행 시 예외 메시지 구체화 _[`issue #268`](http://issues.autojs6.com/268)_",
|
||||||
"파일 관리자가 더 많은 유형을 지원하고, 해당 아이콘 심볼을 표시 (800 이상의 파일 타입 지원)",
|
"파일 관리자가 더 많은 유형을 지원하고, 해당 아이콘 심볼을 표시 (800 이상의 파일 타입 지원)",
|
||||||
|
|||||||
@@ -277,7 +277,7 @@
|
|||||||
"Редактор кода поддерживает интеллектуальное определение места вставки при импорте Java классов и пакетов имен",
|
"Редактор кода поддерживает интеллектуальное определение места вставки при импорте Java классов и пакетов имен",
|
||||||
"Модуль images поддерживает использование путей напрямую в качестве параметров изображений",
|
"Модуль images поддерживает использование путей напрямую в качестве параметров изображений",
|
||||||
"importPackage поддерживает строковые параметры",
|
"importPackage поддерживает строковые параметры",
|
||||||
"IP-адрес в режиме сервера поддерживает импорт из буфера обмена с интеллектуальным определением и конверсией с пробелом",
|
"В режиме клиента IP-адрес поддерживает умное распознавание при вставке из буфера обмена и интеллектуальное преобразование ввода через пробел",
|
||||||
"Файловый менеджер поддерживает выбор префиксов по умолчанию при создании новых файлов и автоматически генерирует подходящее числовое расширение",
|
"Файловый менеджер поддерживает выбор префиксов по умолчанию при создании новых файлов и автоматически генерирует подходящее числовое расширение",
|
||||||
"Файловый менеджер четко уведомляет об ошибке при выполнении проекта _[`issue #268`](http://issues.autojs6.com/268)_",
|
"Файловый менеджер четко уведомляет об ошибке при выполнении проекта _[`issue #268`](http://issues.autojs6.com/268)_",
|
||||||
"Файловый менеджер поддерживает больше типов и отображает соответствующие значки (поддерживает более 800 типов файлов)",
|
"Файловый менеджер поддерживает больше типов и отображает соответствующие значки (поддерживает более 800 типов файлов)",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$data": {
|
"$data": {
|
||||||
"v6.7.0": {
|
"v6.7.0": {
|
||||||
"released_date": "2026/01/08",
|
"released_date": "2026/01/09",
|
||||||
"feature": [
|
"feature": [
|
||||||
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)",
|
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)",
|
||||||
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
|
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
|
||||||
@@ -81,6 +81,9 @@
|
|||||||
"文件管理器删除项目文件夹后 UI 未能自动刷新的问题",
|
"文件管理器删除项目文件夹后 UI 未能自动刷新的问题",
|
||||||
"使用 \"三按钮\" 手势导航时导航栏前景色可能与夜间模式关联异常的问题",
|
"使用 \"三按钮\" 手势导航时导航栏前景色可能与夜间模式关联异常的问题",
|
||||||
"APK 文件类型信息对话框可能无法获取应用名称及 SDK 信息的问题",
|
"APK 文件类型信息对话框可能无法获取应用名称及 SDK 信息的问题",
|
||||||
|
"客户端模式连接时, 旋转屏幕及切换语言等触发 Activity 重建的操作导致 AutoJs6 总是重建连接的问题",
|
||||||
|
"服务端模式连接时, 旋转屏幕及切换语言等触发 Activity 重建的操作导致 VSCode 控制台无法输出日志的问题 _[`issue #385`](http://issues.autojs6.com/385)_",
|
||||||
|
"连接 VSCode 插件时, 多种方式同时连接可能导致日志打印数量成倍增加的问题",
|
||||||
"构建工具启用 isCleanup[Paddle/Rapid]Ocr 配置选项时无法正常完成 Rebuild Project 任务的问题"
|
"构建工具启用 isCleanup[Paddle/Rapid]Ocr 配置选项时无法正常完成 Rebuild Project 任务的问题"
|
||||||
],
|
],
|
||||||
"improvement": [
|
"improvement": [
|
||||||
@@ -101,6 +104,7 @@
|
|||||||
"崩溃报告页面支持双指缩放调整字体大小并添加常用功能按钮",
|
"崩溃报告页面支持双指缩放调整字体大小并添加常用功能按钮",
|
||||||
"应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_",
|
"应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_",
|
||||||
"Rhino 引擎在泛型签名解析失败时回退为原始反射类型以增强低版本安卓系统的反射方法可用性",
|
"Rhino 引擎在泛型签名解析失败时回退为原始反射类型以增强低版本安卓系统的反射方法可用性",
|
||||||
|
"服务端模式连接时支持显示已建立连接的客户端数量",
|
||||||
"使用 LiveData 及 SharedFlow 替代已弃用的 LocalBroadcastManager",
|
"使用 LiveData 及 SharedFlow 替代已弃用的 LocalBroadcastManager",
|
||||||
"Gradle 构建脚本提升 7z 格式文件的解压效率",
|
"Gradle 构建脚本提升 7z 格式文件的解压效率",
|
||||||
"Gradle 构建脚本支持获取详细的 Android Studio IDE 版本 (如 \"2025.1.4.7\")",
|
"Gradle 构建脚本支持获取详细的 Android Studio IDE 版本 (如 \"2025.1.4.7\")",
|
||||||
@@ -414,7 +418,7 @@
|
|||||||
"代码编辑器导入 Java 类和包名时支持智能判断插入位置",
|
"代码编辑器导入 Java 类和包名时支持智能判断插入位置",
|
||||||
"images 模块支持直接使用路径作为图像参数",
|
"images 模块支持直接使用路径作为图像参数",
|
||||||
"importPackage 支持字符串参数",
|
"importPackage 支持字符串参数",
|
||||||
"服务端模式 IP 地址支持剪切板导入智能识别且支持空格按键智能转换",
|
"客户端模式 IP 地址支持剪切板导入智能识别且支持空格按键智能转换",
|
||||||
"文件管理器新建文件时支持默认前缀选择并自动生成合适的数字后缀",
|
"文件管理器新建文件时支持默认前缀选择并自动生成合适的数字后缀",
|
||||||
"文件管理器运行项目时具体化异常消息提示 _[`issue #268`](http://issues.autojs6.com/268)_",
|
"文件管理器运行项目时具体化异常消息提示 _[`issue #268`](http://issues.autojs6.com/268)_",
|
||||||
"文件管理器支持更多类型并支持显示对应的图标符号 (支持 800 多种文件类型)",
|
"文件管理器支持更多类型并支持显示对应的图标符号 (支持 800 多种文件类型)",
|
||||||
|
|||||||
@@ -277,7 +277,7 @@
|
|||||||
"代碼編輯器導入 Java 類和包名時支持智能判斷插入位置",
|
"代碼編輯器導入 Java 類和包名時支持智能判斷插入位置",
|
||||||
"images 模塊支持直接使用路徑作為圖像參數",
|
"images 模塊支持直接使用路徑作為圖像參數",
|
||||||
"importPackage 支持字符串參數",
|
"importPackage 支持字符串參數",
|
||||||
"服務端模式 IP 地址支持剪切板導入智能識別且支持空格按鍵智能轉換",
|
"客户端模式 IP 地址支持剪切板導入智能識別且支持空格按鍵智能轉換",
|
||||||
"文件管理器新建文件時支持默認前綴選擇並自動生成合適的數字後綴",
|
"文件管理器新建文件時支持默認前綴選擇並自動生成合適的數字後綴",
|
||||||
"文件管理器運行項目時具體化異常消息提示 _[`issue #268`](http://issues.autojs6.com/268)_",
|
"文件管理器運行項目時具體化異常消息提示 _[`issue #268`](http://issues.autojs6.com/268)_",
|
||||||
"文件管理器支持更多類型並支持顯示對應的圖標符號 (支持 800 多種文件類型)",
|
"文件管理器支持更多類型並支持顯示對應的圖標符號 (支持 800 多種文件類型)",
|
||||||
|
|||||||
@@ -277,7 +277,7 @@
|
|||||||
"程式碼編輯器匯入 Java 類和包名時支援智慧判斷插入位置",
|
"程式碼編輯器匯入 Java 類和包名時支援智慧判斷插入位置",
|
||||||
"images 模組支援直接使用路徑作為影象引數",
|
"images 模組支援直接使用路徑作為影象引數",
|
||||||
"importPackage 支援字串引數",
|
"importPackage 支援字串引數",
|
||||||
"服務端模式 IP 地址支援剪下板匯入智慧識別且支援空格按鍵智慧轉換",
|
"客戶端模式 IP 地址支援剪下板匯入智慧識別且支援空格按鍵智慧轉換",
|
||||||
"檔案管理器新建檔案時支援預設字首選擇並自動生成合適的數字字尾",
|
"檔案管理器新建檔案時支援預設字首選擇並自動生成合適的數字字尾",
|
||||||
"檔案管理器執行專案時具體化異常訊息提示 _[`issue #268`](http://issues.autojs6.com/268)_",
|
"檔案管理器執行專案時具體化異常訊息提示 _[`issue #268`](http://issues.autojs6.com/268)_",
|
||||||
"檔案管理器支援更多型別並支援顯示對應的圖示符號 (支援 800 多種檔案型別)",
|
"檔案管理器支援更多型別並支援顯示對應的圖示符號 (支援 800 多種檔案型別)",
|
||||||
|
|||||||
@@ -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.LayoutBoundsFloatyWindow
|
||||||
import org.autojs.autojs.ui.floating.layoutinspector.LayoutHierarchyFloatyWindow
|
import org.autojs.autojs.ui.floating.layoutinspector.LayoutHierarchyFloatyWindow
|
||||||
import org.autojs.autojs.util.RhinoUtils.isBackgroundThread
|
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
|
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) {
|
open class AutoJs(appContext: Application) : AbstractAutoJs(appContext) {
|
||||||
|
|
||||||
// @Thank to Zen2H
|
// @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)
|
private val mA11yTool = AccessibilityTool(appContext)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.autojs.autojs.ui.main.drawer.SocketItemHelper
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by SuperMonster003 on Jun 24, 2022.
|
* Created by SuperMonster003 on Jun 24, 2022.
|
||||||
|
* Modified by SuperMonster003 as of Jan 9, 2026.
|
||||||
*/
|
*/
|
||||||
abstract class AbstractJsonSocketTool(final override val context: Context) : SocketItemHelper {
|
abstract class AbstractJsonSocketTool(final override val context: Context) : SocketItemHelper {
|
||||||
|
|
||||||
@@ -23,6 +24,8 @@ abstract class AbstractJsonSocketTool(final override val context: Context) : Soc
|
|||||||
@JvmField
|
@JvmField
|
||||||
protected var onConnectionDialogDismissed = DialogInterface.OnDismissListener { }
|
protected var onConnectionDialogDismissed = DialogInterface.OnDismissListener { }
|
||||||
|
|
||||||
|
internal abstract fun connectIfNotNormallyClosed()
|
||||||
|
|
||||||
fun setOnConnectionException(consumer: Consumer<in Throwable>) = also { onConnectionException = consumer }
|
fun setOnConnectionException(consumer: Consumer<in Throwable>) = also { onConnectionException = consumer }
|
||||||
|
|
||||||
fun setStateDisposable(disposable: Disposable?) = also { stateDisposable = disposable }
|
fun setStateDisposable(disposable: Disposable?) = also { stateDisposable = disposable }
|
||||||
|
|||||||
@@ -10,19 +10,16 @@ import org.autojs.autojs.app.DialogUtils
|
|||||||
import org.autojs.autojs.core.pref.Pref
|
import org.autojs.autojs.core.pref.Pref
|
||||||
import org.autojs.autojs.extension.MaterialDialogExtensions.widgetThemeColor
|
import org.autojs.autojs.extension.MaterialDialogExtensions.widgetThemeColor
|
||||||
import org.autojs.autojs.pluginclient.JsonSocketClient
|
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.Observers
|
||||||
import org.autojs.autojs.util.ViewUtils
|
import org.autojs.autojs.util.ViewUtils
|
||||||
import org.autojs.autojs6.R
|
import org.autojs.autojs6.R
|
||||||
|
|
||||||
class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
|
class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
|
||||||
|
|
||||||
private var mClientModeItem: DrawerMenuDisposableItem? = null
|
|
||||||
|
|
||||||
override val isConnected
|
override val isConnected
|
||||||
get() = devPlugin.isJsonSocketClientConnected
|
get() = devPlugin.isJsonSocketClientConnected
|
||||||
|
|
||||||
private var isNormallyClosed
|
override var isNormallyClosed
|
||||||
get() = devPlugin.isClientSocketNormallyClosed
|
get() = devPlugin.isClientSocketNormallyClosed
|
||||||
set(state) {
|
set(state) {
|
||||||
devPlugin.isClientSocketNormallyClosed = state
|
devPlugin.isClientSocketNormallyClosed = state
|
||||||
@@ -30,29 +27,17 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
|
|||||||
|
|
||||||
override val isInMainThread = true
|
override val isInMainThread = true
|
||||||
|
|
||||||
override fun connect(): Boolean {
|
override fun connect() {
|
||||||
inputRemoteHost(isAutoConnect = !isNormallyClosed)
|
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()
|
if (!isNormallyClosed) connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun setClientModeItem(clientModeItem: DrawerMenuDisposableItem) {
|
override fun disconnect() {
|
||||||
mClientModeItem = clientModeItem
|
devPlugin.jsonSocketClient?.switchOff()
|
||||||
}
|
|
||||||
|
|
||||||
override fun disconnect(): Boolean {
|
|
||||||
mClientModeItem?.subtitle = null
|
|
||||||
val result = runCatching {
|
|
||||||
devPlugin.jsonSocketClient?.switchOff()
|
|
||||||
}.isSuccess
|
|
||||||
isNormallyClosed = true
|
isNormallyClosed = true
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
@@ -64,7 +49,7 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
|
|||||||
val host = Pref.getServerAddress()
|
val host = Pref.getServerAddress()
|
||||||
if (isAutoConnect) {
|
if (isAutoConnect) {
|
||||||
devPlugin
|
devPlugin
|
||||||
.connectToRemoteServer(context, host, mClientModeItem, true)
|
.connectToRemoteServer(context, host, true)
|
||||||
.subscribe(Observers.emptyConsumer(), Observers.emptyConsumer())
|
.subscribe(Observers.emptyConsumer(), Observers.emptyConsumer())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -87,26 +72,25 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
|
|||||||
connectToRemoteServer(dialog)
|
connectToRemoteServer(dialog)
|
||||||
}
|
}
|
||||||
.itemsLongCallback { dHistories, _, _, text ->
|
.itemsLongCallback { dHistories, _, _, text ->
|
||||||
false.also {
|
MaterialDialog.Builder(context)
|
||||||
MaterialDialog.Builder(context)
|
.title(R.string.text_prompt)
|
||||||
.title(R.string.text_prompt)
|
.content(R.string.text_confirm_to_delete)
|
||||||
.content(R.string.text_confirm_to_delete)
|
.negativeText(R.string.dialog_button_cancel)
|
||||||
.negativeText(R.string.dialog_button_cancel)
|
.neutralColorRes(R.color.dialog_button_default)
|
||||||
.neutralColorRes(R.color.dialog_button_default)
|
.positiveText(R.string.dialog_button_confirm)
|
||||||
.positiveText(R.string.dialog_button_confirm)
|
.positiveColorRes(R.color.dialog_button_caution)
|
||||||
.positiveColorRes(R.color.dialog_button_caution)
|
.onPositive { ds, _ ->
|
||||||
.onPositive { ds, _ ->
|
ds.dismiss()
|
||||||
ds.dismiss()
|
JsonSocketClient.removeFromHistories(text.toString())
|
||||||
JsonSocketClient.removeFromHistories(text.toString())
|
dHistories.items?.let {
|
||||||
dHistories.items?.let {
|
it.remove(text)
|
||||||
it.remove(text)
|
dHistories.notifyItemsChanged()
|
||||||
dHistories.notifyItemsChanged()
|
DialogUtils.toggleContentViewByItems(dHistories)
|
||||||
DialogUtils.toggleContentViewByItems(dHistories)
|
DialogUtils.toggleActionButtonAbilityByItems(dHistories, DialogAction.NEUTRAL)
|
||||||
DialogUtils.toggleActionButtonAbilityByItems(dHistories, DialogAction.NEUTRAL)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.show()
|
}
|
||||||
}
|
.show()
|
||||||
|
return@itemsLongCallback false
|
||||||
}
|
}
|
||||||
.neutralText(R.string.dialog_button_clear_items)
|
.neutralText(R.string.dialog_button_clear_items)
|
||||||
.neutralColorRes(R.color.dialog_button_warn)
|
.neutralColorRes(R.color.dialog_button_warn)
|
||||||
@@ -247,7 +231,7 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
|
|||||||
}
|
}
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
devPlugin
|
devPlugin
|
||||||
.connectToRemoteServer(context, input, mClientModeItem)
|
.connectToRemoteServer(context, input)
|
||||||
.subscribe({ Pref.setServerAddress(input) }, onConnectionException)
|
.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)
|
return Regex(regex).matches(nearest.toString()) && Regex(regex).matches(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun triggerRepeatedCharacter(dialog: MaterialDialog, source: CharSequence, nearest: Char): Boolean {
|
private fun triggerRepeatedCharacter(dialog: MaterialDialog, source: CharSequence, nearest: Char) = when {
|
||||||
if (isRepeatedCharacter(source, nearest, REGEX_DOT)) {
|
isRepeatedCharacter(source, nearest, REGEX_DOT) -> {
|
||||||
showSnack(dialog, R.string.error_repeated_dot_symbol)
|
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)
|
showSnack(dialog, R.string.error_repeated_colon_symbol)
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
return false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -4,21 +4,20 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.autojs.autojs.util.Observers
|
import org.autojs.autojs.util.Observers
|
||||||
import org.autojs.autojs.util.ViewUtils
|
import org.autojs.autojs.util.ViewUtils
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class JsonSocketServerTool(context: Context) : AbstractJsonSocketTool(context) {
|
class JsonSocketServerTool(context: Context) : AbstractJsonSocketTool(context) {
|
||||||
|
|
||||||
override val isConnected
|
override val isConnected
|
||||||
get() = devPlugin.isServerSocketConnected
|
get() = devPlugin.isServerSocketConnected
|
||||||
|
|
||||||
private var isNormallyClosed
|
override var isNormallyClosed
|
||||||
get() = devPlugin.isServerSocketNormallyClosed
|
get() = devPlugin.isServerSocketNormallyClosed
|
||||||
set(state) {
|
set(state) {
|
||||||
devPlugin.isServerSocketNormallyClosed = state
|
devPlugin.isServerSocketNormallyClosed = state
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
override fun connect(): Boolean {
|
override fun connect() {
|
||||||
devPlugin
|
devPlugin
|
||||||
.enableLocalServer()
|
.enableLocalServer()
|
||||||
.subscribe(Observers.emptyConsumer()) {
|
.subscribe(Observers.emptyConsumer()) {
|
||||||
@@ -27,23 +26,15 @@ class JsonSocketServerTool(context: Context) : AbstractJsonSocketTool(context) {
|
|||||||
onConnectionException.accept(it)
|
onConnectionException.accept(it)
|
||||||
}
|
}
|
||||||
isNormallyClosed = false
|
isNormallyClosed = false
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun connectIfNotNormallyClosed() {
|
override fun connectIfNotNormallyClosed() {
|
||||||
if (!isNormallyClosed) connect()
|
if (!isNormallyClosed) connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun disconnect(): Boolean {
|
override fun disconnect() {
|
||||||
val result = try {
|
devPlugin.disconnectJsonSocketServer()
|
||||||
devPlugin.jsonSocketServer?.switchOff()
|
|
||||||
true
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
isNormallyClosed = true
|
isNormallyClosed = true
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
|
|||||||
@@ -4,24 +4,34 @@ import android.content.Context
|
|||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.schedulers.Schedulers
|
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.annotation.ScriptInterface
|
||||||
import org.autojs.autojs.pluginclient.JsonSocket.HANDSHAKE_TIMEOUT
|
import org.autojs.autojs.pluginclient.JsonSocket.HANDSHAKE_TIMEOUT
|
||||||
import org.autojs.autojs.runtime.ScriptRuntime
|
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 org.autojs.autojs.util.ThreadUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.ServerSocket
|
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.
|
* 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.
|
* Transformed by SuperMonster003 on Jul 1, 2023.
|
||||||
*/
|
*/
|
||||||
class DevPluginService(val context: Context) {
|
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 {
|
companion object {
|
||||||
const val DISCONNECTED = 0
|
const val DISCONNECTED = 0
|
||||||
@@ -32,14 +42,18 @@ class DevPluginService(val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Port {
|
object Port {
|
||||||
|
|
||||||
const val PC_SERVER = 6347
|
const val PC_SERVER = 6347
|
||||||
const val AJ_SERVER = 7347
|
const val AJ_SERVER = 7347
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val responseHandler = DevPluginResponseHandler(context, File(context.cacheDir, "remote_project"))
|
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
|
@get:ScriptInterface
|
||||||
@Volatile
|
@Volatile
|
||||||
var jsonSocketClient: JsonSocketClient? = null
|
var jsonSocketClient: JsonSocketClient? = null
|
||||||
@@ -50,14 +64,11 @@ class DevPluginService(val context: Context) {
|
|||||||
var jsonSocketServer: JsonSocketServer? = null
|
var jsonSocketServer: JsonSocketServer? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var mServerSocket: ServerSocket? = null
|
|
||||||
|
|
||||||
val isJsonSocketClientConnected
|
val isJsonSocketClientConnected
|
||||||
get() = jsonSocketClient != null && jsonSocketClient!!.isSocketReady
|
get() = jsonSocketClient?.isSocketReady == true
|
||||||
|
|
||||||
val isServerSocketConnected
|
val isServerSocketConnected
|
||||||
get() = mServerSocket != null && !mServerSocket!!.isClosed
|
get() = mServerSocket?.isClosed == false
|
||||||
|
|
||||||
var isClientSocketNormallyClosed
|
var isClientSocketNormallyClosed
|
||||||
get() = JsonSocketClient.isClientSocketNormallyClosed
|
get() = JsonSocketClient.isClientSocketNormallyClosed
|
||||||
@@ -71,6 +82,17 @@ class DevPluginService(val context: Context) {
|
|||||||
JsonSocketServer.isServerSocketNormallyClosed = state
|
JsonSocketServer.isServerSocketNormallyClosed = state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var mServerSocket: ServerSocket? = null
|
||||||
|
|
||||||
|
// Track all active server-side client connections.
|
||||||
|
// zh-CN: 跟踪所有服务端已建立的客户端连接.
|
||||||
|
private val mServerConnections = ConcurrentHashMap.newKeySet<JsonSocketServer>()
|
||||||
|
|
||||||
|
// Prevent duplicate server startup.
|
||||||
|
// zh-CN: 防止服务端重复启动.
|
||||||
|
private val mEnableLocalServerStarted = AtomicBoolean(false)
|
||||||
|
|
||||||
fun disconnectJsonSocketClient() {
|
fun disconnectJsonSocketClient() {
|
||||||
try {
|
try {
|
||||||
jsonSocketClient?.switchOff()
|
jsonSocketClient?.switchOff()
|
||||||
@@ -81,17 +103,36 @@ class DevPluginService(val context: Context) {
|
|||||||
|
|
||||||
fun disconnectJsonSocketServer() {
|
fun disconnectJsonSocketServer() {
|
||||||
try {
|
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()
|
jsonSocketServer?.switchOff()
|
||||||
|
|
||||||
|
// Publish disconnected state for UI.
|
||||||
|
// zh-CN: 发布断开状态用于 UI 刷新.
|
||||||
|
JsonSocketServer.cxnState.onNext(State(State.DISCONNECTED))
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
fun connectToRemoteServer(context: Context, host: String, clientModeItem: DrawerMenuDisposableItem?) = connectToRemoteServer(context, host, clientModeItem, false)
|
fun connectToRemoteServer(
|
||||||
|
context: Context,
|
||||||
|
host: String,
|
||||||
|
ignoreExceptions: Boolean = false,
|
||||||
|
): Observable<JsonSocketClient> {
|
||||||
|
// 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<JsonSocketClient> {
|
|
||||||
try {
|
try {
|
||||||
var port = Port.PC_SERVER
|
var port = Port.PC_SERVER
|
||||||
var ip = host
|
var ip = host
|
||||||
@@ -100,38 +141,46 @@ class DevPluginService(val context: Context) {
|
|||||||
port = host.substring(i + 1).toInt()
|
port = host.substring(i + 1).toInt()
|
||||||
ip = host.substring(0, i)
|
ip = host.substring(0, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show connecting subtitle immediately.
|
||||||
|
// zh-CN: 立即显示正在连接的 subtitle.
|
||||||
|
clientConnectionIpAddress.onNext("$ip ...");
|
||||||
|
|
||||||
return Observable
|
return Observable
|
||||||
.just(JsonSocketClient(this, context, ip, port))
|
.just(JsonSocketClient(this, context, ip, port))
|
||||||
.observeOn(Schedulers.newThread())
|
.observeOn(Schedulers.newThread())
|
||||||
.doOnNext { jsonSocketClient ->
|
.doOnNext { client ->
|
||||||
try {
|
try {
|
||||||
clientModeItem?.subtitle = "$ip ..."
|
this.jsonSocketClient = client
|
||||||
this.jsonSocketClient = jsonSocketClient
|
|
||||||
if (ThreadUtils.wait({ jsonSocketClient.isSocketReady }, HANDSHAKE_TIMEOUT)) {
|
// Mark this instance as active before it can emit state.
|
||||||
clientModeItem?.subtitle = ip
|
// zh-CN: 在该实例可能发出状态前, 将其标记为活跃实例.
|
||||||
this.isClientSocketNormallyClosed = false
|
JsonSocketClient.activeInstanceId = client.instanceId
|
||||||
JsonSocketClient.addIntoHistories(ip)
|
|
||||||
jsonSocketClient
|
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()
|
.subscribeMessage()
|
||||||
.monitorMessage()
|
.monitorMessage()
|
||||||
.sayHello()
|
.sayHello()
|
||||||
} else {
|
} else {
|
||||||
if (jsonSocketClient.isExtensionVersionCheckFailed || jsonSocketClient.hasErrorMessageOnHello) {
|
if (client.isExtensionVersionCheckFailed || client.hasErrorMessageOnHello) {
|
||||||
JsonSocketClient.addIntoHistories(ip)
|
JsonSocketClient.addIntoHistories(ip)
|
||||||
jsonSocketClient.switchOff()
|
client.switchOff()
|
||||||
return@doOnNext
|
return@doOnNext
|
||||||
}
|
}
|
||||||
if (!ignoreExceptions) {
|
if (!ignoreExceptions) {
|
||||||
jsonSocketClient.onHandshakeTimeout()
|
client.onHandshakeTimeout()
|
||||||
} else {
|
} else {
|
||||||
jsonSocketClient.switchOff()
|
client.switchOff()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
if (!ignoreExceptions) {
|
if (!ignoreExceptions) {
|
||||||
jsonSocketClient.onSocketError(e)
|
client.onSocketError(e)
|
||||||
} else {
|
} else {
|
||||||
jsonSocketClient.switchOff()
|
client.switchOff()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,58 +196,98 @@ class DevPluginService(val context: Context) {
|
|||||||
@AnyThread
|
@AnyThread
|
||||||
fun enableLocalServer(): Observable<JsonSocketServer> {
|
fun enableLocalServer(): Observable<JsonSocketServer> {
|
||||||
return Observable
|
return Observable
|
||||||
.just(JsonSocketServer(this, Port.AJ_SERVER))
|
.create<JsonSocketServer> { emitter ->
|
||||||
.observeOn(Schedulers.newThread())
|
|
||||||
.doOnNext { jsonSocketServer: JsonSocketServer ->
|
// Ensure only one accept loop runs at the same time.
|
||||||
do {
|
// zh-CN: 确保同一时间只有一个 accept 循环在运行.
|
||||||
try {
|
if (!mEnableLocalServerStarted.compareAndSet(false, true)) {
|
||||||
this.jsonSocketServer = jsonSocketServer.apply {
|
jsonSocketServer?.let { emitter.onNext(it) }
|
||||||
mServerSocket = JsonSocketServer.serverSocket
|
emitter.onComplete()
|
||||||
}
|
return@create
|
||||||
if (mServerSocket != null) {
|
}
|
||||||
jsonSocketServer
|
|
||||||
.setStateConnected()
|
JsonSocketServer(this, Port.AJ_SERVER).also {
|
||||||
.setSocket(mServerSocket!!.accept())
|
// Listening started.
|
||||||
.subscribeMessage()
|
// zh-CN: 监听已启动.
|
||||||
.monitorMessage()
|
this.jsonSocketServer = it
|
||||||
.sayHello()
|
it.setStateConnected()
|
||||||
}
|
emitter.onNext(it)
|
||||||
} catch (e: Exception) {
|
}
|
||||||
e.printStackTrace()
|
|
||||||
val msg = e.message ?: ""
|
try {
|
||||||
if (!msg.contains(Regex("Socket closed"))) {
|
while (true) {
|
||||||
jsonSocketServer.onSocketError(e)
|
val ss = JsonSocketServer.serverSocket
|
||||||
}
|
mServerSocket = ss
|
||||||
|
if (ss == null || ss.isClosed) break
|
||||||
|
|
||||||
try {
|
try {
|
||||||
jsonSocketServer.socket?.close()
|
val acceptedSocket = ss.accept()
|
||||||
} catch (e: Exception) {
|
// Create a dedicated connection instance per client.
|
||||||
e.printStackTrace()
|
// 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
|
@AnyThread
|
||||||
fun print(log: String?) {
|
fun print(log: String?) {
|
||||||
jsonSocketClient?.writeLog(log)
|
jsonSocketClient?.writeLog(log)
|
||||||
jsonSocketServer?.writeLog(log)
|
|
||||||
|
// Broadcast logs to all active server-side connections.
|
||||||
|
// zh-CN: 向所有服务端已连接客户端广播日志.
|
||||||
|
mServerConnections.forEach { it.writeLog(log) }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val TYPE_HELLO = "hello"
|
const val TYPE_HELLO = "hello"
|
||||||
const val TYPE_COMMAND = "command"
|
const val TYPE_COMMAND = "command"
|
||||||
const val TYPE_BYTES_COMMAND = "bytes_command"
|
const val TYPE_BYTES_COMMAND = "bytes_command"
|
||||||
fun setState(cxn: PublishSubject<State?>, state: Int) {
|
|
||||||
|
fun setState(cxn: Subject<State?>, state: Int) {
|
||||||
cxn.onNext(State(state))
|
cxn.onNext(State(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setState(cxn: PublishSubject<State?>, state: Int, e: Throwable?) {
|
fun setState(cxn: Subject<State?>, state: Int, e: Throwable?) {
|
||||||
cxn.onNext(State(state, e))
|
cxn.onNext(State(state, e))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
package org.autojs.autojs.pluginclient;
|
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.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@@ -19,22 +16,25 @@ import com.google.gson.JsonParseException;
|
|||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
import com.google.gson.JsonPrimitive;
|
import com.google.gson.JsonPrimitive;
|
||||||
import com.google.gson.stream.JsonReader;
|
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.runtime.api.Device;
|
||||||
import org.autojs.autojs.tool.MapBuilder;
|
import org.autojs.autojs.tool.MapBuilder;
|
||||||
import org.autojs.autojs6.BuildConfig;
|
import org.autojs.autojs6.BuildConfig;
|
||||||
import org.autojs.autojs6.R;
|
import org.autojs.autojs6.R;
|
||||||
import org.mozilla.javascript.NativeObject;
|
import org.mozilla.javascript.NativeObject;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
import java.nio.CharBuffer;
|
import java.nio.CharBuffer;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -44,64 +44,59 @@ import java.util.Map;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import io.reactivex.subjects.PublishSubject;
|
|
||||||
import okio.ByteString;
|
|
||||||
|
|
||||||
abstract public class JsonSocket extends Socket {
|
abstract public class JsonSocket extends Socket {
|
||||||
|
|
||||||
private static final String TAG = "JsonSocket";
|
public static final int HEADER_SIZE = 8;
|
||||||
|
|
||||||
protected static final HashMap<String, Bytes> sBytes = new HashMap<>();
|
|
||||||
|
|
||||||
protected static final HashMap<String, JsonObject> sRequiredBytesCommands = new HashMap<>();
|
|
||||||
private PublishSubject<JsonElement> mJsonElementPublishSubject;
|
|
||||||
private PublishSubject<Bytes> mBytesPublishSubject;
|
|
||||||
|
|
||||||
public <E> E getLast(Collection<E> 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 HANDSHAKE_TIMEOUT = 6400;
|
public static final int HANDSHAKE_TIMEOUT = 6400;
|
||||||
|
|
||||||
public static final String TYPE_HELLO = DevPluginService.TYPE_HELLO;
|
public static final String TYPE_HELLO = DevPluginService.TYPE_HELLO;
|
||||||
public static final String TYPE_COMMAND = DevPluginService.TYPE_COMMAND;
|
public static final String TYPE_COMMAND = DevPluginService.TYPE_COMMAND;
|
||||||
public static final String TYPE_BYTES_COMMAND = DevPluginService.TYPE_BYTES_COMMAND;
|
public static final String TYPE_BYTES_COMMAND = DevPluginService.TYPE_BYTES_COMMAND;
|
||||||
|
|
||||||
public static class Bytes {
|
protected static final HashMap<String, Bytes> sBytes = new HashMap<>();
|
||||||
public final String md5;
|
protected static final HashMap<String, JsonObject> sRequiredBytesCommands = new HashMap<>();
|
||||||
public final ByteString byteString;
|
|
||||||
public final long timestamp;
|
|
||||||
|
|
||||||
public Bytes(String md5, ByteString byteString) {
|
private static final String TAG = "JsonSocket";
|
||||||
this.md5 = md5;
|
|
||||||
this.byteString = byteString;
|
|
||||||
this.timestamp = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Type {
|
|
||||||
public static int JSON = 1;
|
|
||||||
public static int BYTES = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final android.os.Handler mHandler = new Handler(Looper.getMainLooper());
|
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<JsonElement> mJsonElementPublishSubject;
|
||||||
|
private PublishSubject<Bytes> mBytesPublishSubject;
|
||||||
|
|
||||||
public JsonSocket(DevPluginService service) {
|
public JsonSocket(DevPluginService service) {
|
||||||
mService = service;
|
mService = service;
|
||||||
mContext = service.getContext();
|
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> E getLast(Collection<E> c) {
|
||||||
|
E last = null;
|
||||||
|
for (E e : c) last = e;
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
protected Context getContext() {
|
protected Context getContext() {
|
||||||
return mContext;
|
return mContext;
|
||||||
}
|
}
|
||||||
@@ -124,12 +119,14 @@ abstract public class JsonSocket extends Socket {
|
|||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
public JsonSocket subscribeMessage() {
|
public JsonSocket subscribeMessage() {
|
||||||
mJsonElementPublishSubject = PublishSubject.create();
|
mJsonElementPublishSubject = PublishSubject.create();
|
||||||
mJsonElementPublishSubject.observeOn(AndroidSchedulers.mainThread());
|
mJsonElementPublishSubject
|
||||||
mJsonElementPublishSubject.subscribe(this::onSocketData, this::onSocketError);
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(this::onSocketData, this::onSocketError);
|
||||||
|
|
||||||
mBytesPublishSubject = PublishSubject.create();
|
mBytesPublishSubject = PublishSubject.create();
|
||||||
mBytesPublishSubject.observeOn(AndroidSchedulers.mainThread());
|
mBytesPublishSubject
|
||||||
mBytesPublishSubject.subscribe(this::onSocketData, this::onSocketError);
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(this::onSocketData, this::onSocketError);
|
||||||
|
|
||||||
return this;
|
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")
|
@SuppressWarnings("SameParameterValue")
|
||||||
private void writeMap(String type, Map<String, ?> map) {
|
private void writeMap(String type, Map<String, ?> map) {
|
||||||
JsonObject data = new JsonObject();
|
JsonObject data = new JsonObject();
|
||||||
@@ -305,6 +236,9 @@ abstract public class JsonSocket extends Socket {
|
|||||||
writeMessageWithType(getSocket(), json, Type.JSON);
|
writeMessageWithType(getSocket(), json, Type.JSON);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
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[] jsonBytes = getJsonBytes(message);
|
||||||
byte[] headerBytes = getHeaderBytes(new int[]{jsonBytes.length, messageType});
|
byte[] headerBytes = getHeaderBytes(new int[]{jsonBytes.length, messageType});
|
||||||
|
|
||||||
OutputStream os = socket.getOutputStream();
|
synchronized (mWriteLock) {
|
||||||
BufferedOutputStream writer = new BufferedOutputStream(os);
|
OutputStream os = socket.getOutputStream();
|
||||||
|
BufferedOutputStream writer = new BufferedOutputStream(os);
|
||||||
|
|
||||||
writer.write(headerBytes);
|
writer.write(headerBytes);
|
||||||
writer.write(jsonBytes);
|
writer.write(jsonBytes);
|
||||||
writer.flush();
|
writer.flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,22 +269,52 @@ abstract public class JsonSocket extends Socket {
|
|||||||
// ! 这将导致产生的 MD5 散列值与在 Node.js 环境中得到的 MD5 散列值不匹配.
|
// ! 这将导致产生的 MD5 散列值与在 Node.js 环境中得到的 MD5 散列值不匹配.
|
||||||
|
|
||||||
executorService.execute(() -> {
|
executorService.execute(() -> {
|
||||||
// try (AutoCloseable) { ... }
|
// Use byte stream to read framed binary protocol reliably.
|
||||||
// @Thank to Zen2H
|
// zh-CN: 使用字节流读取分帧二进制协议, 提升可靠性.
|
||||||
try (InputStream inputStream = socket.getInputStream();
|
try (InputStream inputStream = socket.getInputStream();
|
||||||
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, UTF_8);
|
BufferedInputStream bis = new BufferedInputStream(inputStream)
|
||||||
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)
|
|
||||||
) {
|
) {
|
||||||
char[] buff = new char[2048];
|
|
||||||
int k;
|
|
||||||
|
|
||||||
Log.d(TAG, "bufferedReader is reading...");
|
Log.d(TAG, "bufferedReader is reading...");
|
||||||
while ((k = bufferedReader.read(buff, 0, buff.length)) > -1 && !socket.isClosed()) {
|
|
||||||
Log.d(TAG, "read length: " + k);
|
byte[] header = new byte[HEADER_SIZE];
|
||||||
onMessageDispatch(Arrays.copyOfRange(buff, 0, k));
|
|
||||||
|
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) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
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 {
|
} finally {
|
||||||
if (jsonSocket instanceof JsonSocketClient) {
|
if (jsonSocket instanceof JsonSocketClient) {
|
||||||
try {
|
try {
|
||||||
@@ -370,11 +336,11 @@ abstract public class JsonSocket extends Socket {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setState(PublishSubject<DevPluginService.State> cxn, int state) {
|
public void setState(Subject<DevPluginService.State> cxn, int state) {
|
||||||
cxn.onNext(new DevPluginService.State(state));
|
cxn.onNext(new DevPluginService.State(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setState(PublishSubject<DevPluginService.State> cxn, int state, Throwable e) {
|
public void setState(Subject<DevPluginService.State> cxn, int state, Throwable e) {
|
||||||
cxn.onNext(new DevPluginService.State(state, e));
|
cxn.onNext(new DevPluginService.State(state, e));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,54 +390,21 @@ abstract public class JsonSocket extends Socket {
|
|||||||
return buffer.array();
|
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;
|
public Bytes(String md5, ByteString byteString) {
|
||||||
private final int mAimDataType;
|
this.md5 = md5;
|
||||||
|
this.byteString = byteString;
|
||||||
private char[] bytes = new char[0];
|
this.timestamp = System.currentTimeMillis();
|
||||||
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 static class Type {
|
||||||
|
public static int JSON = 1;
|
||||||
|
public static int BYTES = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,11 +10,11 @@ import com.afollestad.materialdialogs.MaterialDialog
|
|||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import io.github.g00fy2.versioncompare.Version
|
import io.github.g00fy2.versioncompare.Version
|
||||||
import io.reactivex.subjects.PublishSubject
|
import io.reactivex.subjects.BehaviorSubject
|
||||||
import org.autojs.autojs.AutoJs
|
|
||||||
import org.autojs.autojs.core.pref.Pref
|
import org.autojs.autojs.core.pref.Pref
|
||||||
import org.autojs.autojs.core.pref.Pref.getBoolean
|
import org.autojs.autojs.core.pref.Pref.getBoolean
|
||||||
import org.autojs.autojs.core.pref.Pref.putBoolean
|
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.StringUtils.key
|
||||||
import org.autojs.autojs.util.ViewUtils
|
import org.autojs.autojs.util.ViewUtils
|
||||||
import org.autojs.autojs6.BuildConfig
|
import org.autojs.autojs6.BuildConfig
|
||||||
@@ -24,7 +24,7 @@ import java.net.Socket
|
|||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
import java.util.concurrent.Executors
|
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()
|
private val jsonSocketExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
@@ -33,7 +33,28 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
|
|||||||
var isExtensionVersionCheckFailed = false
|
var isExtensionVersionCheckFailed = false
|
||||||
var hasErrorMessageOnHello = 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 {
|
init {
|
||||||
|
// Mark this instance as active as early as possible so CONNECTING state is visible.
|
||||||
|
// zh-CN: 尽可能早地将此实例标记为 active, 以便 CONNECTING 状态可见.
|
||||||
|
activeInstanceId = instanceId
|
||||||
|
|
||||||
jsonSocketExecutor.submit {
|
jsonSocketExecutor.submit {
|
||||||
try {
|
try {
|
||||||
setStateConnecting()
|
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
|
override fun getSocket() = mSocket
|
||||||
|
|
||||||
@@ -61,26 +88,27 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
|
|||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun close() {
|
override fun close() {
|
||||||
Log.w(TAG, "closing socket...")
|
Log.w(TAG, "closing socket...")
|
||||||
|
cancelHandshakeTimeout()
|
||||||
setStateDisconnected()
|
setStateDisconnected()
|
||||||
mSocket?.apply { close() }
|
mSocket?.apply { close() }
|
||||||
mSocket = null
|
mSocket = null
|
||||||
jsonSocketExecutor.shutdown()
|
jsonSocketExecutor.shutdown()
|
||||||
|
|
||||||
|
// Clear subtitle on close.
|
||||||
|
// zh-CN: 关闭连接时清空 subtitle.
|
||||||
|
service.clientConnectionIpAddress.onNext(NetworkUtils.DEFAULT_IP_ADDRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sayHello() {
|
override fun sayHello() {
|
||||||
super.sayHello()
|
super.sayHello()
|
||||||
mHandler.postDelayed({
|
cancelHandshakeTimeout()
|
||||||
if (isExtensionVersionCheckFailed || hasErrorMessageOnHello) {
|
mHandler.postDelayed(handshakeTimeoutRunnable, HANDSHAKE_TIMEOUT.toLong())
|
||||||
return@postDelayed
|
}
|
||||||
}
|
|
||||||
if (!isSocketReady && !isClientSocketNormallyClosed) {
|
private fun cancelHandshakeTimeout() {
|
||||||
try {
|
// Cancel pending handshake timeout to avoid stale callbacks.
|
||||||
onHandshakeTimeout()
|
// zh-CN: 取消挂起的握手超时回调, 避免旧回调误触发.
|
||||||
} catch (e: IOException) {
|
mHandler.removeCallbacks(handshakeTimeoutRunnable)
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, HANDSHAKE_TIMEOUT.toLong())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getContext() = ctx
|
override fun getContext() = ctx
|
||||||
@@ -93,41 +121,79 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
|
|||||||
if (data != null && data.isJsonObject) {
|
if (data != null && data.isJsonObject) {
|
||||||
val errorMessage = data.asJsonObject["errorMessage"]
|
val errorMessage = data.asJsonObject["errorMessage"]
|
||||||
if (errorMessage != null && errorMessage.isJsonPrimitive) {
|
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
|
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"]
|
val extensionVersion = data.asJsonObject["extensionVersion"]
|
||||||
if (extensionVersion != null && extensionVersion.isJsonPrimitive) {
|
if (extensionVersion != null && extensionVersion.isJsonPrimitive) {
|
||||||
currentVersion = extensionVersion.asString
|
currentVersion = extensionVersion.asString
|
||||||
if (Version(currentVersion).isAtLeast(requiredVersion)) {
|
if (Version(currentVersion).isAtLeast(requiredVersion)) {
|
||||||
|
// Handshake accepted.
|
||||||
|
// zh-CN: 握手通过.
|
||||||
|
host?.let { service.clientConnectionIpAddress.onNext(it) }
|
||||||
|
service.isClientSocketNormallyClosed = false
|
||||||
setStateConnected()
|
setStateConnected()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isExtensionVersionCheckFailed = true
|
isExtensionVersionCheckFailed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: version check failed or invalid hello.
|
||||||
|
// zh-CN: 兜底: 版本校验失败或 hello 异常.
|
||||||
|
service.clientConnectionIpAddress.onNext(NetworkUtils.DEFAULT_IP_ADDRESS)
|
||||||
|
setStateDisconnected(IllegalStateException("Handshake rejected"))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switchOff()
|
switchOff()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
val activity = AutoJs.instance.appUtils.currentActivity ?: context
|
|
||||||
|
|
||||||
val msg = """
|
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
|
${ctx.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_current_version)}: ${currentVersion ?: "${ctx.getString(R.string.text_lower_than)} $requiredVersion"}
|
||||||
|
|
||||||
${activity.getString(R.string.text_repo_url_of_vscode_vsc_ext)}:
|
${ctx.getString(R.string.text_repo_url_of_vscode_vsc_ext)}:
|
||||||
${activity.getString(R.string.url_github_autojs6_vscode_extension_repo)}
|
${ctx.getString(R.string.url_github_autojs6_vscode_extension_repo)}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
if (activity is Activity) {
|
if (ctx is Activity) {
|
||||||
activity.runOnUiThread {
|
ctx.runOnUiThread {
|
||||||
MaterialDialog.Builder(activity)
|
MaterialDialog.Builder(ctx)
|
||||||
.title(activity.getString(R.string.text_connection_cannot_be_established))
|
.title(ctx.getString(R.string.text_connection_cannot_be_established))
|
||||||
.content(msg)
|
.content(msg)
|
||||||
.positiveText(R.string.dialog_button_dismiss)
|
.positiveText(R.string.dialog_button_dismiss)
|
||||||
.positiveColorRes(R.color.dialog_button_default)
|
.positiveColorRes(R.color.dialog_button_default)
|
||||||
@@ -142,10 +208,10 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val toastMsg = """
|
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
|
$requiredVersion
|
||||||
""".trimIndent()
|
""".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)
|
@Throws(IOException::class)
|
||||||
fun onHandshakeTimeout() {
|
fun onHandshakeTimeout() {
|
||||||
Log.i(TAG, "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()
|
switchOff()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun monitorMessage() = also { super.monitorMessage(mSocket, this) }
|
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 {
|
companion object {
|
||||||
|
|
||||||
@@ -241,7 +328,13 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
|
|||||||
serverAddressHistories = linkedSetOf()
|
serverAddressHistories = linkedSetOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
val cxnState = PublishSubject.create<DevPluginService.State>()
|
// Track current active instance for guarding state emission.
|
||||||
|
// zh-CN: 记录当前活跃实例, 用于防止旧实例状态污染.
|
||||||
|
@Volatile
|
||||||
|
internal var activeInstanceId: Long = 0L
|
||||||
|
|
||||||
|
val cxnState: BehaviorSubject<DevPluginService.State> =
|
||||||
|
BehaviorSubject.createDefault(DevPluginService.State(DevPluginService.State.DISCONNECTED))
|
||||||
|
|
||||||
var isClientSocketNormallyClosed
|
var isClientSocketNormallyClosed
|
||||||
get() = getBoolean(key(R.string.key_client_socket_normally_closed), true)
|
get() = getBoolean(key(R.string.key_client_socket_normally_closed), true)
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import android.util.Log
|
|||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.google.gson.JsonElement
|
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.getBoolean
|
||||||
import org.autojs.autojs.core.pref.Pref.putBoolean
|
import org.autojs.autojs.core.pref.Pref.putBoolean
|
||||||
import org.autojs.autojs.util.StringUtils.key
|
import org.autojs.autojs.util.StringUtils.key
|
||||||
import org.autojs.autojs.util.ViewUtils
|
|
||||||
import org.autojs.autojs6.R
|
import org.autojs.autojs6.R
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.ServerSocket
|
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
|
private val isServerSocketSetUp
|
||||||
get() = serverSocket != null && !serverSocket!!.isClosed
|
get() = serverSocket != null && !serverSocket!!.isClosed
|
||||||
@@ -105,8 +110,20 @@ class JsonSocketServer(service: DevPluginService?, port: Int) : JsonSocket(servi
|
|||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
public override fun onSocketError(e: Throwable) {
|
public override fun onSocketError(e: Throwable) {
|
||||||
|
Log.w(TAG, "onSocketError")
|
||||||
e.printStackTrace()
|
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) }
|
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
|
private val TAG = JsonSocketServer::class.java.simpleName
|
||||||
|
|
||||||
val cxnState = PublishSubject.create<DevPluginService.State>()
|
// Replay latest state to new subscribers (e.g. after language change / recreation).
|
||||||
|
// zh-CN: 向新订阅者回放最新状态 (例如切换语言/重建后).
|
||||||
|
val cxnState: BehaviorSubject<DevPluginService.State> =
|
||||||
|
BehaviorSubject.createDefault(DevPluginService.State(DevPluginService.State.DISCONNECTED))
|
||||||
|
|
||||||
var isServerSocketNormallyClosed
|
var isServerSocketNormallyClosed
|
||||||
get() = getBoolean(key(R.string.key_server_socket_normally_closed), true)
|
get() = getBoolean(key(R.string.key_server_socket_normally_closed), true)
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ open class AccessibilityService(final override val context: Context) : ServiceIt
|
|||||||
override val isRunning
|
override val isRunning
|
||||||
get() = mA11yTool.hasService() || mA11yTool.isRunning()
|
get() = mA11yTool.hasService() || mA11yTool.isRunning()
|
||||||
|
|
||||||
override fun active(): Boolean {
|
override fun active() {
|
||||||
return mA11yTool.restartService(true)
|
mA11yTool.restartService(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start(): Boolean {
|
override fun start(): Boolean {
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import android.view.ViewGroup
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import io.reactivex.Observable
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import org.autojs.autojs.App
|
||||||
import org.autojs.autojs.app.tool.FloatingButtonTool
|
import org.autojs.autojs.app.tool.FloatingButtonTool
|
||||||
import org.autojs.autojs.app.tool.JsonSocketClientTool
|
import org.autojs.autojs.app.tool.JsonSocketClientTool
|
||||||
import org.autojs.autojs.app.tool.JsonSocketServerTool
|
import org.autojs.autojs.app.tool.JsonSocketServerTool
|
||||||
@@ -54,7 +56,7 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Stardust on Jan 30, 2017.
|
* 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.
|
* Transformed by SuperMonster003 on Sep 19, 2022.
|
||||||
*/
|
*/
|
||||||
open class DrawerFragment : Fragment() {
|
open class DrawerFragment : Fragment() {
|
||||||
@@ -185,42 +187,78 @@ open class DrawerFragment : Fragment() {
|
|||||||
R.string.key_floating_menu_shown,
|
R.string.key_floating_menu_shown,
|
||||||
)
|
)
|
||||||
|
|
||||||
JsonSocketClientTool(mContext).apply {
|
JsonSocketClientTool(mActivity).apply {
|
||||||
mClientModeItem = DrawerMenuDisposableItem(this, R.drawable.ic_computer_black_48dp, R.string.text_client_mode).also {
|
val devPluginService = App.app.devPluginService
|
||||||
setClientModeItem(it)
|
|
||||||
|
val drawerItem = DrawerMenuDisposableItem(this, R.drawable.ic_computer_black_48dp, R.string.text_client_mode).also {
|
||||||
|
mClientModeItem = it
|
||||||
}
|
}
|
||||||
setStateDisposable(
|
|
||||||
JsonSocketClient.cxnState
|
val disposable = Observable
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.combineLatest(
|
||||||
.subscribe {
|
JsonSocketClient.cxnState,
|
||||||
if (it.state == DevPluginService.State.DISCONNECTED) {
|
devPluginService.clientConnectionIpAddress,
|
||||||
mClientModeItem.subtitle = null
|
) { state: DevPluginService.State, ip: String ->
|
||||||
}
|
Pair(state, ip)
|
||||||
consumeJsonSocketItemState(mClientModeItem, it)
|
}
|
||||||
})
|
.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 ->
|
setOnConnectionException { e: Throwable ->
|
||||||
mClientModeItem.setCheckedIfNeeded(false)
|
drawerItem.setCheckedIfNeeded(false)
|
||||||
ViewUtils.showToast(context, getString(R.string.error_connect_to_remote, e.message), true)
|
ViewUtils.showToast(context, getString(R.string.error_connect_to_remote, e.message), true)
|
||||||
}
|
}
|
||||||
setOnConnectionDialogDismissed { mClientModeItem.setCheckedIfNeeded(false) }
|
setOnConnectionDialogDismissed { drawerItem.setCheckedIfNeeded(false) }
|
||||||
connectIfNotNormallyClosed()
|
connectIfNotNormallyClosed()
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonSocketServerTool(mContext).apply {
|
JsonSocketServerTool(mActivity).apply {
|
||||||
setStateDisposable(
|
val devPluginService = App.app.devPluginService
|
||||||
JsonSocketServer.cxnState
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
val drawerItem = DrawerMenuDisposableItem(this, R.drawable.ic_smartphone_black_48dp, R.string.text_server_mode).also {
|
||||||
.subscribe { state: DevPluginService.State ->
|
mServerModeItem = it
|
||||||
mServerModeItem.subtitle = takeIf { state.state == DevPluginService.State.CONNECTED }?.let {
|
}
|
||||||
NetworkUtils.getIpAddress()
|
|
||||||
|
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 ->
|
setOnConnectionException { e: Throwable ->
|
||||||
mServerModeItem.setCheckedIfNeeded(false)
|
drawerItem.setCheckedIfNeeded(false)
|
||||||
ViewUtils.showToast(context, getString(R.string.error_enable_server, e.message), true)
|
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()
|
connectIfNotNormallyClosed()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,15 +568,6 @@ open class DrawerFragment : Fragment() {
|
|||||||
mKeepScreenOnWhenInForegroundItem,
|
mKeepScreenOnWhenInForegroundItem,
|
||||||
).forEach { it.sync() }
|
).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 {
|
companion object {
|
||||||
|
|
||||||
lateinit var drawerMenuAdapter: DrawerMenuAdapter
|
lateinit var drawerMenuAdapter: DrawerMenuAdapter
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ abstract class DrawerMenuItemCustomHelper internal constructor(private val mCont
|
|||||||
override val context: Context
|
override val context: Context
|
||||||
get() = mContext
|
get() = mContext
|
||||||
|
|
||||||
override fun active(): Boolean {
|
override fun active() {
|
||||||
if (isActive) return true
|
if (!isActive) toggle()
|
||||||
return toggle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ interface DrawerMenuItemHelper {
|
|||||||
val isInMainThread: Boolean
|
val isInMainThread: Boolean
|
||||||
get() = false
|
get() = false
|
||||||
|
|
||||||
fun active(): Boolean
|
fun active()
|
||||||
|
|
||||||
fun toggle(): Boolean
|
fun toggle(): Boolean
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ interface PermissionItemHelper : DrawerMenuItemHelper {
|
|||||||
override val isActive
|
override val isActive
|
||||||
get() = has()
|
get() = has()
|
||||||
|
|
||||||
override fun active(): Boolean {
|
override fun active() {
|
||||||
if (has()) return true
|
requestIfNeeded()
|
||||||
return request()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun has(): Boolean
|
fun has(): Boolean
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ interface RunnableItemHelper : DrawerMenuItemHelper {
|
|||||||
override val isActive: Boolean
|
override val isActive: Boolean
|
||||||
get() = isRunning
|
get() = isRunning
|
||||||
|
|
||||||
override fun active(): Boolean {
|
override fun active() {
|
||||||
if (isRunning) return true
|
launchIfNeeded()
|
||||||
return launch()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRunning: Boolean
|
val isRunning: Boolean
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ interface ServiceItemHelper : DrawerMenuItemHelper {
|
|||||||
override val isActive: Boolean
|
override val isActive: Boolean
|
||||||
get() = isRunning
|
get() = isRunning
|
||||||
|
|
||||||
override fun active(): Boolean {
|
override fun active() {
|
||||||
if (isRunning) return true
|
startIfNeeded()
|
||||||
return start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRunning: Boolean
|
val isRunning: Boolean
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ interface ShowableItemHelper : DrawerMenuItemHelper {
|
|||||||
override val isActive: Boolean
|
override val isActive: Boolean
|
||||||
get() = isShowing
|
get() = isShowing
|
||||||
|
|
||||||
override fun active(): Boolean {
|
override fun active() {
|
||||||
if (isShowing) return true
|
showIfNeeded()
|
||||||
return show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isShowing: Boolean
|
val isShowing: Boolean
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ interface SocketItemHelper : DrawerMenuItemHelper {
|
|||||||
override val isActive
|
override val isActive
|
||||||
get() = isConnected
|
get() = isConnected
|
||||||
|
|
||||||
override fun active(): Boolean {
|
override fun active() {
|
||||||
if (isConnected) return true
|
connectIfNeeded()
|
||||||
return connect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isConnected: Boolean
|
val isConnected: Boolean
|
||||||
|
|
||||||
fun connect(): Boolean
|
var isNormallyClosed: Boolean
|
||||||
|
|
||||||
|
fun connect()
|
||||||
|
|
||||||
fun dispose()
|
fun dispose()
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ interface SocketItemHelper : DrawerMenuItemHelper {
|
|||||||
if (!isConnected) connect()
|
if (!isConnected) connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disconnect(): Boolean
|
fun disconnect()
|
||||||
|
|
||||||
fun disconnectIfNeeded() {
|
fun disconnectIfNeeded() {
|
||||||
if (isConnected) disconnect()
|
if (isConnected) disconnect()
|
||||||
@@ -28,6 +29,8 @@ interface SocketItemHelper : DrawerMenuItemHelper {
|
|||||||
|
|
||||||
override fun toggle() = toggle(!isConnected)
|
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
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -20,11 +20,13 @@ import java.util.Locale
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Stardust on Apr 9, 2017.
|
* 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")
|
@Suppress("DEPRECATION", "unused")
|
||||||
object NetworkUtils {
|
object NetworkUtils {
|
||||||
|
|
||||||
|
const val DEFAULT_IP_ADDRESS = "0.0.0.0"
|
||||||
|
|
||||||
private val globalAppContext = GlobalAppContext.get()
|
private val globalAppContext = GlobalAppContext.get()
|
||||||
|
|
||||||
private val connectivityManager
|
private val connectivityManager
|
||||||
@@ -47,7 +49,7 @@ object NetworkUtils {
|
|||||||
} catch (e: UnknownHostException) {
|
} catch (e: UnknownHostException) {
|
||||||
Log.e(NetworkUtils::class.java.simpleName, "Error getting Hotspot IP address ", e)
|
Log.e(NetworkUtils::class.java.simpleName, "Error getting Hotspot IP address ", e)
|
||||||
null
|
null
|
||||||
} ?: "0.0.0.0"
|
} ?: DEFAULT_IP_ADDRESS
|
||||||
|
|
||||||
private fun getIpAddressByte(): ByteArray {
|
private fun getIpAddressByte(): ByteArray {
|
||||||
val manager = globalAppContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
val manager = globalAppContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||||
@@ -185,7 +187,7 @@ object NetworkUtils {
|
|||||||
} catch (ignored: Exception) {
|
} catch (ignored: Exception) {
|
||||||
// for now eat exceptions
|
// for now eat exceptions
|
||||||
}
|
}
|
||||||
return "0.0.0.0"
|
return DEFAULT_IP_ADDRESS
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|||||||
@@ -1140,5 +1140,7 @@
|
|||||||
<string name="text_scheduled_restart_start_delay">تأخير البدء</string>
|
<string name="text_scheduled_restart_start_delay">تأخير البدء</string>
|
||||||
<string name="dialog_button_advanced_settings">متقدم</string>
|
<string name="dialog_button_advanced_settings">متقدم</string>
|
||||||
<string name="error_failed_to_save_project_config">تعذّر حفظ إعدادات المشروع</string>
|
<string name="error_failed_to_save_project_config">تعذّر حفظ إعدادات المشروع</string>
|
||||||
|
<string name="text_connected">متصل</string>
|
||||||
|
<string name="text_connected_count_with_colon">متصل: %1$d</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1135,5 +1135,7 @@
|
|||||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||||
<string name="error_failed_to_save_project_config">Failed to save project config</string>
|
<string name="error_failed_to_save_project_config">Failed to save project config</string>
|
||||||
|
<string name="text_connected">Connected</string>
|
||||||
|
<string name="text_connected_count_with_colon">Connected: %1$d</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1138,5 +1138,7 @@
|
|||||||
<string name="text_scheduled_restart_start_delay">Retraso de inicio</string>
|
<string name="text_scheduled_restart_start_delay">Retraso de inicio</string>
|
||||||
<string name="dialog_button_advanced_settings">Avanzado</string>
|
<string name="dialog_button_advanced_settings">Avanzado</string>
|
||||||
<string name="error_failed_to_save_project_config">No se pudo guardar la configuración del proyecto</string>
|
<string name="error_failed_to_save_project_config">No se pudo guardar la configuración del proyecto</string>
|
||||||
|
<string name="text_connected">Conectado</string>
|
||||||
|
<string name="text_connected_count_with_colon">Conectados: %1$d</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1138,5 +1138,7 @@
|
|||||||
<string name="text_scheduled_restart_start_delay">Délai de démarrage</string>
|
<string name="text_scheduled_restart_start_delay">Délai de démarrage</string>
|
||||||
<string name="dialog_button_advanced_settings">Avancé</string>
|
<string name="dialog_button_advanced_settings">Avancé</string>
|
||||||
<string name="error_failed_to_save_project_config">Échec de l\'enregistrement de la configuration du projet</string>
|
<string name="error_failed_to_save_project_config">Échec de l\'enregistrement de la configuration du projet</string>
|
||||||
|
<string name="text_connected">Connecté</string>
|
||||||
|
<string name="text_connected_count_with_colon">Connectés : %1$d</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1139,5 +1139,7 @@
|
|||||||
<string name="text_scheduled_restart_start_delay">開始遅延</string>
|
<string name="text_scheduled_restart_start_delay">開始遅延</string>
|
||||||
<string name="dialog_button_advanced_settings">詳細</string>
|
<string name="dialog_button_advanced_settings">詳細</string>
|
||||||
<string name="error_failed_to_save_project_config">プロジェクト設定の保存に失敗しました</string>
|
<string name="error_failed_to_save_project_config">プロジェクト設定の保存に失敗しました</string>
|
||||||
|
<string name="text_connected">接続済み</string>
|
||||||
|
<string name="text_connected_count_with_colon">接続済み: %1$d</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1140,5 +1140,7 @@
|
|||||||
<string name="text_scheduled_restart_start_delay">시작 지연</string>
|
<string name="text_scheduled_restart_start_delay">시작 지연</string>
|
||||||
<string name="dialog_button_advanced_settings">고급</string>
|
<string name="dialog_button_advanced_settings">고급</string>
|
||||||
<string name="error_failed_to_save_project_config">프로젝트 설정을 저장하지 못했습니다</string>
|
<string name="error_failed_to_save_project_config">프로젝트 설정을 저장하지 못했습니다</string>
|
||||||
|
<string name="text_connected">연결됨</string>
|
||||||
|
<string name="text_connected_count_with_colon">연결됨: %1$d</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1138,5 +1138,7 @@
|
|||||||
<string name="text_scheduled_restart_start_delay">Задержка старта</string>
|
<string name="text_scheduled_restart_start_delay">Задержка старта</string>
|
||||||
<string name="dialog_button_advanced_settings">Доп.</string>
|
<string name="dialog_button_advanced_settings">Доп.</string>
|
||||||
<string name="error_failed_to_save_project_config">Не удалось сохранить конфигурацию проекта</string>
|
<string name="error_failed_to_save_project_config">Не удалось сохранить конфигурацию проекта</string>
|
||||||
|
<string name="text_connected">Подключено</string>
|
||||||
|
<string name="text_connected_count_with_colon">Подключено: %1$d</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1136,5 +1136,7 @@
|
|||||||
<string name="text_scheduled_restart_start_delay">啓動延遲</string>
|
<string name="text_scheduled_restart_start_delay">啓動延遲</string>
|
||||||
<string name="dialog_button_advanced_settings">高級設置</string>
|
<string name="dialog_button_advanced_settings">高級設置</string>
|
||||||
<string name="error_failed_to_save_project_config">保存項目配置失敗</string>
|
<string name="error_failed_to_save_project_config">保存項目配置失敗</string>
|
||||||
|
<string name="text_connected">已連接</string>
|
||||||
|
<string name="text_connected_count_with_colon">已連接: %1$d</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1136,5 +1136,7 @@
|
|||||||
<string name="text_scheduled_restart_start_delay">啟動延遲</string>
|
<string name="text_scheduled_restart_start_delay">啟動延遲</string>
|
||||||
<string name="dialog_button_advanced_settings">高階設定</string>
|
<string name="dialog_button_advanced_settings">高階設定</string>
|
||||||
<string name="error_failed_to_save_project_config">儲存專案配置失敗</string>
|
<string name="error_failed_to_save_project_config">儲存專案配置失敗</string>
|
||||||
|
<string name="text_connected">已連線</string>
|
||||||
|
<string name="text_connected_count_with_colon">已連線: %1$d</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1136,5 +1136,7 @@
|
|||||||
<string name="text_scheduled_restart_start_delay">启动延迟</string>
|
<string name="text_scheduled_restart_start_delay">启动延迟</string>
|
||||||
<string name="dialog_button_advanced_settings">高级设置</string>
|
<string name="dialog_button_advanced_settings">高级设置</string>
|
||||||
<string name="error_failed_to_save_project_config">保存项目配置失败</string>
|
<string name="error_failed_to_save_project_config">保存项目配置失败</string>
|
||||||
|
<string name="text_connected">已连接</string>
|
||||||
|
<string name="text_connected_count_with_colon">已连接: %1$d</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1393,5 +1393,7 @@
|
|||||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||||
<string name="error_failed_to_save_project_config">Failed to save project config</string>
|
<string name="error_failed_to_save_project_config">Failed to save project config</string>
|
||||||
|
<string name="text_connected">Connected</string>
|
||||||
|
<string name="text_connected_count_with_colon">Connected: %1$d</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#Thu Jan 08 01:30:58 CST 2026
|
#Fri Jan 09 23:39:38 CST 2026
|
||||||
BUILD_TIME=1767807058123
|
BUILD_TIME=1767973178586
|
||||||
COMPILE_SDK_VERSION=36
|
COMPILE_SDK_VERSION=36
|
||||||
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
||||||
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
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
|
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
||||||
TARGET_SDK_VERSION=36
|
TARGET_SDK_VERSION=36
|
||||||
TARGET_SDK_VERSION_INRT=29
|
TARGET_SDK_VERSION_INRT=29
|
||||||
VERSION_BUILD=3583
|
VERSION_BUILD=3591
|
||||||
VERSION_NAME=6.7.0 Alpha13
|
VERSION_NAME=6.7.0 Alpha14
|
||||||
VSCODE_EXT_REQUIRED_VERSION=1.0.8
|
VSCODE_EXT_REQUIRED_VERSION=1.0.13
|
||||||
|
|||||||
Reference in New Issue
Block a user