6.7.0 - Alpha14 - 修复服务端模式连接时, Activity 重建操作导致 VSCode 控制台无法输出日志的问题 (issue #385); 深度优化 VSCode 插件连接相关代码逻辑
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@ interface DrawerMenuItemHelper {
|
||||
val isInMainThread: Boolean
|
||||
get() = false
|
||||
|
||||
fun active(): Boolean
|
||||
fun active()
|
||||
|
||||
fun toggle(): Boolean
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user