6.7.0 - Alpha14 - 修复服务端模式连接时, Activity 重建操作导致 VSCode 控制台无法输出日志的问题 (issue #385); 深度优化 VSCode 插件连接相关代码逻辑

This commit is contained in:
SuperMonster003
2026-01-09 23:51:51 +08:00
parent 71d70b6055
commit e01f010e46
40 changed files with 599 additions and 420 deletions

View File

@@ -277,7 +277,7 @@
"دعم التعرف الذكي على موقع الإدراج عند استيراد فئات وحزم Java في محرر الأكواد",
"دعم استخدام المسارات كمعلمات الصور مباشرة في وحدة images",
"دعم المعلمات في استيراد الحزم باستخدام importPackage",
"دعم التعرف الذكي على عناوين IP في وضع الخادم من الحافظة ودعم التحويل الذكي باستخدام مفتاح المسافة",
"في وضع العميل، يدعم عنوان IP الاستيراد من الحافظة مع تعرّف ذكي، ويدعم التحويل الذكي لإدخال مفتاح المسافة",
"دعم إنشاء الملفات برقم تسلسلي افتراضي ومناسب مع اختيار تلقائي للبادئة في مدير الملفات",
"توفر رسائل خطأ مفصلة عند تشغيل مشروع معين في مدير الملفات _[`issue #268`](http://issues.autojs6.com/268)_",
"دعم المزيد من أنواع الملفات ودعم عرض الرموز المقابلة (يدعم أكثر من 800 نوع من الملفات) في مدير الملفات",

View File

@@ -277,7 +277,7 @@
"Code editor supports intelligently determining insertion location when importing Java classes and package names",
"images module supports using paths directly as image parameters",
"importPackage supports string parameters",
"Server mode IP address supports clipboard import with intelligent recognition and smart conversion with space key",
"In client mode, the IP address supports smart recognition when importing from the clipboard and also supports intelligent conversion of space-key input",
"File manager supports default prefix selection when creating new files and automatically generates appropriate numeric suffix",
"File manager specifically informs on exception message when running project _[`issue #268`](http://issues.autojs6.com/268)_",
"File manager supports more types and displays corresponding icon symbols (supports over 800 file types)",

View File

@@ -277,7 +277,7 @@
"El editor de código admite la determinación inteligente de la ubicación de inserción al importar clases Java y nombres de paquetes",
"El módulo images admite el uso de rutas directamente como parámetros de imagen",
"importPackage admite parámetros de cadena",
"La dirección IP del modo servidor admite la importación del portapapeles con reconocimiento inteligente y conversión inteligente con la tecla de espacio",
"En el modo cliente, la dirección IP admite la importación desde el portapapeles con reconocimiento inteligente y permite la conversión inteligente de la tecla de espacio",
"El administrador de archivos admite la selección de prefijos predeterminados al crear nuevos archivos y genera automáticamente el sufijo numérico apropiado",
"El administrador de archivos informa específicamente sobre el mensaje de excepción al ejecutar el proyecto _[`issue #268`](http://issues.autojs6.com/268)_",
"El administrador de archivos admite más tipos y muestra símbolos de icono correspondientes (admite más de 800 tipos de archivos)",

View File

@@ -277,7 +277,7 @@
"L'éditeur de code prend en charge la détermination intelligente de l'emplacement d'insertion lors de l'importation de classes Java et de noms de paquets",
"Le module images prend en charge l'utilisation des chemins directement comme paramètres d'image",
"importPackage prend en charge les paramètres de chaîne",
"L'adresse IP du mode serveur prend en charge l'importation du presse-papiers avec reconnaissance intelligente et conversion intelligente avec la touche espace",
"En mode client, l'adresse IP prend en charge l'importation depuis le presse-papiers avec reconnaissance intelligente et permet une conversion intelligente de la touche espace",
"Le gestionnaire de fichiers prend en charge la sélection de préfixes par défaut lors de la création de nouveaux fichiers et génère automatiquement un suffixe numérique approprié",
"Le gestionnaire de fichiers informe spécifiquement sur le message d'exception lors de l'exécution du projet _[`issue #268`](http://issues.autojs6.com/268)_",
"Le gestionnaire de fichiers prend en charge plus de types et affiche les symboles d'icône correspondants (prend en charge plus de 800 types de fichiers)",

View File

@@ -277,7 +277,7 @@
"コードエディタで Java クラスやパッケージ名をインポートする位置のインテリジェント判断をサポート",
"images モジュールが画像パラメータとしてパスの直接使用をサポート",
"importPackage が文字列パラメータをサポート",
"サーバーモードの IP アドレスがクリップボードインポートをスマートに認識し, スペースキーのスマート変換をサポート",
"クライアントモードでは, IP アドレスがクリップボードからの取り込みに対応し, インテリジェントに認識します. さらに, スペースキー入力の自動変換にも対応します",
"ファイルマネージャが新しいファイルを作成する際にデフォルトのプレフィックス選択と適切な数字サフィックスの自動生成をサポート",
"ファイル管理プロジェクトの実行時の例外メッセージを具体化 _[`issue #268`](http://issues.autojs6.com/268)_",
"ファイルマネージャがより多くの種類をサポートし, 対応するアイコンシンボルを表示 (800 種類以上のファイルタイプをサポート)",

View File

@@ -277,7 +277,7 @@
"코드 편집기에서 Java 클래스나 패키지명을 가져오는 위치의 지능형 판단을 지원",
"images 모듈이 이미지 매개변수로 경로의 직접 사용을 지원",
"importPackage 가 문자열 매개변수를 지원",
"서버 모드 IP 주소 클립보드 가져오기를 스마트하게 인식하, 스페이스 키 스마트 변환을 지원",
"클라이언트 모드에서 IP 주소 클립보드 가져오기를 지능적으로 인식하, 공백 키 입력을 지능적으로 변환하는 기능도 지원합니다",
"파일 관리자가 새 파일을 생성할 때 기본 접두사 선택 및 적절한 숫자 접미사의 자동 생성을 지원",
"파일 관리 프로젝트 실행 시 예외 메시지 구체화 _[`issue #268`](http://issues.autojs6.com/268)_",
"파일 관리자가 더 많은 유형을 지원하고, 해당 아이콘 심볼을 표시 (800 이상의 파일 타입 지원)",

View File

@@ -277,7 +277,7 @@
"Редактор кода поддерживает интеллектуальное определение места вставки при импорте Java классов и пакетов имен",
"Модуль images поддерживает использование путей напрямую в качестве параметров изображений",
"importPackage поддерживает строковые параметры",
"IP-адрес в режиме сервера поддерживает импорт из буфера обмена с интеллектуальным определением и конверсией с пробелом",
"В режиме клиента IP-адрес поддерживает умное распознавание при вставке из буфера обмена и интеллектуальное преобразование ввода через пробел",
"Файловый менеджер поддерживает выбор префиксов по умолчанию при создании новых файлов и автоматически генерирует подходящее числовое расширение",
"Файловый менеджер четко уведомляет об ошибке при выполнении проекта _[`issue #268`](http://issues.autojs6.com/268)_",
"Файловый менеджер поддерживает больше типов и отображает соответствующие значки (поддерживает более 800 типов файлов)",

View File

@@ -1,7 +1,7 @@
{
"$data": {
"v6.7.0": {
"released_date": "2026/01/08",
"released_date": "2026/01/09",
"feature": [
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)",
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
@@ -81,6 +81,9 @@
"文件管理器删除项目文件夹后 UI 未能自动刷新的问题",
"使用 \"三按钮\" 手势导航时导航栏前景色可能与夜间模式关联异常的问题",
"APK 文件类型信息对话框可能无法获取应用名称及 SDK 信息的问题",
"客户端模式连接时, 旋转屏幕及切换语言等触发 Activity 重建的操作导致 AutoJs6 总是重建连接的问题",
"服务端模式连接时, 旋转屏幕及切换语言等触发 Activity 重建的操作导致 VSCode 控制台无法输出日志的问题 _[`issue #385`](http://issues.autojs6.com/385)_",
"连接 VSCode 插件时, 多种方式同时连接可能导致日志打印数量成倍增加的问题",
"构建工具启用 isCleanup[Paddle/Rapid]Ocr 配置选项时无法正常完成 Rebuild Project 任务的问题"
],
"improvement": [
@@ -101,6 +104,7 @@
"崩溃报告页面支持双指缩放调整字体大小并添加常用功能按钮",
"应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_",
"Rhino 引擎在泛型签名解析失败时回退为原始反射类型以增强低版本安卓系统的反射方法可用性",
"服务端模式连接时支持显示已建立连接的客户端数量",
"使用 LiveData 及 SharedFlow 替代已弃用的 LocalBroadcastManager",
"Gradle 构建脚本提升 7z 格式文件的解压效率",
"Gradle 构建脚本支持获取详细的 Android Studio IDE 版本 (如 \"2025.1.4.7\")",
@@ -414,7 +418,7 @@
"代码编辑器导入 Java 类和包名时支持智能判断插入位置",
"images 模块支持直接使用路径作为图像参数",
"importPackage 支持字符串参数",
"服务端模式 IP 地址支持剪切板导入智能识别且支持空格按键智能转换",
"客户端模式 IP 地址支持剪切板导入智能识别且支持空格按键智能转换",
"文件管理器新建文件时支持默认前缀选择并自动生成合适的数字后缀",
"文件管理器运行项目时具体化异常消息提示 _[`issue #268`](http://issues.autojs6.com/268)_",
"文件管理器支持更多类型并支持显示对应的图标符号 (支持 800 多种文件类型)",

View File

@@ -277,7 +277,7 @@
"代碼編輯器導入 Java 類和包名時支持智能判斷插入位置",
"images 模塊支持直接使用路徑作為圖像參數",
"importPackage 支持字符串參數",
"服務端模式 IP 地址支持剪切板導入智能識別且支持空格按鍵智能轉換",
"客户端模式 IP 地址支持剪切板導入智能識別且支持空格按鍵智能轉換",
"文件管理器新建文件時支持默認前綴選擇並自動生成合適的數字後綴",
"文件管理器運行項目時具體化異常消息提示 _[`issue #268`](http://issues.autojs6.com/268)_",
"文件管理器支持更多類型並支持顯示對應的圖標符號 (支持 800 多種文件類型)",

View File

@@ -277,7 +277,7 @@
"程式碼編輯器匯入 Java 類和包名時支援智慧判斷插入位置",
"images 模組支援直接使用路徑作為影象引數",
"importPackage 支援字串引數",
"服務端模式 IP 地址支援剪下板匯入智慧識別且支援空格按鍵智慧轉換",
"客戶端模式 IP 地址支援剪下板匯入智慧識別且支援空格按鍵智慧轉換",
"檔案管理器新建檔案時支援預設字首選擇並自動生成合適的數字字尾",
"檔案管理器執行專案時具體化異常訊息提示 _[`issue #268`](http://issues.autojs6.com/268)_",
"檔案管理器支援更多型別並支援顯示對應的圖示符號 (支援 800 多種檔案型別)",

View File

@@ -26,7 +26,9 @@ import org.autojs.autojs.ui.floating.FullScreenFloatyWindow
import org.autojs.autojs.ui.floating.layoutinspector.LayoutBoundsFloatyWindow
import org.autojs.autojs.ui.floating.layoutinspector.LayoutHierarchyFloatyWindow
import org.autojs.autojs.util.RhinoUtils.isBackgroundThread
import java.util.concurrent.Executors
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import org.autojs.autojs.inrt.autojs.AutoJs as AutoJsInrt
/**
@@ -38,7 +40,16 @@ import org.autojs.autojs.inrt.autojs.AutoJs as AutoJsInrt
open class AutoJs(appContext: Application) : AbstractAutoJs(appContext) {
// @Thank to Zen2H
private val mPrintExecutor = Executors.newSingleThreadExecutor()
// Use bounded queue to prevent log flooding from blocking the whole channel.
// zh-CN: 使用有界队列避免日志洪泛导致整个通道被阻塞.
private val mPrintExecutor = ThreadPoolExecutor(
1,
1,
0L,
TimeUnit.MILLISECONDS,
ArrayBlockingQueue(2048),
ThreadPoolExecutor.DiscardOldestPolicy(),
)
private val mA11yTool = AccessibilityTool(appContext)

View File

@@ -9,6 +9,7 @@ import org.autojs.autojs.ui.main.drawer.SocketItemHelper
/**
* Created by SuperMonster003 on Jun 24, 2022.
* Modified by SuperMonster003 as of Jan 9, 2026.
*/
abstract class AbstractJsonSocketTool(final override val context: Context) : SocketItemHelper {
@@ -23,6 +24,8 @@ abstract class AbstractJsonSocketTool(final override val context: Context) : Soc
@JvmField
protected var onConnectionDialogDismissed = DialogInterface.OnDismissListener { }
internal abstract fun connectIfNotNormallyClosed()
fun setOnConnectionException(consumer: Consumer<in Throwable>) = also { onConnectionException = consumer }
fun setStateDisposable(disposable: Disposable?) = also { stateDisposable = disposable }

View File

@@ -10,19 +10,16 @@ import org.autojs.autojs.app.DialogUtils
import org.autojs.autojs.core.pref.Pref
import org.autojs.autojs.extension.MaterialDialogExtensions.widgetThemeColor
import org.autojs.autojs.pluginclient.JsonSocketClient
import org.autojs.autojs.ui.main.drawer.DrawerMenuDisposableItem
import org.autojs.autojs.util.Observers
import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs6.R
class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
private var mClientModeItem: DrawerMenuDisposableItem? = null
override val isConnected
get() = devPlugin.isJsonSocketClientConnected
private var isNormallyClosed
override var isNormallyClosed
get() = devPlugin.isClientSocketNormallyClosed
set(state) {
devPlugin.isClientSocketNormallyClosed = state
@@ -30,29 +27,17 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
override val isInMainThread = true
override fun connect(): Boolean {
override fun connect() {
inputRemoteHost(isAutoConnect = !isNormallyClosed)
// @Hint by SuperMonster003 on Nov 9, 2023.
// ! Method with showing a dialog always returns false.
// ! zh-CN: 含对话框显示的方法总是返回 false 值.
return false
}
internal fun connectIfNotNormallyClosed() {
override fun connectIfNotNormallyClosed() {
if (!isNormallyClosed) connect()
}
internal fun setClientModeItem(clientModeItem: DrawerMenuDisposableItem) {
mClientModeItem = clientModeItem
}
override fun disconnect(): Boolean {
mClientModeItem?.subtitle = null
val result = runCatching {
devPlugin.jsonSocketClient?.switchOff()
}.isSuccess
override fun disconnect() {
devPlugin.jsonSocketClient?.switchOff()
isNormallyClosed = true
return result
}
override fun dispose() {
@@ -64,7 +49,7 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
val host = Pref.getServerAddress()
if (isAutoConnect) {
devPlugin
.connectToRemoteServer(context, host, mClientModeItem, true)
.connectToRemoteServer(context, host, true)
.subscribe(Observers.emptyConsumer(), Observers.emptyConsumer())
return
}
@@ -87,26 +72,25 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
connectToRemoteServer(dialog)
}
.itemsLongCallback { dHistories, _, _, text ->
false.also {
MaterialDialog.Builder(context)
.title(R.string.text_prompt)
.content(R.string.text_confirm_to_delete)
.negativeText(R.string.dialog_button_cancel)
.neutralColorRes(R.color.dialog_button_default)
.positiveText(R.string.dialog_button_confirm)
.positiveColorRes(R.color.dialog_button_caution)
.onPositive { ds, _ ->
ds.dismiss()
JsonSocketClient.removeFromHistories(text.toString())
dHistories.items?.let {
it.remove(text)
dHistories.notifyItemsChanged()
DialogUtils.toggleContentViewByItems(dHistories)
DialogUtils.toggleActionButtonAbilityByItems(dHistories, DialogAction.NEUTRAL)
}
MaterialDialog.Builder(context)
.title(R.string.text_prompt)
.content(R.string.text_confirm_to_delete)
.negativeText(R.string.dialog_button_cancel)
.neutralColorRes(R.color.dialog_button_default)
.positiveText(R.string.dialog_button_confirm)
.positiveColorRes(R.color.dialog_button_caution)
.onPositive { ds, _ ->
ds.dismiss()
JsonSocketClient.removeFromHistories(text.toString())
dHistories.items?.let {
it.remove(text)
dHistories.notifyItemsChanged()
DialogUtils.toggleContentViewByItems(dHistories)
DialogUtils.toggleActionButtonAbilityByItems(dHistories, DialogAction.NEUTRAL)
}
.show()
}
}
.show()
return@itemsLongCallback false
}
.neutralText(R.string.dialog_button_clear_items)
.neutralColorRes(R.color.dialog_button_warn)
@@ -247,7 +231,7 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
}
dialog.dismiss()
devPlugin
.connectToRemoteServer(context, input, mClientModeItem)
.connectToRemoteServer(context, input)
.subscribe({ Pref.setServerAddress(input) }, onConnectionException)
}
@@ -259,16 +243,16 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) {
return Regex(regex).matches(nearest.toString()) && Regex(regex).matches(source)
}
private fun triggerRepeatedCharacter(dialog: MaterialDialog, source: CharSequence, nearest: Char): Boolean {
if (isRepeatedCharacter(source, nearest, REGEX_DOT)) {
private fun triggerRepeatedCharacter(dialog: MaterialDialog, source: CharSequence, nearest: Char) = when {
isRepeatedCharacter(source, nearest, REGEX_DOT) -> {
showSnack(dialog, R.string.error_repeated_dot_symbol)
return true
true
}
if (isRepeatedCharacter(source, nearest, REGEX_COLON)) {
isRepeatedCharacter(source, nearest, REGEX_COLON) -> {
showSnack(dialog, R.string.error_repeated_colon_symbol)
return true
true
}
return false
else -> false
}
companion object {

View File

@@ -4,21 +4,20 @@ import android.annotation.SuppressLint
import android.content.Context
import org.autojs.autojs.util.Observers
import org.autojs.autojs.util.ViewUtils
import java.io.IOException
class JsonSocketServerTool(context: Context) : AbstractJsonSocketTool(context) {
override val isConnected
get() = devPlugin.isServerSocketConnected
private var isNormallyClosed
override var isNormallyClosed
get() = devPlugin.isServerSocketNormallyClosed
set(state) {
devPlugin.isServerSocketNormallyClosed = state
}
@SuppressLint("CheckResult")
override fun connect(): Boolean {
override fun connect() {
devPlugin
.enableLocalServer()
.subscribe(Observers.emptyConsumer()) {
@@ -27,23 +26,15 @@ class JsonSocketServerTool(context: Context) : AbstractJsonSocketTool(context) {
onConnectionException.accept(it)
}
isNormallyClosed = false
return true
}
internal fun connectIfNotNormallyClosed() {
override fun connectIfNotNormallyClosed() {
if (!isNormallyClosed) connect()
}
override fun disconnect(): Boolean {
val result = try {
devPlugin.jsonSocketServer?.switchOff()
true
} catch (e: IOException) {
e.printStackTrace()
false
}
override fun disconnect() {
devPlugin.disconnectJsonSocketServer()
isNormallyClosed = true
return result
}
override fun dispose() {

View File

@@ -4,24 +4,34 @@ import android.content.Context
import androidx.annotation.AnyThread
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import org.autojs.autojs.annotation.ScriptInterface
import org.autojs.autojs.pluginclient.JsonSocket.HANDSHAKE_TIMEOUT
import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.ui.main.drawer.DrawerMenuDisposableItem
import org.autojs.autojs.util.NetworkUtils.DEFAULT_IP_ADDRESS
import org.autojs.autojs.util.ThreadUtils
import java.io.File
import java.io.IOException
import java.net.ServerSocket
import java.net.SocketException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
/**
* Created by Stardust on May 11, 2017.
* Modified by SuperMonster003 as of Jan 1, 2022.
* Modified by SuperMonster003 as of Jan 9, 2026.
* Transformed by SuperMonster003 on Jul 1, 2023.
*/
class DevPluginService(val context: Context) {
class State @JvmOverloads constructor(val state: Int, val exception: Throwable? = null) {
class State @JvmOverloads constructor(private val state: Int, val exception: Throwable? = null) {
fun isDisconnected() = state == DISCONNECTED
fun isConnecting() = state == CONNECTING
fun isConnected() = state == CONNECTED
companion object {
const val DISCONNECTED = 0
@@ -32,14 +42,18 @@ class DevPluginService(val context: Context) {
}
object Port {
const val PC_SERVER = 6347
const val AJ_SERVER = 7347
}
val responseHandler = DevPluginResponseHandler(context, File(context.cacheDir, "remote_project"))
// Publish current server-side connection count.
// zh-CN: 发布当前服务端连接数量.
val serverConnectionCount = BehaviorSubject.createDefault(0)
val clientConnectionIpAddress = BehaviorSubject.createDefault(DEFAULT_IP_ADDRESS)
@get:ScriptInterface
@Volatile
var jsonSocketClient: JsonSocketClient? = null
@@ -50,14 +64,11 @@ class DevPluginService(val context: Context) {
var jsonSocketServer: JsonSocketServer? = null
private set
@Volatile
private var mServerSocket: ServerSocket? = null
val isJsonSocketClientConnected
get() = jsonSocketClient != null && jsonSocketClient!!.isSocketReady
get() = jsonSocketClient?.isSocketReady == true
val isServerSocketConnected
get() = mServerSocket != null && !mServerSocket!!.isClosed
get() = mServerSocket?.isClosed == false
var isClientSocketNormallyClosed
get() = JsonSocketClient.isClientSocketNormallyClosed
@@ -71,6 +82,17 @@ class DevPluginService(val context: Context) {
JsonSocketServer.isServerSocketNormallyClosed = state
}
@Volatile
private var mServerSocket: ServerSocket? = null
// Track all active server-side client connections.
// zh-CN: 跟踪所有服务端已建立的客户端连接.
private val mServerConnections = ConcurrentHashMap.newKeySet<JsonSocketServer>()
// Prevent duplicate server startup.
// zh-CN: 防止服务端重复启动.
private val mEnableLocalServerStarted = AtomicBoolean(false)
fun disconnectJsonSocketClient() {
try {
jsonSocketClient?.switchOff()
@@ -81,17 +103,36 @@ class DevPluginService(val context: Context) {
fun disconnectJsonSocketServer() {
try {
// Close all active client connections first.
// zh-CN: 先关闭所有已连接客户端.
mServerConnections.toList().forEach { runCatching { it.close() } }
mServerConnections.clear()
serverConnectionCount.onNext(0)
// Then close listening socket.
// zh-CN: 然后关闭监听 socket.
jsonSocketServer?.switchOff()
// Publish disconnected state for UI.
// zh-CN: 发布断开状态用于 UI 刷新.
JsonSocketServer.cxnState.onNext(State(State.DISCONNECTED))
} catch (e: IOException) {
e.printStackTrace()
}
}
@AnyThread
fun connectToRemoteServer(context: Context, host: String, clientModeItem: DrawerMenuDisposableItem?) = connectToRemoteServer(context, host, clientModeItem, false)
fun connectToRemoteServer(
context: Context,
host: String,
ignoreExceptions: Boolean = false,
): Observable<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 {
var port = Port.PC_SERVER
var ip = host
@@ -100,38 +141,46 @@ class DevPluginService(val context: Context) {
port = host.substring(i + 1).toInt()
ip = host.substring(0, i)
}
// Show connecting subtitle immediately.
// zh-CN: 立即显示正在连接的 subtitle.
clientConnectionIpAddress.onNext("$ip ...");
return Observable
.just(JsonSocketClient(this, context, ip, port))
.observeOn(Schedulers.newThread())
.doOnNext { jsonSocketClient ->
.doOnNext { client ->
try {
clientModeItem?.subtitle = "$ip ..."
this.jsonSocketClient = jsonSocketClient
if (ThreadUtils.wait({ jsonSocketClient.isSocketReady }, HANDSHAKE_TIMEOUT)) {
clientModeItem?.subtitle = ip
this.isClientSocketNormallyClosed = false
JsonSocketClient.addIntoHistories(ip)
jsonSocketClient
this.jsonSocketClient = client
// Mark this instance as active before it can emit state.
// zh-CN: 在该实例可能发出状态前, 将其标记为活跃实例.
JsonSocketClient.activeInstanceId = client.instanceId
if (ThreadUtils.wait({ client.isSocketReady }, HANDSHAKE_TIMEOUT)) {
// Do NOT mark as connected here. Wait for hello validation in JsonSocketClient.
// zh-CN: 不要在此处标记为已连接, 等待 JsonSocketClient 的 hello 校验通过.
client
.subscribeMessage()
.monitorMessage()
.sayHello()
} else {
if (jsonSocketClient.isExtensionVersionCheckFailed || jsonSocketClient.hasErrorMessageOnHello) {
if (client.isExtensionVersionCheckFailed || client.hasErrorMessageOnHello) {
JsonSocketClient.addIntoHistories(ip)
jsonSocketClient.switchOff()
client.switchOff()
return@doOnNext
}
if (!ignoreExceptions) {
jsonSocketClient.onHandshakeTimeout()
client.onHandshakeTimeout()
} else {
jsonSocketClient.switchOff()
client.switchOff()
}
}
} catch (e: IOException) {
if (!ignoreExceptions) {
jsonSocketClient.onSocketError(e)
client.onSocketError(e)
} else {
jsonSocketClient.switchOff()
client.switchOff()
}
}
}
@@ -147,58 +196,98 @@ class DevPluginService(val context: Context) {
@AnyThread
fun enableLocalServer(): Observable<JsonSocketServer> {
return Observable
.just(JsonSocketServer(this, Port.AJ_SERVER))
.observeOn(Schedulers.newThread())
.doOnNext { jsonSocketServer: JsonSocketServer ->
do {
try {
this.jsonSocketServer = jsonSocketServer.apply {
mServerSocket = JsonSocketServer.serverSocket
}
if (mServerSocket != null) {
jsonSocketServer
.setStateConnected()
.setSocket(mServerSocket!!.accept())
.subscribeMessage()
.monitorMessage()
.sayHello()
}
} catch (e: Exception) {
e.printStackTrace()
val msg = e.message ?: ""
if (!msg.contains(Regex("Socket closed"))) {
jsonSocketServer.onSocketError(e)
}
.create<JsonSocketServer> { emitter ->
// Ensure only one accept loop runs at the same time.
// zh-CN: 确保同一时间只有一个 accept 循环在运行.
if (!mEnableLocalServerStarted.compareAndSet(false, true)) {
jsonSocketServer?.let { emitter.onNext(it) }
emitter.onComplete()
return@create
}
JsonSocketServer(this, Port.AJ_SERVER).also {
// Listening started.
// zh-CN: 监听已启动.
this.jsonSocketServer = it
it.setStateConnected()
emitter.onNext(it)
}
try {
while (true) {
val ss = JsonSocketServer.serverSocket
mServerSocket = ss
if (ss == null || ss.isClosed) break
try {
jsonSocketServer.socket?.close()
} catch (e: Exception) {
e.printStackTrace()
val acceptedSocket = ss.accept()
// Create a dedicated connection instance per client.
// zh-CN: 为每个客户端创建独立连接实例.
val conn = JsonSocketServer(this, Port.AJ_SERVER).apply {
setSocket(acceptedSocket)
subscribeMessage()
monitorMessage()
sayHello()
}
mServerConnections.add(conn)
serverConnectionCount.onNext(mServerConnections.size)
emitter.onNext(conn)
} catch (e: SocketException) {
// Treat "Socket closed" as a normal shutdown path.
// zh-CN: 将 "Socket closed" 视为正常关闭流程.
if ((e.message ?: "").contains("Socket closed", true)) {
break
}
emitter.onError(e)
return@create
}
}
} while (mServerSocket != null)
emitter.onComplete()
} finally {
// Allow restarting after loop exits.
// zh-CN: 循环退出后允许再次启动.
mEnableLocalServerStarted.set(false)
}
}
// Run blocking accept loop on IO scheduler to avoid ANR.
// zh-CN: 在 IO 调度器运行阻塞 accept 循环, 避免 ANR.
.subscribeOn(Schedulers.io())
}
internal fun onServerClientDisconnected(conn: JsonSocketServer) {
// Remove disconnected connection and update count.
// zh-CN: 移除已断开连接并更新计数.
mServerConnections.remove(conn)
serverConnectionCount.onNext(mServerConnections.size)
}
// FIXME by SuperMonster003 as of Dec 29, 2021.
// ! Would print double (may be even more times) the amount of
// ! messages on VSCode when multi connection were established.
@AnyThread
fun print(log: String?) {
jsonSocketClient?.writeLog(log)
jsonSocketServer?.writeLog(log)
// Broadcast logs to all active server-side connections.
// zh-CN: 向所有服务端已连接客户端广播日志.
mServerConnections.forEach { it.writeLog(log) }
}
companion object {
const val TYPE_HELLO = "hello"
const val TYPE_COMMAND = "command"
const val TYPE_BYTES_COMMAND = "bytes_command"
fun setState(cxn: PublishSubject<State?>, state: Int) {
fun setState(cxn: Subject<State?>, state: Int) {
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))
}
}
}

View File

@@ -1,8 +1,5 @@
package org.autojs.autojs.pluginclient;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
@@ -19,22 +16,25 @@ import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.stream.JsonReader;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.subjects.PublishSubject;
import io.reactivex.subjects.Subject;
import okio.ByteString;
import org.autojs.autojs.runtime.api.Device;
import org.autojs.autojs.tool.MapBuilder;
import org.autojs.autojs6.BuildConfig;
import org.autojs.autojs6.R;
import org.mozilla.javascript.NativeObject;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
@@ -44,64 +44,59 @@ import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.subjects.PublishSubject;
import okio.ByteString;
import static java.nio.charset.StandardCharsets.UTF_8;
abstract public class JsonSocket extends Socket {
private static final String TAG = "JsonSocket";
protected static final HashMap<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 HEADER_SIZE = 8;
public static final int HANDSHAKE_TIMEOUT = 6400;
public static final String TYPE_HELLO = DevPluginService.TYPE_HELLO;
public static final String TYPE_COMMAND = DevPluginService.TYPE_COMMAND;
public static final String TYPE_BYTES_COMMAND = DevPluginService.TYPE_BYTES_COMMAND;
public static class Bytes {
public final String md5;
public final ByteString byteString;
public final long timestamp;
protected static final HashMap<String, Bytes> sBytes = new HashMap<>();
protected static final HashMap<String, JsonObject> sRequiredBytesCommands = new HashMap<>();
public Bytes(String md5, ByteString byteString) {
this.md5 = md5;
this.byteString = byteString;
this.timestamp = System.currentTimeMillis();
}
}
public static class Type {
public static int JSON = 1;
public static int BYTES = 2;
}
private static final String TAG = "JsonSocket";
public final android.os.Handler mHandler = new Handler(Looper.getMainLooper());
// Serialize writes to prevent interleaving between threads.
// zh-CN: 串行化写入, 避免多线程导致帧交错.
private final Object mWriteLock = new Object();
private final Context mContext;
private final DevPluginService mService;
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
private PublishSubject<JsonElement> mJsonElementPublishSubject;
private PublishSubject<Bytes> mBytesPublishSubject;
public JsonSocket(DevPluginService service) {
mService = service;
mContext = service.getContext();
}
private static void readFully(InputStream in, byte[] buffer, int offset, int length) throws IOException {
int read;
int total = 0;
while (total < length) {
read = in.read(buffer, offset + total, length - total);
if (read < 0) {
throw new IOException("Stream ended unexpectedly");
}
total += read;
}
}
public <E> E getLast(Collection<E> c) {
E last = null;
for (E e : c) last = e;
return last;
}
protected Context getContext() {
return mContext;
}
@@ -124,12 +119,14 @@ abstract public class JsonSocket extends Socket {
@SuppressLint("CheckResult")
public JsonSocket subscribeMessage() {
mJsonElementPublishSubject = PublishSubject.create();
mJsonElementPublishSubject.observeOn(AndroidSchedulers.mainThread());
mJsonElementPublishSubject.subscribe(this::onSocketData, this::onSocketError);
mJsonElementPublishSubject
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onSocketData, this::onSocketError);
mBytesPublishSubject = PublishSubject.create();
mBytesPublishSubject.observeOn(AndroidSchedulers.mainThread());
mBytesPublishSubject.subscribe(this::onSocketData, this::onSocketError);
mBytesPublishSubject
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onSocketData, this::onSocketError);
return this;
}
@@ -182,72 +179,6 @@ abstract public class JsonSocket extends Socket {
}
}
private void onMessageDispatch(char[] bytes) {
Log.d(TAG, "Received message total bytes: " + Arrays.toString(bytes));
Log.d(TAG, "Received message total length: " + bytes.length);
char[] overload;
if (mFragment != null) {
overload = mFragment.splice(bytes);
} else {
String header = new String(Arrays.copyOfRange(bytes, 0, HEADER_SIZE));
int dataSize = parseHeaderInt(header, 0, HEADER_SIZE - 2);
Log.d(TAG, "Data length from header: " + dataSize);
if (dataSize < 0) {
Log.e(TAG, "Invalid data length, ignored");
return;
}
int dataType = parseHeaderInt(header, HEADER_SIZE - 2, 2);
Log.d(TAG, "Data type from header: " + dataType);
if (dataType < 0) {
Log.e(TAG, "Unknown data type, ignored");
return;
}
mFragment = new Fragment(dataSize, dataType);
if (HEADER_SIZE > bytes.length) {
Log.e(TAG, "Bytes length (" + bytes.length + ") is less than header size (" + HEADER_SIZE + "), ignored");
return;
}
overload = mFragment.splice(Arrays.copyOfRange(bytes, HEADER_SIZE, bytes.length));
}
if (!mFragment.isRestored()) {
return;
}
char[] restored = mFragment.getRestoredBytes();
int type = mFragment.getAimDataType();
mFragment = null;
if (type == Type.JSON) {
onMessage(new String(charsToBytes(restored, UTF_8), UTF_8));
} else if (type == Type.BYTES) {
onMessage(ByteString.of(charsToBytes(restored, ISO_8859_1)));
} else {
Log.e(TAG, "Unknown data type (" + type + ") for message dispatching");
return;
}
if (overload != null) {
onMessageDispatch(overload);
}
}
private static int parseHeaderInt(String header, int offset, int length) {
try {
String s = new String(header.getBytes(UTF_8), offset, length).replaceAll("\\D", "");
if (!s.isEmpty()) {
return Integer.parseInt(s);
}
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
@SuppressWarnings("SameParameterValue")
private void writeMap(String type, Map<String, ?> map) {
JsonObject data = new JsonObject();
@@ -305,6 +236,9 @@ abstract public class JsonSocket extends Socket {
writeMessageWithType(getSocket(), json, Type.JSON);
} catch (IOException e) {
e.printStackTrace();
// Fail fast on write errors to avoid half-dead connections.
// zh-CN: 写入出错时快速失败, 避免连接处于"半死不活"状态.
onSocketError(e);
}
}
@@ -313,12 +247,14 @@ abstract public class JsonSocket extends Socket {
byte[] jsonBytes = getJsonBytes(message);
byte[] headerBytes = getHeaderBytes(new int[]{jsonBytes.length, messageType});
OutputStream os = socket.getOutputStream();
BufferedOutputStream writer = new BufferedOutputStream(os);
synchronized (mWriteLock) {
OutputStream os = socket.getOutputStream();
BufferedOutputStream writer = new BufferedOutputStream(os);
writer.write(headerBytes);
writer.write(jsonBytes);
writer.flush();
writer.write(headerBytes);
writer.write(jsonBytes);
writer.flush();
}
}
}
@@ -333,22 +269,52 @@ abstract public class JsonSocket extends Socket {
// ! 这将导致产生的 MD5 散列值与在 Node.js 环境中得到的 MD5 散列值不匹配.
executorService.execute(() -> {
// try (AutoCloseable) { ... }
// @Thank to Zen2H
// Use byte stream to read framed binary protocol reliably.
// zh-CN: 使用字节流读取分帧二进制协议, 提升可靠性.
try (InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)
BufferedInputStream bis = new BufferedInputStream(inputStream)
) {
char[] buff = new char[2048];
int k;
Log.d(TAG, "bufferedReader is reading...");
while ((k = bufferedReader.read(buff, 0, buff.length)) > -1 && !socket.isClosed()) {
Log.d(TAG, "read length: " + k);
onMessageDispatch(Arrays.copyOfRange(buff, 0, k));
byte[] header = new byte[HEADER_SIZE];
while (!socket.isClosed()) {
readFully(bis, header, 0, HEADER_SIZE);
ByteBuffer hb = ByteBuffer.wrap(header).order(ByteOrder.BIG_ENDIAN);
int dataSize = hb.getInt();
int dataType = hb.getInt();
Log.d(TAG, "Data length from header: " + dataSize);
Log.d(TAG, "Data type from header: " + dataType);
if (dataSize < 0) {
throw new IOException("Invalid data length: " + dataSize);
}
byte[] payload = new byte[dataSize];
readFully(bis, payload, 0, dataSize);
if (dataType == Type.JSON) {
onMessage(new String(payload, UTF_8));
} else if (dataType == Type.BYTES) {
onMessage(ByteString.of(payload));
} else {
throw new IOException("Unknown data type: " + dataType);
}
}
} catch (IOException e) {
e.printStackTrace();
// Treat "Socket closed" as a normal shutdown path.
// zh-CN: 将 "Socket closed" 视为正常关闭流程.
String message = e.getMessage();
if (message != null && (
message.toLowerCase().contains("socket closed") ||
message.toLowerCase().contains("stream ended unexpectedly")
)) {
return;
}
onSocketError(e);
} finally {
if (jsonSocket instanceof JsonSocketClient) {
try {
@@ -370,11 +336,11 @@ abstract public class JsonSocket extends Socket {
return bytes;
}
public void setState(PublishSubject<DevPluginService.State> cxn, int state) {
public void setState(Subject<DevPluginService.State> cxn, int 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));
}
@@ -424,54 +390,21 @@ abstract public class JsonSocket extends Socket {
return buffer.array();
}
private static class Fragment {
public static class Bytes {
public final String md5;
public final ByteString byteString;
public final long timestamp;
private final int mAimLength;
private final int mAimDataType;
private char[] bytes = new char[0];
private int currentLength = 0;
public Fragment(int aimLength, int aimDataType) {
mAimLength = aimLength;
mAimDataType = aimDataType;
}
public boolean isRestored() {
return currentLength >= mAimLength;
}
public char[] splice(char[] charBytes) {
int tempLength = currentLength + charBytes.length;
if (tempLength <= mAimLength) {
this.bytes = joinBytes(this.bytes, charBytes);
currentLength = currentLength + charBytes.length;
Log.d(TAG, "currentLength: " + currentLength + "/" + mAimLength);
return null;
}
int overloadLength = tempLength - mAimLength;
Log.w(TAG, "charBytes overloaded: " + overloadLength);
char[] leftPart = Arrays.copyOfRange(charBytes, 0, charBytes.length - overloadLength);
char[] rightPart = Arrays.copyOfRange(charBytes, charBytes.length - overloadLength, charBytes.length);
splice(leftPart);
return rightPart;
}
public char[] getRestoredBytes() {
return bytes;
}
public int getAimDataType() {
return mAimDataType;
}
private static char[] joinBytes(final char[] array1, char[] array2) {
char[] joinedArray = Arrays.copyOf(array1, array1.length + array2.length);
System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
return joinedArray;
public Bytes(String md5, ByteString byteString) {
this.md5 = md5;
this.byteString = byteString;
this.timestamp = System.currentTimeMillis();
}
}
public static class Type {
public static int JSON = 1;
public static int BYTES = 2;
}
}

View File

@@ -10,11 +10,11 @@ import com.afollestad.materialdialogs.MaterialDialog
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import io.github.g00fy2.versioncompare.Version
import io.reactivex.subjects.PublishSubject
import org.autojs.autojs.AutoJs
import io.reactivex.subjects.BehaviorSubject
import org.autojs.autojs.core.pref.Pref
import org.autojs.autojs.core.pref.Pref.getBoolean
import org.autojs.autojs.core.pref.Pref.putBoolean
import org.autojs.autojs.util.NetworkUtils
import org.autojs.autojs.util.StringUtils.key
import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs6.BuildConfig
@@ -24,7 +24,7 @@ import java.net.Socket
import java.net.SocketTimeoutException
import java.util.concurrent.Executors
class JsonSocketClient(service: DevPluginService?, private val ctx: Context, host: String?, port: Int) : JsonSocket(service) {
class JsonSocketClient(service: DevPluginService?, private val ctx: Context, private val host: String?, port: Int) : JsonSocket(service) {
private val jsonSocketExecutor = Executors.newSingleThreadExecutor()
@@ -33,7 +33,28 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
var isExtensionVersionCheckFailed = false
var hasErrorMessageOnHello = false
// Unique id for guarding against stale state emission from old instances.
// zh-CN: 用于防止旧实例发出的状态污染 UI 的唯一 id.
internal val instanceId: Long = System.nanoTime()
private val handshakeTimeoutRunnable = Runnable {
if (isExtensionVersionCheckFailed || hasErrorMessageOnHello) {
return@Runnable
}
if (!isSocketReady && !isClientSocketNormallyClosed) {
try {
onHandshakeTimeout()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
init {
// Mark this instance as active as early as possible so CONNECTING state is visible.
// zh-CN: 尽可能早地将此实例标记为 active, 以便 CONNECTING 状态可见.
activeInstanceId = instanceId
jsonSocketExecutor.submit {
try {
setStateConnecting()
@@ -46,7 +67,13 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
}
}
override fun isSocketReady() = mSocket?.isConnected == true
override fun isSocketReady(): Boolean {
val s = mSocket ?: return false
return s.isConnected &&
!s.isClosed &&
!s.isInputShutdown &&
!s.isOutputShutdown
}
override fun getSocket() = mSocket
@@ -61,26 +88,27 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
@Throws(IOException::class)
override fun close() {
Log.w(TAG, "closing socket...")
cancelHandshakeTimeout()
setStateDisconnected()
mSocket?.apply { close() }
mSocket = null
jsonSocketExecutor.shutdown()
// Clear subtitle on close.
// zh-CN: 关闭连接时清空 subtitle.
service.clientConnectionIpAddress.onNext(NetworkUtils.DEFAULT_IP_ADDRESS)
}
override fun sayHello() {
super.sayHello()
mHandler.postDelayed({
if (isExtensionVersionCheckFailed || hasErrorMessageOnHello) {
return@postDelayed
}
if (!isSocketReady && !isClientSocketNormallyClosed) {
try {
onHandshakeTimeout()
} catch (e: IOException) {
e.printStackTrace()
}
}
}, HANDSHAKE_TIMEOUT.toLong())
cancelHandshakeTimeout()
mHandler.postDelayed(handshakeTimeoutRunnable, HANDSHAKE_TIMEOUT.toLong())
}
private fun cancelHandshakeTimeout() {
// Cancel pending handshake timeout to avoid stale callbacks.
// zh-CN: 取消挂起的握手超时回调, 避免旧回调误触发.
mHandler.removeCallbacks(handshakeTimeoutRunnable)
}
override fun getContext() = ctx
@@ -93,41 +121,79 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
if (data != null && data.isJsonObject) {
val errorMessage = data.asJsonObject["errorMessage"]
if (errorMessage != null && errorMessage.isJsonPrimitive) {
ViewUtils.showToast(context, errorMessage.asString, true)
val msg = errorMessage.asString
if (ctx is Activity) {
ctx.runOnUiThread {
MaterialDialog.Builder(ctx)
.title(ctx.getString(R.string.text_connection_cannot_be_established))
.content(msg)
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)
.build()
.also {
mHandler.post { it.show() }
}
}
} else {
ViewUtils.showToast(ctx, msg, true)
}
hasErrorMessageOnHello = true
// Mark as disconnected when server rejects handshake (e.g. version mismatch).
// zh-CN: 当服务端拒绝握手 (例如版本不匹配) 时, 标记为已断开.
service.clientConnectionIpAddress.onNext(NetworkUtils.DEFAULT_IP_ADDRESS)
setStateDisconnected(IllegalStateException(errorMessage.asString))
try {
switchOff()
} catch (e: IOException) {
e.printStackTrace()
}
return
}
val extensionVersion = data.asJsonObject["extensionVersion"]
if (extensionVersion != null && extensionVersion.isJsonPrimitive) {
currentVersion = extensionVersion.asString
if (Version(currentVersion).isAtLeast(requiredVersion)) {
// Handshake accepted.
// zh-CN: 握手通过.
host?.let { service.clientConnectionIpAddress.onNext(it) }
service.isClientSocketNormallyClosed = false
setStateConnected()
return
}
isExtensionVersionCheckFailed = true
}
}
// Fallback: version check failed or invalid hello.
// zh-CN: 兜底: 版本校验失败或 hello 异常.
service.clientConnectionIpAddress.onNext(NetworkUtils.DEFAULT_IP_ADDRESS)
setStateDisconnected(IllegalStateException("Handshake rejected"))
try {
switchOff()
} catch (e: IOException) {
e.printStackTrace()
}
val activity = AutoJs.instance.appUtils.currentActivity ?: context
val msg = """
${activity.getString(R.string.text_vsc_ext_version_not_meet_requirement)}.
${ctx.getString(R.string.text_vsc_ext_version_not_meet_requirement)}.
${activity.getString(R.string.text_min_version)}: $requiredVersion
${activity.getString(R.string.text_current_version)}: ${currentVersion ?: "${activity.getString(R.string.text_lower_than)} $requiredVersion"}
${ctx.getString(R.string.text_min_version)}: $requiredVersion
${ctx.getString(R.string.text_current_version)}: ${currentVersion ?: "${ctx.getString(R.string.text_lower_than)} $requiredVersion"}
${activity.getString(R.string.text_repo_url_of_vscode_vsc_ext)}:
${activity.getString(R.string.url_github_autojs6_vscode_extension_repo)}
${ctx.getString(R.string.text_repo_url_of_vscode_vsc_ext)}:
${ctx.getString(R.string.url_github_autojs6_vscode_extension_repo)}
""".trimIndent()
if (activity is Activity) {
activity.runOnUiThread {
MaterialDialog.Builder(activity)
.title(activity.getString(R.string.text_connection_cannot_be_established))
if (ctx is Activity) {
ctx.runOnUiThread {
MaterialDialog.Builder(ctx)
.title(ctx.getString(R.string.text_connection_cannot_be_established))
.content(msg)
.positiveText(R.string.dialog_button_dismiss)
.positiveColorRes(R.color.dialog_button_default)
@@ -142,10 +208,10 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
}
} else {
val toastMsg = """
${activity.getString(R.string.text_min_version_of_vscode_vsc_ext)}:
${ctx.getString(R.string.text_min_version_of_vscode_vsc_ext)}:
$requiredVersion
""".trimIndent()
ViewUtils.showToast(activity, toastMsg, true)
ViewUtils.showToast(ctx, toastMsg, true)
}
}
@@ -207,19 +273,40 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
@Throws(IOException::class)
fun onHandshakeTimeout() {
Log.i(TAG, "onHandshakeTimeout")
setStateDisconnected(SocketTimeoutException(context.getString(R.string.error_handshake_timed_out, HANDSHAKE_TIMEOUT)))
setStateDisconnected(SocketTimeoutException(ctx.getString(R.string.error_handshake_timed_out, HANDSHAKE_TIMEOUT)))
switchOff()
}
override fun monitorMessage() = also { super.monitorMessage(mSocket, this) }
override fun setStateConnected() = also { setState(cxnState, DevPluginService.State.CONNECTED) }
override fun setStateConnected() = also {
cancelHandshakeTimeout()
emitStateIfActive(DevPluginService.State.CONNECTED)
}
private fun setStateConnecting() = also { setState(cxnState, DevPluginService.State.CONNECTING) }
private fun setStateConnecting() = also {
emitStateIfActive(DevPluginService.State.CONNECTING)
}
private fun setStateDisconnected() = also { setState(cxnState, DevPluginService.State.DISCONNECTED) }
private fun setStateDisconnected() = also {
cancelHandshakeTimeout()
emitStateIfActive(DevPluginService.State.DISCONNECTED)
}
private fun setStateDisconnected(e: Throwable?) = also { setState(cxnState, DevPluginService.State.DISCONNECTED, e) }
private fun setStateDisconnected(e: Throwable?) = also {
cancelHandshakeTimeout()
emitStateIfActive(DevPluginService.State.DISCONNECTED, e)
}
private fun emitStateIfActive(state: Int, e: Throwable? = null) {
// Only the active client instance is allowed to publish state to the global subject.
// zh-CN: 仅允许当前活跃的客户端实例向全局 subject 发布状态.
if (activeInstanceId != instanceId) return
when (e) {
null -> setState(cxnState, state)
else -> setState(cxnState, state, e)
}
}
companion object {
@@ -241,7 +328,13 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, hos
serverAddressHistories = linkedSetOf()
}
val cxnState = PublishSubject.create<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
get() = getBoolean(key(R.string.key_client_socket_normally_closed), true)

View File

@@ -4,11 +4,10 @@ import android.util.Log
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import com.google.gson.JsonElement
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.BehaviorSubject
import org.autojs.autojs.core.pref.Pref.getBoolean
import org.autojs.autojs.core.pref.Pref.putBoolean
import org.autojs.autojs.util.StringUtils.key
import org.autojs.autojs.util.ViewUtils
import org.autojs.autojs6.R
import java.io.IOException
import java.net.ServerSocket
@@ -29,7 +28,13 @@ class JsonSocketServer(service: DevPluginService?, port: Int) : JsonSocket(servi
}
}
override fun isSocketReady() = mSocket != null && !mSocket!!.isClosed
override fun isSocketReady(): Boolean {
val s = mSocket ?: return false
return s.isConnected &&
!s.isClosed &&
!s.isInputShutdown &&
!s.isOutputShutdown
}
private val isServerSocketSetUp
get() = serverSocket != null && !serverSocket!!.isClosed
@@ -105,8 +110,20 @@ class JsonSocketServer(service: DevPluginService?, port: Int) : JsonSocket(servi
@MainThread
public override fun onSocketError(e: Throwable) {
Log.w(TAG, "onSocketError")
e.printStackTrace()
ViewUtils.showToast(context, e.message)
// Close client socket and keep listening socket alive.
// zh-CN: 关闭客户端 socket, 保持监听 socket 存活.
try {
close()
} catch (ex: Exception) {
ex.printStackTrace()
}
// Notify service to update connection count.
// zh-CN: 通知 service 更新连接计数.
service.onServerClientDisconnected(this)
}
override fun setStateConnected() = also { setState(cxnState, DevPluginService.State.CONNECTED) }
@@ -119,7 +136,10 @@ class JsonSocketServer(service: DevPluginService?, port: Int) : JsonSocket(servi
private val TAG = JsonSocketServer::class.java.simpleName
val cxnState = PublishSubject.create<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
get() = getBoolean(key(R.string.key_server_socket_normally_closed), true)

View File

@@ -13,8 +13,8 @@ open class AccessibilityService(final override val context: Context) : ServiceIt
override val isRunning
get() = mA11yTool.hasService() || mA11yTool.isRunning()
override fun active(): Boolean {
return mA11yTool.restartService(true)
override fun active() {
mA11yTool.restartService(true)
}
override fun start(): Boolean {

View File

@@ -9,7 +9,9 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import org.autojs.autojs.App
import org.autojs.autojs.app.tool.FloatingButtonTool
import org.autojs.autojs.app.tool.JsonSocketClientTool
import org.autojs.autojs.app.tool.JsonSocketServerTool
@@ -54,7 +56,7 @@ import kotlin.math.roundToInt
/**
* Created by Stardust on Jan 30, 2017.
* Modified by SuperMonster003 as of Nov 16, 2021.
* Modified by SuperMonster003 as of Jan 9, 2026.
* Transformed by SuperMonster003 on Sep 19, 2022.
*/
open class DrawerFragment : Fragment() {
@@ -185,42 +187,78 @@ open class DrawerFragment : Fragment() {
R.string.key_floating_menu_shown,
)
JsonSocketClientTool(mContext).apply {
mClientModeItem = DrawerMenuDisposableItem(this, R.drawable.ic_computer_black_48dp, R.string.text_client_mode).also {
setClientModeItem(it)
JsonSocketClientTool(mActivity).apply {
val devPluginService = App.app.devPluginService
val drawerItem = DrawerMenuDisposableItem(this, R.drawable.ic_computer_black_48dp, R.string.text_client_mode).also {
mClientModeItem = it
}
setStateDisposable(
JsonSocketClient.cxnState
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.state == DevPluginService.State.DISCONNECTED) {
mClientModeItem.subtitle = null
}
consumeJsonSocketItemState(mClientModeItem, it)
})
val disposable = Observable
.combineLatest(
JsonSocketClient.cxnState,
devPluginService.clientConnectionIpAddress,
) { state: DevPluginService.State, ip: String ->
Pair(state, ip)
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (state, ip) ->
drawerItem.subtitle = when {
state.isDisconnected() -> null
else -> ip
}
drawerItem.setCheckedIfNeeded(state.isConnected())
drawerItem.isProgress = state.isConnecting()
state.exception?.let { e ->
drawerItem.subtitle = null
ViewUtils.showToast(mContext, e.message)
}
}
setStateDisposable(disposable)
setOnConnectionException { e: Throwable ->
mClientModeItem.setCheckedIfNeeded(false)
drawerItem.setCheckedIfNeeded(false)
ViewUtils.showToast(context, getString(R.string.error_connect_to_remote, e.message), true)
}
setOnConnectionDialogDismissed { mClientModeItem.setCheckedIfNeeded(false) }
setOnConnectionDialogDismissed { drawerItem.setCheckedIfNeeded(false) }
connectIfNotNormallyClosed()
}
JsonSocketServerTool(mContext).apply {
setStateDisposable(
JsonSocketServer.cxnState
.observeOn(AndroidSchedulers.mainThread())
.subscribe { state: DevPluginService.State ->
mServerModeItem.subtitle = takeIf { state.state == DevPluginService.State.CONNECTED }?.let {
NetworkUtils.getIpAddress()
JsonSocketServerTool(mActivity).apply {
val devPluginService = App.app.devPluginService
val drawerItem = DrawerMenuDisposableItem(this, R.drawable.ic_smartphone_black_48dp, R.string.text_server_mode).also {
mServerModeItem = it
}
val disposable = Observable
.combineLatest(
JsonSocketServer.cxnState,
devPluginService.serverConnectionCount,
) { state: DevPluginService.State, count: Int ->
Pair(state, count)
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (state, count) ->
drawerItem.subtitle = when {
state.isDisconnected() -> null
else -> NetworkUtils.getIpAddress().let { ip ->
when {
count > 0 -> "$ip [ ${context.getString(R.string.text_connected_count_with_colon, count)} ]"
else -> ip
}
}
consumeJsonSocketItemState(mServerModeItem, state)
})
}
drawerItem.setCheckedIfNeeded(!state.isDisconnected())
drawerItem.isProgress = state.isConnecting()
state.exception?.let { e ->
ViewUtils.showToast(mContext, e.message)
}
}
setStateDisposable(disposable)
setOnConnectionException { e: Throwable ->
mServerModeItem.setCheckedIfNeeded(false)
drawerItem.setCheckedIfNeeded(false)
ViewUtils.showToast(context, getString(R.string.error_enable_server, e.message), true)
}
mServerModeItem = DrawerMenuDisposableItem(this, R.drawable.ic_smartphone_black_48dp, R.string.text_server_mode)
connectIfNotNormallyClosed()
}
@@ -530,15 +568,6 @@ open class DrawerFragment : Fragment() {
mKeepScreenOnWhenInForegroundItem,
).forEach { it.sync() }
private fun consumeJsonSocketItemState(item: DrawerMenuToggleableItem, state: DevPluginService.State) {
item.setCheckedIfNeeded(state.state == DevPluginService.State.CONNECTED)
item.isProgress = state.state == DevPluginService.State.CONNECTING
state.exception?.let { e ->
item.subtitle = null
ViewUtils.showToast(mContext, e.message)
}
}
companion object {
lateinit var drawerMenuAdapter: DrawerMenuAdapter

View File

@@ -7,9 +7,8 @@ abstract class DrawerMenuItemCustomHelper internal constructor(private val mCont
override val context: Context
get() = mContext
override fun active(): Boolean {
if (isActive) return true
return toggle()
override fun active() {
if (!isActive) toggle()
}
}

View File

@@ -15,7 +15,7 @@ interface DrawerMenuItemHelper {
val isInMainThread: Boolean
get() = false
fun active(): Boolean
fun active()
fun toggle(): Boolean

View File

@@ -5,9 +5,8 @@ interface PermissionItemHelper : DrawerMenuItemHelper {
override val isActive
get() = has()
override fun active(): Boolean {
if (has()) return true
return request()
override fun active() {
requestIfNeeded()
}
fun has(): Boolean

View File

@@ -5,9 +5,8 @@ interface RunnableItemHelper : DrawerMenuItemHelper {
override val isActive: Boolean
get() = isRunning
override fun active(): Boolean {
if (isRunning) return true
return launch()
override fun active() {
launchIfNeeded()
}
val isRunning: Boolean

View File

@@ -5,9 +5,8 @@ interface ServiceItemHelper : DrawerMenuItemHelper {
override val isActive: Boolean
get() = isRunning
override fun active(): Boolean {
if (isRunning) return true
return start()
override fun active() {
startIfNeeded()
}
val isRunning: Boolean

View File

@@ -5,9 +5,8 @@ interface ShowableItemHelper : DrawerMenuItemHelper {
override val isActive: Boolean
get() = isShowing
override fun active(): Boolean {
if (isShowing) return true
return show()
override fun active() {
showIfNeeded()
}
val isShowing: Boolean

View File

@@ -5,14 +5,15 @@ interface SocketItemHelper : DrawerMenuItemHelper {
override val isActive
get() = isConnected
override fun active(): Boolean {
if (isConnected) return true
return connect()
override fun active() {
connectIfNeeded()
}
val isConnected: Boolean
fun connect(): Boolean
var isNormallyClosed: Boolean
fun connect()
fun dispose()
@@ -20,7 +21,7 @@ interface SocketItemHelper : DrawerMenuItemHelper {
if (!isConnected) connect()
}
fun disconnect(): Boolean
fun disconnect()
fun disconnectIfNeeded() {
if (isConnected) disconnect()
@@ -28,6 +29,8 @@ interface SocketItemHelper : DrawerMenuItemHelper {
override fun toggle() = toggle(!isConnected)
override fun toggle(aimState: Boolean): Boolean = if (aimState) connect() else disconnect()
override fun toggle(aimState: Boolean): Boolean = runCatching {
if (aimState) connect() else disconnect()
}.isSuccess
}

View File

@@ -20,11 +20,13 @@ import java.util.Locale
/**
* Created by Stardust on Apr 9, 2017.
* Modified by SuperMonster003 as of Jun 3, 2022.
* Modified by SuperMonster003 as of Jan 9, 2026.
*/
@Suppress("DEPRECATION", "unused")
object NetworkUtils {
const val DEFAULT_IP_ADDRESS = "0.0.0.0"
private val globalAppContext = GlobalAppContext.get()
private val connectivityManager
@@ -47,7 +49,7 @@ object NetworkUtils {
} catch (e: UnknownHostException) {
Log.e(NetworkUtils::class.java.simpleName, "Error getting Hotspot IP address ", e)
null
} ?: "0.0.0.0"
} ?: DEFAULT_IP_ADDRESS
private fun getIpAddressByte(): ByteArray {
val manager = globalAppContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
@@ -185,7 +187,7 @@ object NetworkUtils {
} catch (ignored: Exception) {
// for now eat exceptions
}
return "0.0.0.0"
return DEFAULT_IP_ADDRESS
}
@JvmStatic

View File

@@ -1140,5 +1140,7 @@
<string name="text_scheduled_restart_start_delay">تأخير البدء</string>
<string name="dialog_button_advanced_settings">متقدم</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>

View File

@@ -1135,5 +1135,7 @@
<string name="text_scheduled_restart_start_delay">Start delay</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="text_connected">Connected</string>
<string name="text_connected_count_with_colon">Connected: %1$d</string>
</resources>

View File

@@ -1138,5 +1138,7 @@
<string name="text_scheduled_restart_start_delay">Retraso de inicio</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="text_connected">Conectado</string>
<string name="text_connected_count_with_colon">Conectados: %1$d</string>
</resources>

View File

@@ -1138,5 +1138,7 @@
<string name="text_scheduled_restart_start_delay">Délai de démarrage</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="text_connected">Connecté</string>
<string name="text_connected_count_with_colon">Connectés : %1$d</string>
</resources>

View File

@@ -1139,5 +1139,7 @@
<string name="text_scheduled_restart_start_delay">開始遅延</string>
<string name="dialog_button_advanced_settings">詳細</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>

View File

@@ -1140,5 +1140,7 @@
<string name="text_scheduled_restart_start_delay">시작 지연</string>
<string name="dialog_button_advanced_settings">고급</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>

View File

@@ -1138,5 +1138,7 @@
<string name="text_scheduled_restart_start_delay">Задержка старта</string>
<string name="dialog_button_advanced_settings">Доп.</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>

View File

@@ -1136,5 +1136,7 @@
<string name="text_scheduled_restart_start_delay">啓動延遲</string>
<string name="dialog_button_advanced_settings">高級設置</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>

View File

@@ -1136,5 +1136,7 @@
<string name="text_scheduled_restart_start_delay">啟動延遲</string>
<string name="dialog_button_advanced_settings">高階設定</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>

View File

@@ -1136,5 +1136,7 @@
<string name="text_scheduled_restart_start_delay">启动延迟</string>
<string name="dialog_button_advanced_settings">高级设置</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>

View File

@@ -1393,5 +1393,7 @@
<string name="text_scheduled_restart_start_delay">Start delay</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="text_connected">Connected</string>
<string name="text_connected_count_with_colon">Connected: %1$d</string>
</resources>

View File

@@ -1,5 +1,5 @@
#Thu Jan 08 01:30:58 CST 2026
BUILD_TIME=1767807058123
#Fri Jan 09 23:39:38 CST 2026
BUILD_TIME=1767973178586
COMPILE_SDK_VERSION=36
IMAGE_QUANT_CMAKE_VERSION=3.22.1
IMAGE_QUANT_NDK_VERSION=26.1.10909125
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
TARGET_SDK_VERSION=36
TARGET_SDK_VERSION_INRT=29
VERSION_BUILD=3583
VERSION_NAME=6.7.0 Alpha13
VSCODE_EXT_REQUIRED_VERSION=1.0.8
VERSION_BUILD=3591
VERSION_NAME=6.7.0 Alpha14
VSCODE_EXT_REQUIRED_VERSION=1.0.13