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

@@ -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>