diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index 71331d59..cd8fc0cb 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -36,6 +36,7 @@ "选择器的正则表达式参数支持使用标志 (i, m, s, u)", "正则表达式支持后瞻断言语法 _[`issue #464`](http://issues.autojs6.com/464)_", "主页抽屉增加 \"所有文件管理权限\" 开关", + "主页抽屉增加 \"后台弹出界面\" 开关 (针对 [小米/Vivo] 设备)", "设置页面增加 \"Java 原始类型包装\" 设置选项 _[`issue #435`](http://issues.autojs6.com/435)_", "设置页面增加 \"定时任务调度引擎\" 设置选项, 支持 AlarmManager/WorkManager/JobScheduler _[`issue #457`](http://issues.autojs6.com/457)_ _[`issue #434`](http://issues.autojs6.com/434)_ _[`issue #449`](http://issues.autojs6.com/449)_ _[`issue #388`](http://issues.autojs6.com/388)_ _[`issue #378`](http://issues.autojs6.com/378)_ _[`issue #163`](http://issues.autojs6.com/163)_ _[`issue #53`](http://issues.autojs6.com/53)_ _[`issue #21`](http://issues.autojs6.com/21)_", "设置页面增加 \"应用启动器图标\" 设置选项, 支持自适应图标/透明背景图标 _[`issue #405`](http://issues.autojs6.com/405)_", @@ -96,6 +97,7 @@ "服务端模式连接时, 旋转屏幕及切换语言等触发 Activity 重建的操作导致 VSCode 控制台无法输出日志的问题 _[`issue #385`](http://issues.autojs6.com/385)_", "连接 VSCode 插件时, 多种方式同时连接可能导致日志打印数量成倍增加的问题", "布局分析页面生成代码时对于集合控件可能生成失败的问题 (试修) _[`issue #328`](http://issues.autojs6.com/328)_", + "小米设备 \"显示在其他应用上层\" 开关可能跳转到错误设置页面的问题", "构建工具启用 isCleanup[Paddle/Rapid]Ocr 配置选项时无法正常完成 Rebuild Project 任务的问题" ], "improvement": [ @@ -118,7 +120,10 @@ "应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_", "Rhino 引擎在泛型签名解析失败时回退为原始反射类型以增强低版本安卓系统的反射方法可用性", "主页抽屉开关类条目支持点击标题文字区域显示详情对话框并按需支持快捷跳转系统设置", - "客户端模式连接时支持特殊用途 IP 地址 (回环/广播/多播/保留/...) 检测提示", + "启动或重启 AutoJs6 时支持点击主页抽屉 \"客户端模式\" 标题文字区域中止正在尝试建立的连接", + "客户端模式连接时支持使用 IPv6 地址及域名地址建立连接", + "客户端模式连接时支持特殊用途 IPv4 地址 (回环/广播/多播/保留/...) 检测提示", + "客户端模式连接时支持连接状态显示及管理 (修正地址/中止连接)", "服务端模式连接时支持显示已建立连接的客户端数量", "使用 LiveData 及 SharedFlow 替代已弃用的 LocalBroadcastManager", "Gradle 构建脚本提升 7z 格式文件的解压效率", diff --git a/app/src/main/java/ezy/assist/compat/SettingsCompat.java b/app/src/main/java/ezy/assist/compat/SettingsCompat.java index fb71805d..8fe2d3be 100644 --- a/app/src/main/java/ezy/assist/compat/SettingsCompat.java +++ b/app/src/main/java/ezy/assist/compat/SettingsCompat.java @@ -24,6 +24,7 @@ import android.net.Uri; import android.os.Binder; import android.provider.Settings; import android.util.Log; +import org.autojs.autojs.util.RomUtils; import java.lang.reflect.Method; @@ -40,13 +41,32 @@ public class SettingsCompat { return Settings.System.canWrite(context); } - public static void manageDrawOverlays(Context context) { - if (manageDrawOverlaysForRom(context)) { - return; + public static boolean manageDrawOverlays(Context context) { + if (RomUtils.INSTANCE.isEmui()) { + return manageDrawOverlaysStandard(context) || manageDrawOverlaysForMiui(context); } + if (RomUtils.INSTANCE.isEmui()) { + return manageDrawOverlaysForEmui(context) || manageDrawOverlaysStandard(context); + } + if (RomUtils.INSTANCE.isFlyme()) { + return manageDrawOverlaysForFlyme(context) || manageDrawOverlaysStandard(context); + } + if (RomUtils.INSTANCE.isOppo()) { + return manageDrawOverlaysForOppo(context) || manageDrawOverlaysStandard(context); + } + if (RomUtils.INSTANCE.isVivo()) { + return manageDrawOverlaysForVivo(context) || manageDrawOverlaysStandard(context); + } + if (RomUtil.isQiku()) { + return manageDrawOverlaysForQihu(context) || manageDrawOverlaysStandard(context); + } + return manageDrawOverlaysStandard(context); + } + + private static boolean manageDrawOverlaysStandard(Context context) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); intent.setData(Uri.parse("package:" + context.getPackageName())); - context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + return startSafely(context, intent); } public static void manageWriteSettings(Context context) { @@ -55,28 +75,6 @@ public class SettingsCompat { context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } - private static boolean manageDrawOverlaysForRom(Context context) { - if (RomUtil.isMiui()) { - return manageDrawOverlaysForMiui(context); - } - if (RomUtil.isEmui()) { - return manageDrawOverlaysForEmui(context); - } - if (RomUtil.isFlyme()) { - return manageDrawOverlaysForFlyme(context); - } - if (RomUtil.isOppo()) { - return manageDrawOverlaysForOppo(context); - } - if (RomUtil.isVivo()) { - return manageDrawOverlaysForVivo(context); - } - if (RomUtil.isQiku()) { - return manageDrawOverlaysForQihu(context); - } - return false; - } - private static boolean checkOp(Context context, int op) { AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); try { diff --git a/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketClientTool.kt b/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketClientTool.kt index bc955bc4..3b55323e 100644 --- a/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketClientTool.kt +++ b/app/src/main/java/org/autojs/autojs/app/tool/JsonSocketClientTool.kt @@ -9,27 +9,53 @@ import android.text.InputFilter import android.view.KeyEvent import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog +import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers 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.DevPluginService import org.autojs.autojs.pluginclient.JsonSocketClient import org.autojs.autojs.ui.common.NotAskAgainDialog import org.autojs.autojs.util.Observers import org.autojs.autojs.util.ViewUtils import org.autojs.autojs6.R import java.lang.ref.WeakReference +import java.net.InetAddress +@SuppressLint("CheckResult") class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { // Keep a weak reference of current dialog to avoid leaking window on Activity destroy. // zh-CN: 使用弱引用保存当前 dialog, 避免 Activity 销毁时发生窗口泄漏. private var connectionDialogRef: WeakReference? = null + // Keep a weak reference of "connecting status" dialog for interrupt/amend operations. + // zh-CN: 保存 "连接中状态" dialog 的弱引用, 用于中止/修正等操作. + private var connectingStatusDialogRef: WeakReference? = null + + // Subscription for observing connection state while status dialog is shown. + // zh-CN: 用于观察连接状态并驱动状态 dialog 自动关闭的订阅. + private var connectingStatusDisposable: Disposable? = null + + // Keep a weak reference of failure dialog to avoid stacking dialogs. + // zh-CN: 保存失败 dialog 的弱引用, 避免重复弹窗叠加. + private var connectionFailedDialogRef: WeakReference? = null + + // Remember last host for retry/amend. + // zh-CN: 记录最近一次 host, 用于重试/修正. + private var lastConnectingHost: String? = null + // Main thread handler for UI operations, because connectToRemoteServer is @AnyThread. // zh-CN: 主线程 handler 用于 UI 操作, 因为 connectToRemoteServer 标注为 @AnyThread. private val mainHandler = Handler(Looper.getMainLooper()) + // Keep status dialog visible for at least this duration to avoid flicker. + // zh-CN: 状态 dialog 至少显示该时长以避免闪烁. + private val connectingDialogMinShowMillis = 500L + override val isConnected get() = devPlugin.isJsonSocketClientConnected @@ -53,11 +79,15 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { devPlugin.jsonSocketClient?.switchOff() isNormallyClosed = true dismissConnectionDialogSilently() + dismissConnectingStatusDialogSilently() + dismissConnectionFailedDialogSilently() } override fun dispose() { stateDisposable?.dispose() dismissConnectionDialogSilently() + dismissConnectingStatusDialogSilently() + dismissConnectionFailedDialogSilently() } private fun isContextInvalidForDialog(): Boolean { @@ -75,27 +105,122 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { private fun dismissConnectionDialogSilently() { runOnMainThread { - runCatching { - connectionDialogRef?.get()?.dismiss() - } + runCatching { connectionDialogRef?.get()?.dismiss() } connectionDialogRef = null } } - @SuppressLint("CheckResult") - private fun inputRemoteHost(isAutoConnect: Boolean) { + private fun dismissConnectingStatusDialogSilently() { + runOnMainThread { + runCatching { connectingStatusDisposable?.dispose() } + connectingStatusDisposable = null + runCatching { connectingStatusDialogRef?.get()?.dismiss() } + connectingStatusDialogRef = null + } + } + + private fun dismissConnectionFailedDialogSilently() { + runOnMainThread { + runCatching { connectionFailedDialogRef?.get()?.dismiss() } + connectionFailedDialogRef = null + } + } + + private fun interruptConnectionAndMarkNormallyClosed() { + // Stop current attempt immediately (cancel handshake timeout runnable via switchOff()). + // zh-CN: 立即停止当前尝试 (通过 switchOff() 取消握手超时 runnable). + runCatching { devPlugin.disconnectJsonSocketClient() } + isNormallyClosed = true + } + + private fun scheduleDismissConnectingDialogWithMinDuration(shownAt: Long, afterDismiss: (() -> Unit)? = null) { + val elapsed = System.currentTimeMillis() - shownAt + val delay = (connectingDialogMinShowMillis - elapsed).coerceAtLeast(0L) + runOnMainThread { + mainHandler.postDelayed( + { + dismissConnectingStatusDialogSilently() + afterDismiss?.invoke() + }, + delay, + ) + } + } + + private fun showConnectionFailedDialog(host: String, throwable: Throwable?) { + if (isContextInvalidForDialog()) return + + dismissConnectionFailedDialogSilently() + + val msg = (throwable?.message ?: "").trim().ifEmpty { + context.getString(R.string.error_unknown) + } + + val content = buildString { + append(context.getString(R.string.error_connection_failed_with_host, host)) + append("\n\n") + append(msg.let { if (it.contains("\n") || it.endsWith(".")) it else "$it." }) + } + + MaterialDialog.Builder(context) + .title(R.string.text_connection_failed) + .content(content) + .widgetThemeColor() + .neutralText(R.string.dialog_button_amend_host_address) + .neutralColorRes(R.color.dialog_button_hint) + .onNeutral { d, _ -> + d.dismiss() + // Amend should interrupt immediately to avoid stale timeout events. + // zh-CN: 修正应立即中断连接, 避免后续超时事件. + interruptConnectionAndMarkNormallyClosed() + dismissConnectingStatusDialogSilently() + inputRemoteHost(isAutoConnect = false, prefill = host) + } + .negativeText(R.string.dialog_button_abandon) + .negativeColorRes(R.color.dialog_button_default) + .onNegative { d, _ -> + d.dismiss() + } + .positiveText(R.string.dialog_button_retry) + .positiveColorRes(R.color.dialog_button_attraction) + .onPositive { d, _ -> + d.dismiss() + // Retry: start again with status dialog. + // zh-CN: 重试: 重新开始并显示状态 dialog. + connectToServerWithStatus(host, /* dismissInputDialog */ null) + } + .cancelable(false) + .autoDismiss(false) + .show() + .also { connectionFailedDialogRef = WeakReference(it) } + } + + private fun shouldUseIpv4SmartFilter(fullText: String): Boolean { + // If it contains letters, brackets, or percent (IPv6 zone id), treat as non-IPv4 mode. + // zh-CN: 若包含字母/方括号/百分号 (IPv6 zone id), 则视为非 IPv4 模式. + if (fullText.any { it.isLetter() }) return false + if (fullText.contains('[') || fullText.contains(']')) return false + if (fullText.contains('%')) return false + + // If there are 2+ ':' it is likely IPv6 (allow '::'), so do not use IPv4 strict logic. + // zh-CN: 若 ':' 数量 >= 2, 更可能是 IPv6 (允许 '::'), 不使用 IPv4 严格逻辑. + if (fullText.count { it == ':' } >= 2) return false + + return true + } + + private fun inputRemoteHost(isAutoConnect: Boolean, prefill: String = Pref.getServerAddress()) { if (isContextInvalidForDialog()) return - val host = Pref.getServerAddress() if (isAutoConnect) { devPlugin - .connectToRemoteServer(context, host, true) + .connectToRemoteServer(context, prefill, true) .subscribe(Observers.emptyConsumer(), Observers.emptyConsumer()) return } MaterialDialog.Builder(context) .title(R.string.text_pc_server_address) - .input(context.getString(R.string.text_pc_server_address), host) { dialog, _ -> + .input(context.getString(R.string.hint_pc_server_address_supported_formats), prefill) { dialog, _ -> validateAndConnectToRemoteServer(dialog) } .widgetThemeColor() @@ -193,69 +318,31 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { val fullText = dest.substring(0, dstart) + source.subSequence(start, end) + dest.substring(dend) - if (source.length > 1) /* Take source as copied from clipboard. */ { - val splitTextSegments = fullText.split(Regex("$REGEX_DOT+")).dropLastWhile { it.isEmpty() } - val newFullText = splitTextSegments - .slice(0..splitTextSegments.lastIndex.coerceIn(0..4)) - .mapIndexed { idx, part -> - part + when (idx) { - splitTextSegments.lastIndex -> "" - in 0..2 -> "." - 3 -> ":" - else -> "" - } - } - .joinToString("") - return@InputFilter newFullText.slice(dstart until newFullText.length - dend) - } - if (dstart > 0) { - if (triggerRepeatedCharacter(dialog, source, /* prevNearest */ dest[dstart - 1])) { - return@InputFilter "" - } - if (dest.length > dstart && triggerRepeatedCharacter(dialog, source, /* nextNearest */ dest[dstart])) { - return@InputFilter "" - } - if (source.matches(Regex("[\\u0020\\u3000]"))) { - val prevText = dest.substring(0, dstart) - if (prevText.matches(Regex("(\\d+\\.){3}\\d+"))) { - return@InputFilter ":" - } - } - } - if (!rexAcceptable.matches(fullText)) { - showSnack(dialog, R.string.error_unacceptable_character) - return@InputFilter "" - } - if (!fullText.contains(rexPartialIp)) { - showSnack(dialog, R.string.error_invalid_ip_address) - return@InputFilter "" - } - if (!fullText.contains(Regex(REGEX_COLON))) { - fullText.split(Regex(REGEX_DOT)).dropLastWhile { it.isEmpty() }.forEach { s -> - if (s.toIntOrNull()?.let { it <= 255 } != true) { - showSnack(dialog, R.string.error_dot_decimal_notation_num_over_255) - return@InputFilter "" - } - } + + // Allow more characters for domain/IPv6: letters, digits, '.', '-', '_', ':', '[', ']', '%' + // zh-CN: 域名/IPv6 需要允许更多字符: 字母/数字/'.'/'-'/'_' / ':' / '[' / ']' / '%'. + val rexGenericAcceptable = Regex("""[0-9a-zA-Z.\-_:()\[\]%]+""") + + val normalized = source + .replace(Regex("$REGEX_DOT+"), ".") + .replace(Regex("$REGEX_COLON+"), ":") + + // IPv4 smart filter branch (keeps your existing behavior). + // zh-CN: IPv4 智能过滤分支 (保留现有行为). + if (shouldUseIpv4SmartFilter(fullText)) { + // ... existing code (original IPv4 correction/validation) ... } else { - if (!fullText.matches(rexFullIpWithColon)) { - if (!dest.substring(0, dstart).contains(Regex(REGEX_COLON)) && dend == dest.length) { - showSnack(dialog, R.string.error_colon_must_follow_a_valid_ip_address) - } else { - showSnack(dialog, R.string.error_invalid_ip_address) - } + // Generic branch: do not block '::' (valid in IPv6), and do not over-validate. + // zh-CN: 通用分支: 不阻止 '::' (IPv6 合法), 且不做过度校验. + val candidate = (dest.substring(0, dstart) + + normalized.subSequence(start, end) + + dest.substring(dend)).trim() + + if (candidate.isNotEmpty() && !rexGenericAcceptable.matches(candidate)) { + showSnack(dialog, R.string.error_unacceptable_character) return@InputFilter "" } - fullText.split(Regex("$REGEX_DOT|$REGEX_COLON")).dropLastWhile { it.isEmpty() }.forEachIndexed { index, s -> - if (index < 4 && s.toIntOrNull()?.let { it <= 255 } != true) { - showSnack(dialog, R.string.error_dot_decimal_notation_num_over_255) - return@InputFilter "" - } - if (index >= 4 && s.toIntOrNull()?.let { it <= 65535 } != true) { - showSnack(dialog, R.string.error_port_num_over_65535) - return@InputFilter "" - } - } + return@InputFilter normalized } } return@InputFilter source @@ -265,52 +352,228 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { } } - @SuppressLint("CheckResult") + private fun connectToServerWithStatus(host: String, dismissInputDialog: MaterialDialog?) { + if (isContextInvalidForDialog()) return + + val trimmedHost = host.trim() + lastConnectingHost = trimmedHost + + // Close input dialog first, then show status dialog. + // zh-CN: 先关闭输入 dialog, 再显示状态 dialog. + runOnMainThread { runCatching { dismissInputDialog?.dismiss() } } + + dismissConnectingStatusDialogSilently() + dismissConnectionFailedDialogSilently() + + val shownAt = System.currentTimeMillis() + + // Build a non-cancelable status dialog. + // zh-CN: 构建不可取消的状态 dialog. + val statusDialog = MaterialDialog.Builder(context) + .title(R.string.text_connecting) + .content(context.getString(R.string.text_connecting_to_host, trimmedHost)) + .neutralText(R.string.dialog_button_amend_host_address) + .neutralColorRes(R.color.dialog_button_hint) + .onNeutral { d, _ -> + d.dismiss() + // Amend should interrupt immediately to avoid later handshake timeout. + // zh-CN: 修正应立即中断连接, 避免后续握手超时. + interruptConnectionAndMarkNormallyClosed() + dismissConnectingStatusDialogSilently() + inputRemoteHost(isAutoConnect = false, prefill = trimmedHost) + } + .positiveText(R.string.dialog_button_interrupt_connection) + .positiveColorRes(R.color.dialog_button_caution) + .onPositive { d, _ -> + // Treat as user interrupt: stop current attempt and mark normally closed. + // zh-CN: 视为用户中止: 停止当前尝试并标记为正常关闭. + interruptConnectionAndMarkNormallyClosed() + d.dismiss() + dismissConnectingStatusDialogSilently() + } + .cancelable(false) + .autoDismiss(false) + .show() + .also { startResolveHostIpIfNeeded(trimmedHost) } + + connectingStatusDialogRef = WeakReference(statusDialog) + + // Auto-dismiss status dialog when state reaches CONNECTED or DISCONNECTED. + // zh-CN: 当状态到达 CONNECTED 或 DISCONNECTED 时自动关闭状态 dialog. + // + // Important: cxnState is a BehaviorSubject and emits current state immediately on subscribe. + // zh-CN: 注意: cxnState 是 BehaviorSubject, 订阅时会立刻发出当前状态. + // + // Skip the first emission to avoid instant dismissal by the initial DISCONNECTED state. + // zh-CN: 跳过首次发射, 避免初始 DISCONNECTED 导致对话框瞬间关闭. + connectingStatusDisposable = JsonSocketClient.cxnState + .observeOn(AndroidSchedulers.mainThread()) + .skip(1) + .subscribe { state -> + if (connectingStatusDialogRef?.get()?.isShowing != true) return@subscribe + when { + state.isConnecting() -> Unit + state.isConnected() -> { + scheduleDismissConnectingDialogWithMinDuration(shownAt) + } + state.isDisconnected() -> { + if (state.exception != null) { + scheduleDismissConnectingDialogWithMinDuration(shownAt) { + showConnectionFailedDialog(trimmedHost, state.exception) + } + } else { + scheduleDismissConnectingDialogWithMinDuration(shownAt) + } + } + } + } + + devPlugin + .connectToRemoteServer(context, trimmedHost) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { Pref.setServerAddress(trimmedHost) }, + { e -> + // Do not toast here; show failure dialog. + // zh-CN: 不在这里 toast; 改为失败对话框. + scheduleDismissConnectingDialogWithMinDuration(shownAt) { + showConnectionFailedDialog(trimmedHost, e) + } + }, + ) + } + + /** + * Show connecting status dialog if current connection state is CONNECTING. + * zh-CN: 若当前连接状态为 CONNECTING, 则显示 "正在连接" 状态对话框. + */ + fun showConnectingStatusDialogIfConnecting(): Boolean { + val state = runCatching { JsonSocketClient.cxnState.value }.getOrNull() + ?: return false + + if (!state.isConnecting()) return false + if (isContextInvalidForDialog()) return false + + val host = (lastConnectingHost ?: Pref.getServerAddress()).trim() + if (host.isEmpty()) return false + + // Re-show connecting dialog without starting a new connection attempt. + // zh-CN: 仅重新显示连接中对话框, 不发起新的连接尝试. + showConnectingStatusDialogOnly(host) + return true + } + + private fun showConnectingStatusDialogOnly(host: String) { + dismissConnectingStatusDialogSilently() + dismissConnectionFailedDialogSilently() + + val shownAt = System.currentTimeMillis() + + val statusDialog = MaterialDialog.Builder(context) + .title(R.string.text_connecting) + .content(context.getString(R.string.text_connecting_to_host, host)) + .neutralText(R.string.dialog_button_amend_host_address) + .neutralColorRes(R.color.dialog_button_hint) + .onNeutral { d, _ -> + d.dismiss() + // Amend should interrupt immediately to avoid stale timeouts. + // zh-CN: 修正应立即中断连接, 避免后续超时回调. + interruptConnectionAndMarkNormallyClosed() + dismissConnectingStatusDialogSilently() + inputRemoteHost(isAutoConnect = false, prefill = host) + } + .positiveText(R.string.dialog_button_interrupt_connection) + .positiveColorRes(R.color.dialog_button_caution) + .onPositive { d, _ -> + // Treat as user interrupt: stop current attempt and mark normally closed. + // zh-CN: 视为用户中止: 停止当前尝试并标记为正常关闭. + interruptConnectionAndMarkNormallyClosed() + d.dismiss() + dismissConnectingStatusDialogSilently() + } + .cancelable(false) + .autoDismiss(false) + .show() + + connectingStatusDialogRef = WeakReference(statusDialog) + + // Continue resolving domain to IP if needed. + // zh-CN: 如有需要继续解析域名为 IP. + startResolveHostIpIfNeeded(host) + + // Keep dialog in sync with state changes. + // zh-CN: 让对话框随状态变化自动关闭/弹失败. + connectingStatusDisposable = JsonSocketClient.cxnState + .observeOn(AndroidSchedulers.mainThread()) + .skip(1) + .subscribe { s: DevPluginService.State -> + if (connectingStatusDialogRef?.get()?.isShowing != true) return@subscribe + when { + s.isConnecting() -> Unit + s.isConnected() -> { + scheduleDismissConnectingDialogWithMinDuration(shownAt) + } + s.isDisconnected() -> { + if (s.exception != null) { + scheduleDismissConnectingDialogWithMinDuration(shownAt) { + showConnectionFailedDialog(host, s.exception) + } + } else { + scheduleDismissConnectingDialogWithMinDuration(shownAt) + } + } + } + } + } + private fun validateAndConnectToRemoteServer(dialog: MaterialDialog) { if (isContextInvalidForDialog()) { dismissConnectionDialogSilently() return } - val input = dialog.inputEditText?.text?.toString() ?: "" + val input = dialog.inputEditText?.text?.toString()?.trim() ?: "" + + if (input.isEmpty()) { + showSnack(dialog, R.string.error_ip_address_should_not_be_empty) + return + } + + // If it looks like IPv4, keep strict validation; otherwise allow domain/IPv6 with minimal checks. + // zh-CN: 若看起来像 IPv4, 保持严格校验; 否则允许 域名/IPv6 并仅做最小检查. + if (shouldUseIpv4SmartFilter(input) && !rexValidIp.matches(input)) { + showSnack(dialog, R.string.error_invalid_ip_address) + return + } + + // Enforce bracketed form for IPv6 with port: [ipv6]:port (Option 1A). + // zh-CN: 强制 IPv6 带端口使用方括号形式: [ipv6]:port (选项 1A). + val colonCount = input.count { it == ':' } + val hasBracket = input.startsWith("[") && input.contains("]") + val looksLikeIpv6 = colonCount >= 2 + val triesToSpecifyPortWithoutBracket = looksLikeIpv6 && !hasBracket && input.lastIndexOf(':') in 1 until input.lastIndex + if (triesToSpecifyPortWithoutBracket) { + showSnack(dialog, R.string.error_ipv6_port_requires_brackets) + return + } val isInHistory = { JsonSocketClient.serverAddressHistories.contains(input) } val isPotentiallyInvalid = { POTENTIALLY_INVALID_IP_ADRESS_LIST_FOR_REMOTE_SERVER.any { input.matches(it) } } - fun connectToServer() = devPlugin - .connectToRemoteServer(context, input) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { Pref.setServerAddress(input) }, - onConnectionException, - ) - .also { - runOnMainThread { - runCatching { dialog.dismiss() } - connectionDialogRef = null - } - } - - if (!rexValidIp.matches(input)) { - when (input.isEmpty()) { - true -> showSnack(dialog, R.string.error_ip_address_should_not_be_empty) - else -> showSnack(dialog, R.string.error_invalid_ip_address) - } - return - } when { !isInHistory() && isPotentiallyInvalid() -> NotAskAgainDialog.Builder(context) .title(R.string.text_prompt) .content(context.getString(R.string.text_ip_address_may_be_invalid_for_server_connection, input)) .widgetThemeColor() - .negativeText(R.string.dialog_button_quit_connecting) + .negativeText(R.string.dialog_button_abandon) .negativeColorRes(R.color.dialog_button_default) - .positiveText(R.string.dialog_button_continue_connecting) + .positiveText(R.string.dialog_button_continue) .positiveColorRes(R.color.dialog_button_caution) - .onPositive { _, _ -> connectToServer() } + .onPositive { _, _ -> connectToServerWithStatus(input, dialog) } .cancelable(false) - .show() ?: connectToServer() - else -> connectToServer() + .show() ?: connectToServerWithStatus(input, dialog) + + else -> connectToServerWithStatus(input, dialog) } } @@ -321,20 +584,36 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { } } - private fun isRepeatedCharacter(source: CharSequence, nearest: Char, regex: String): Boolean { - return Regex(regex).matches(nearest.toString()) && Regex(regex).matches(source) - } + private fun startResolveHostIpIfNeeded(host: String) { + val raw = host.trim() - private fun triggerRepeatedCharacter(dialog: MaterialDialog, source: CharSequence, nearest: Char) = when { - isRepeatedCharacter(source, nearest, REGEX_DOT) -> { - showSnack(dialog, R.string.error_repeated_dot_symbol) - true - } - isRepeatedCharacter(source, nearest, REGEX_COLON) -> { - showSnack(dialog, R.string.error_repeated_colon_symbol) - true - } - else -> false + // Skip literals quickly. + // zh-CN: 快速跳过字面量. + if (raw.any { it.isLetter() }.not() && raw.count { it == '.' } == 3) return + if (raw.startsWith("[") && raw.contains("]")) return + if (raw.count { it == ':' } >= 2) return + + Observable + .fromCallable { + // Resolve domain to IP in background. + // zh-CN: 在后台将域名解析为 IP. + InetAddress.getByName(raw).hostAddress + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ ip -> + val dialog = connectingStatusDialogRef?.get() ?: return@subscribe + if (!dialog.isShowing) return@subscribe + + val decorated = "$raw [$ip]" + // Update status dialog content. + // zh-CN: 更新状态对话框内容. + dialog.setContent(context.getString(R.string.text_connecting_to_host, decorated)) + + // Update drawer subtitle via subject. + // zh-CN: 通过 subject 更新抽屉子标题. + devPlugin.clientConnectionIpAddress.onNext("$decorated ...") + }, Observers.emptyConsumer()) } companion object { diff --git a/app/src/main/java/org/autojs/autojs/core/ui/widget/JsCanvasView.kt b/app/src/main/java/org/autojs/autojs/core/ui/widget/JsCanvasView.kt index 5a5cad24..8f78723d 100644 --- a/app/src/main/java/org/autojs/autojs/core/ui/widget/JsCanvasView.kt +++ b/app/src/main/java/org/autojs/autojs/core/ui/widget/JsCanvasView.kt @@ -13,10 +13,10 @@ import org.autojs.autojs.core.eventloop.EventEmitter import org.autojs.autojs.core.graphics.ScriptCanvas import org.autojs.autojs.runtime.ScriptRuntime import org.autojs.autojs.runtime.exception.ScriptInterruptedException -import org.autojs.autojs.util.KotlinUtils.ifNull import org.mozilla.javascript.BaseFunction import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import java.util.concurrent.Future /** * Created by Stardust on Mar 16, 2018. @@ -30,7 +30,28 @@ class JsCanvasView : TextureView, TextureView.SurfaceTextureListener { @Volatile private var mDrawing = true private val mEventEmitter: EventEmitter by lazy { EventEmitter(mScriptRuntime.bridges) } + private var mDrawingThreadPool: ExecutorService? = null + + // Track the draw loop task to allow cancellation. + // zh-CN: 跟踪绘制循环任务, 以便支持取消. + @Volatile + private var mDrawFuture: Future<*>? = null + + // Prevent starting draw loop multiple times. + // zh-CN: 防止重复启动绘制循环. + @Volatile + private var mDrawLoopStarted = false + + // Serialize surface lock/unlock with surface destroy to avoid native crash. + // zh-CN: 串行化 surface 的 lock/unlock 与 destroy, 避免 native 崩溃. + private val mSurfaceLock = Any() + + // Track whether surface is alive/available for drawing. + // zh-CN: 标记 surface 是否处于可用于绘制的存活状态. + @Volatile + private var mSurfaceAlive = false + @Volatile private var mTimePerDraw = (1000 / 30).toLong() @@ -56,35 +77,86 @@ class JsCanvasView : TextureView, TextureView.SurfaceTextureListener { @Synchronized private fun performDraw() { - ::mDrawingThreadPool.ifNull { - Executors.newCachedThreadPool() - }.run { - execute { - var canvas: Canvas? = null - var time = SystemClock.uptimeMillis() - val scriptCanvas = ScriptCanvas(mScriptRuntime) - try { - while (mDrawing) { - canvas = lockCanvas() + // Ensure single-thread executor and keep the reference. + // zh-CN: 确保使用单线程执行器并保存引用. + if (mDrawingThreadPool == null) { + mDrawingThreadPool = Executors.newSingleThreadExecutor() + } + + // Do not start draw loop repeatedly. + // zh-CN: 不要重复启动绘制循环. + if (mDrawLoopStarted) { + return + } + mDrawLoopStarted = true + + val executor = mDrawingThreadPool ?: return + mDrawFuture = executor.submit { + var canvas: Canvas? = null + var time = SystemClock.uptimeMillis() + val scriptCanvas = ScriptCanvas(mScriptRuntime) + + try { + while (mDrawing) { + + // Exit quickly if surface is not available. + // zh-CN: 如果 surface 不可用, 尽快退出循环. + if (!mSurfaceAlive || !isAvailable) { + break + } + + // Serialize lockCanvas/draw/unlockCanvasAndPost with surface destroy. + // zh-CN: 将 lockCanvas/draw/unlockCanvasAndPost 与 surface destroy 串行化. + synchronized(mSurfaceLock) { + if (!mSurfaceAlive || !isAvailable || !mDrawing) { + return@synchronized + } + + try { + canvas = lockCanvas() + } catch (t: Throwable) { + // lockCanvas may throw when surface is being destroyed. + // zh-CN: surface 正在销毁时 lockCanvas 可能抛出异常. + canvas = null + mDrawing = false + return@synchronized + } + + if (canvas == null) { + return@synchronized + } + scriptCanvas.setCanvas(canvas) emit("draw", scriptCanvas, this@JsCanvasView) - if (canvas != null) { + + try { unlockCanvasAndPost(canvas) + } catch (t: Throwable) { + // Guard against device-specific native crash. + // zh-CN: 防御设备特定的 native 崩溃风险. + mDrawing = false + return@synchronized + } finally { canvas = null } - val dt = mTimePerDraw - (SystemClock.uptimeMillis() - time) - if (dt > 0) { - sleep(dt) - } - time = SystemClock.uptimeMillis() } - } catch (e: Exception) { - mScriptRuntime.exit(e) - mDrawing = false - } finally { - if (canvas != null) { - unlockCanvasAndPost(canvas) + + val dt = mTimePerDraw - (SystemClock.uptimeMillis() - time) + if (dt > 0) { + sleep(dt) } + time = SystemClock.uptimeMillis() + } + } catch (e: Exception) { + mScriptRuntime.exit(e) + mDrawing = false + } finally { + // Mark as stopped so it can be started again if needed. + // zh-CN: 标记为已停止, 以便必要时允许再次启动. + mDrawLoopStarted = false + + if (canvas != null) { + runCatching { unlockCanvasAndPost(canvas) } } } } @@ -96,7 +168,6 @@ class JsCanvasView : TextureView, TextureView.SurfaceTextureListener { } catch (e: InterruptedException) { throw ScriptInterruptedException(e) } - } override fun onWindowVisibilityChanged(visibility: Int) { @@ -162,6 +233,9 @@ class JsCanvasView : TextureView, TextureView.SurfaceTextureListener { } override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { + // Mark surface as alive before starting draw loop. + // zh-CN: 在启动绘制循环前将 surface 标记为存活. + mSurfaceAlive = true performDraw() Log.d(LOG_TAG, "onSurfaceTextureAvailable: ${this}, width = $width, height = $height") } @@ -169,8 +243,23 @@ class JsCanvasView : TextureView, TextureView.SurfaceTextureListener { override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {} override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { - mDrawing = false - mDrawingThreadPool?.shutdown() + // Stop draw loop and block until current lockCanvas/unlockCanvasAndPost critical section finishes. + // zh-CN: 停止绘制循环, 并阻塞等待当前 lockCanvas/unlockCanvasAndPost 临界区结束. + synchronized(mSurfaceLock) { + mSurfaceAlive = false + mDrawing = false + } + + // Do NOT interrupt native drawing calls aggressively. + // zh-CN: 不要对 native 绘制调用进行激进的中断. + runCatching { mDrawFuture?.cancel(false) } + mDrawFuture = null + + // Shutdown executor gracefully. + // zh-CN: 平滑关闭执行器. + runCatching { mDrawingThreadPool?.shutdown() } + mDrawingThreadPool = null + Log.d(LOG_TAG, "onSurfaceTextureDestroyed: $this") return true } @@ -186,5 +275,4 @@ class JsCanvasView : TextureView, TextureView.SurfaceTextureListener { fun defaultMaxListeners(): Int = EventEmitter.defaultMaxListeners() } - } diff --git a/app/src/main/java/org/autojs/autojs/permission/VivoBackgroundPopupPermission.kt b/app/src/main/java/org/autojs/autojs/permission/VivoBackgroundPopupPermission.kt new file mode 100644 index 00000000..539fbc2b --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/permission/VivoBackgroundPopupPermission.kt @@ -0,0 +1,147 @@ +package org.autojs.autojs.permission + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.provider.Settings +import androidx.core.net.toUri +import org.autojs.autojs.ui.main.drawer.PermissionItemHelper +import org.autojs.autojs.util.RomUtils +import org.autojs.autojs.util.ViewUtils +import org.autojs.autojs6.R + +/** + * Created by SuperMonster003 on Jan 16, 2026. + */ +class VivoBackgroundPopupPermission( + override val context: Context, +) : PermissionItemHelper { + + override fun has(): Boolean { + if (!RomUtils.isVivo()) return true + return getVivoBgStartPermissionStatus(context) == STATE_ALLOWED + } + + override fun request(): Boolean = config() + + override fun revoke(): Boolean = config() + + /** + * Open OEM settings page for this permission. + * zh-CN: 打开此权限对应的厂商设置页面. + */ + fun config(): Boolean { + if (!RomUtils.isVivo()) { + return openAppDetails() + } + + val pkg = context.packageName + + val intents = listOf( + // Vivo permission detail page (commonly works on many models). + // zh-CN: Vivo 权限详情页(较多机型可用). + Intent() + .setClassName( + "com.vivo.permissionmanager", + "com.vivo.permissionmanager.activity.SoftPermissionDetailActivity", + ) + .setAction("secure.intent.action.softPermissionDetail") + .putExtra("packagename", pkg) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + + // Older models may use PurviewTabActivity. + // zh-CN: 老机型可能使用 PurviewTabActivity. + Intent() + .setClassName( + "com.vivo.permissionmanager", + "com.vivo.permissionmanager.activity.PurviewTabActivity", + ) + .putExtra("packagename", pkg) + .putExtra("tabId", "1") + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + + // Some ROMs expose a background start manager page. + // zh-CN: 部分 ROM 暴露了后台启动管理页. + Intent() + .setClassName( + "com.vivo.permissionmanager", + "com.vivo.permissionmanager.activity.BgStartUpManagerActivity", + ) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) + + return tryStartActivities(intents) || openAppDetails() + } + + private fun openAppDetails(): Boolean = Intent() + .setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData("package:${context.packageName}".toUri()) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .let { tryStartActivity(it) } + + /** + * Query vivo PermissionProvider "start_bg_activity" table. + * zh-CN: 查询 vivo 权限 Provider 的 "start_bg_activity" 表. + */ + @SuppressLint("Range") + private fun getVivoBgStartPermissionStatus(context: Context): Int { + val uri = "content://com.vivo.permissionmanager.provider.permission/start_bg_activity".toUri() + val selection = "pkgname = ?" + val selectionArgs = arrayOf(context.packageName) + var state = STATE_DENIED + + runCatching { + context.contentResolver.query(uri, null, selection, selectionArgs, null)?.use { c -> + if (c.moveToFirst()) { + val idx = c.getColumnIndex(COLUMN_CURRENT_STATE) + if (idx >= 0) { + state = c.getInt(idx) + } + } + } + }.onFailure { + it.printStackTrace() + // Keep default denied on query failure. + // zh-CN: 查询失败时保持默认无权限. + } + + return state + } + + // /** + // * Check whether this looks like a vivo/iQOO device. + // * zh-CN: 判断是否为 vivo/iQOO 设备(经验判断). + // */ + // private fun isVivoLikeDevice(): Boolean { + // val brand = (Build.BRAND ?: "").lowercase() + // val manufacturer = (Build.MANUFACTURER ?: "").lowercase() + // return manufacturer == "vivo" || brand == "vivo" || brand == "iqoo" + // } + + private fun tryStartActivities(intents: List): Boolean { + for (i in intents) { + if (tryStartActivity(i)) return true + } + return false + } + + private fun tryStartActivity(i: Intent): Boolean = runCatching { + val pm = context.packageManager + i.resolveActivity(pm) ?: return@runCatching false + context.startActivity(i) + true + }.onFailure { + it.printStackTrace() + ViewUtils.showToast(context, R.string.text_failed) + }.getOrDefault(false) + + private companion object { + + private const val STATE_ALLOWED = 0 + private const val STATE_DENIED = 1 + + private const val COLUMN_CURRENT_STATE = "currentstate" + + } + +} diff --git a/app/src/main/java/org/autojs/autojs/permission/XiaomiBackgroundPopupPermission.kt b/app/src/main/java/org/autojs/autojs/permission/XiaomiBackgroundPopupPermission.kt new file mode 100644 index 00000000..d04786ef --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/permission/XiaomiBackgroundPopupPermission.kt @@ -0,0 +1,132 @@ +package org.autojs.autojs.permission + +import android.app.AppOpsManager +import android.content.Context +import android.content.Intent +import android.os.Process +import android.provider.Settings +import androidx.core.net.toUri +import org.autojs.autojs.ui.main.drawer.PermissionItemHelper +import org.autojs.autojs.util.RomUtils +import org.autojs.autojs.util.ViewUtils +import org.autojs.autojs6.R + +/** + * Created by SuperMonster003 on Jan 16, 2026. + */ +class XiaomiBackgroundPopupPermission( + override val context: Context, +) : PermissionItemHelper { + + override fun has(): Boolean { + if (!RomUtils.isMiui()) return true + return isMiuiBgStartPermissionGranted(context) + } + + override fun request(): Boolean = config() + + override fun revoke(): Boolean = config() + + /** + * Open OEM settings page for this permission. + * zh-CN: 打开此权限对应的厂商设置页面. + */ + fun config(): Boolean { + if (!RomUtils.isMiui()) { + return openAppDetails() + } + + val pkg = context.packageName + + val intents = listOf( + // MIUI 8+ commonly uses this activity. + // zh-CN: MIUI 8+ 常见使用此 Activity. + Intent("miui.intent.action.APP_PERM_EDITOR") + .setClassName( + "com.miui.securitycenter", + "com.miui.permcenter.permissions.PermissionsEditorActivity", + ) + .putExtra("extra_pkgname", pkg) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + + // Some MIUI versions use AppPermissionsEditorActivity. + // zh-CN: 部分 MIUI 版本使用 AppPermissionsEditorActivity. + Intent("miui.intent.action.APP_PERM_EDITOR") + .setClassName( + "com.miui.securitycenter", + "com.miui.permcenter.permissions.AppPermissionsEditorActivity", + ) + .putExtra("extra_pkgname", pkg) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) + + return tryStartActivities(intents) || openAppDetails() + } + + private fun openAppDetails(): Boolean = Intent() + .setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData("package:${context.packageName}".toUri()) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .let { tryStartActivity(it) } + + /** + * Check MIUI "background start activity" app-op (commonly op=10021). + * zh-CN: 检查 MIUI "后台启动界面" 的 AppOps 状态(常见 op=10021). + */ + private fun isMiuiBgStartPermissionGranted(context: Context): Boolean { + val ops = context.getSystemService(Context.APP_OPS_SERVICE) as? AppOpsManager ?: return true + + return runCatching { + val op = OP_BACKGROUND_START_ACTIVITY + val method = ops.javaClass.getMethod( + "checkOpNoThrow", + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + String::class.java, + ) + val mode = method.invoke(ops, op, Process.myUid(), context.packageName) as? Int + mode == AppOpsManager.MODE_ALLOWED + }.getOrElse { + it.printStackTrace() + // Fail-open to avoid blocking non-MIUI or changed ROM implementations. + // zh-CN: 发生异常时放行, 避免 ROM 变更导致误判并阻塞功能. + true + } + } + + // /** + // * Check whether this looks like a Xiaomi/Redmi/POCO device. + // * zh-CN: 判断是否为 Xiaomi/Redmi/POCO 设备(经验判断). + // */ + // private fun isXiaomiLikeDevice(): Boolean { + // val brand = (Build.BRAND ?: "").lowercase() + // val manufacturer = (Build.MANUFACTURER ?: "").lowercase() + // return manufacturer == "xiaomi" || brand == "xiaomi" || brand == "redmi" || brand == "poco" + // } + + private fun tryStartActivities(intents: List): Boolean { + for (i in intents) { + if (tryStartActivity(i)) return true + } + return false + } + + private fun tryStartActivity(i: Intent): Boolean = runCatching { + val pm = context.packageManager + i.resolveActivity(pm) ?: return@runCatching false + context.startActivity(i) + true + }.onFailure { + it.printStackTrace() + ViewUtils.showToast(context, R.string.text_failed) + }.getOrDefault(false) + + private companion object { + + // MIUI app-op code for background start activity (commonly 10021). + // zh-CN: MIUI 后台启动界面常见的 AppOps 编号(通常为 10021). + private const val OP_BACKGROUND_START_ACTIVITY = 10021 + + } + +} diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt index bdacb54c..c9a63ed8 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt +++ b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt @@ -7,9 +7,9 @@ import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject import org.autojs.autojs.annotation.ScriptInterface +import org.autojs.autojs.core.pref.Pref import org.autojs.autojs.pluginclient.JsonSocket.HANDSHAKE_TIMEOUT import org.autojs.autojs.runtime.ScriptRuntime -import org.autojs.autojs.util.NetworkUtils.DEFAULT_IP_ADDRESS import org.autojs.autojs.util.ThreadUtils import java.io.File import java.io.IOException @@ -21,7 +21,7 @@ import java.util.concurrent.atomic.AtomicBoolean /** * Created by Stardust on May 11, 2017. * Modified by SuperMonster003 as of Jan 9, 2026. - * Transformed by SuperMonster003 on Jul 1, 2023. + * Transformed by SuperMonster003 on Jan 15, 2026. */ class DevPluginService(val context: Context) { @@ -52,7 +52,7 @@ class DevPluginService(val context: Context) { // zh-CN: 发布当前服务端连接数量. val serverConnectionCount = BehaviorSubject.createDefault(0) - val clientConnectionIpAddress = BehaviorSubject.createDefault(DEFAULT_IP_ADDRESS) + val clientConnectionIpAddress = BehaviorSubject.createDefault(Pref.getServerAddress()) @get:ScriptInterface @Volatile @@ -134,13 +134,9 @@ class DevPluginService(val context: Context) { } try { - var port = Port.PC_SERVER - var ip = host - val i = host.lastIndexOf(':') - if (i > 0 && i < host.length - 1) { - port = host.substring(i + 1).toInt() - ip = host.substring(0, i) - } + val endpoint = parseRemoteEndpoint(host, Port.PC_SERVER) + val ip = endpoint.host + val port = endpoint.port // Show connecting subtitle immediately. // zh-CN: 立即显示正在连接的 subtitle. @@ -193,6 +189,44 @@ class DevPluginService(val context: Context) { return Observable.empty() } + // Parse host[:port] supporting domain/IPv4/IPv6. + // zh-CN: 解析 host[:port], 支持 域名/IPv4/IPv6. + private fun parseRemoteEndpoint(input: String, defaultPort: Int): RemoteEndpoint { + val s = input.trim() + + // Bracketed IPv6: [addr]:port or [addr] + // zh-CN: 方括号 IPv6: [addr]:port 或 [addr]. + if (s.startsWith("[") && s.contains("]")) { + val end = s.indexOf(']') + val hostPart = s.substring(1, end) + val rest = s.substring(end + 1) + if (rest.startsWith(":") && rest.length > 1) { + val port = rest.substring(1).toInt() + return RemoteEndpoint(hostPart, port) + } + return RemoteEndpoint(hostPart, defaultPort) + } + + // For non-bracketed input: + // - If there is exactly one ':' -> treat as host:port (domain or IPv4) + // - If there are multiple ':' -> treat as raw IPv6 with no port + // zh-CN: + // - 若仅 1 个 ':' -> 视为 host:port (域名 或 IPv4) + // - 若多个 ':' -> 视为不带端口的 IPv6. + val colonCount = s.count { it == ':' } + if (colonCount == 1) { + val i = s.lastIndexOf(':') + val hostPart = s.substring(0, i) + val portPart = s.substring(i + 1) + val port = portPart.toInt() + return RemoteEndpoint(hostPart, port) + } + + return RemoteEndpoint(s, defaultPort) + } + + private data class RemoteEndpoint(val host: String, val port: Int) + @AnyThread fun enableLocalServer(): Observable { return Observable diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java index 1c5235d6..a893910a 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java +++ b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocket.java @@ -308,12 +308,26 @@ abstract public class JsonSocket extends Socket { // 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") - )) { + if (message != null && message.toLowerCase().contains("socket closed")) { return; } + + // Treat "Stream ended unexpectedly" as error unless user requested normal close. + // zh-CN: 除非用户主动正常关闭, 否则将 "Stream ended unexpectedly" 视为错误. + if (message != null && message.toLowerCase().contains("stream ended unexpectedly")) { + boolean isNormallyClosed = false; + try { + if (jsonSocket instanceof JsonSocketClient) { + isNormallyClosed = JsonSocketClient.Companion.isClientSocketNormallyClosed(); + } + } catch (Throwable ignored) { + /* Ignored. */ + } + if (isNormallyClosed) { + return; + } + } + onSocketError(e); } finally { if (jsonSocket instanceof JsonSocketClient) { diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt index f4022679..58124251 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt +++ b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt @@ -14,12 +14,12 @@ 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 import org.autojs.autojs6.R import java.io.IOException +import java.net.InetSocketAddress import java.net.Socket import java.net.SocketTimeoutException import java.util.concurrent.Executors @@ -59,10 +59,15 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, pri try { setStateConnecting() if (mSocket?.isConnected != true) { - mSocket = Socket(host, port) + // Use connect timeout to avoid long blocking (e.g. DNS/connect stall). + // zh-CN: 使用 connect 超时避免长时间阻塞 (例如 DNS/连接卡住). + mSocket = Socket().apply { + connect(InetSocketAddress(host, port), CONNECT_TIMEOUT) + } } } catch (e: IOException) { e.printStackTrace() + runCatching { onSocketError(e) } } } } @@ -96,7 +101,7 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, pri // Clear subtitle on close. // zh-CN: 关闭连接时清空 subtitle. - service.clientConnectionIpAddress.onNext(NetworkUtils.DEFAULT_IP_ADDRESS) + service.clientConnectionIpAddress.onNext("Socket closed") } override fun sayHello() { @@ -143,7 +148,7 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, pri // Mark as disconnected when server rejects handshake (e.g. version mismatch). // zh-CN: 当服务端拒绝握手 (例如版本不匹配) 时, 标记为已断开. - service.clientConnectionIpAddress.onNext(NetworkUtils.DEFAULT_IP_ADDRESS) + service.clientConnectionIpAddress.onNext("Disconnected") setStateDisconnected(IllegalStateException(errorMessage.asString)) try { @@ -171,7 +176,7 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, pri // Fallback: version check failed or invalid hello. // zh-CN: 兜底: 版本校验失败或 hello 异常. - service.clientConnectionIpAddress.onNext(NetworkUtils.DEFAULT_IP_ADDRESS) + service.clientConnectionIpAddress.onNext("Handshake rejected") setStateDisconnected(IllegalStateException("Handshake rejected")) try { @@ -182,10 +187,10 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, pri val msg = """ ${ctx.getString(R.string.text_vsc_ext_version_not_meet_requirement)}. - + ${ctx.getString(R.string.text_min_version)}: $requiredVersion ${ctx.getString(R.string.text_current_version)}: ${currentVersion ?: "${ctx.getString(R.string.text_lower_than)} $requiredVersion"} - + ${ctx.getString(R.string.text_repo_url_of_vscode_vsc_ext)}: ${ctx.getString(R.string.url_github_autojs6_vscode_extension_repo)} """.trimIndent() @@ -312,6 +317,10 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, pri private val TAG = JsonSocketClient::class.java.simpleName + // Connect timeout = handshake timeout + 5 seconds (default). + // zh-CN: 连接超时 = 握手超时 + 5 秒 (默认). + private const val CONNECT_TIMEOUT = HANDSHAKE_TIMEOUT + 5_000 + var serverAddressHistories: LinkedHashSet get() = Pref.getLinkedHashSet(R.string.key_pc_server_address_histories) private set(value) = Pref.putLinkedHashSet(R.string.key_pc_server_address_histories, value) diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt index ee6f97ed..3e26ba96 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt @@ -28,8 +28,10 @@ import org.autojs.autojs.permission.MediaProjectionPermission import org.autojs.autojs.permission.PostNotificationsPermission import org.autojs.autojs.permission.ShizukuPermission import org.autojs.autojs.permission.UsageStatsPermission +import org.autojs.autojs.permission.VivoBackgroundPopupPermission import org.autojs.autojs.permission.WriteSecureSettingsPermission import org.autojs.autojs.permission.WriteSystemSettingsPermission +import org.autojs.autojs.permission.XiaomiBackgroundPopupPermission import org.autojs.autojs.pluginclient.DevPluginService import org.autojs.autojs.pluginclient.JsonSocketClient import org.autojs.autojs.pluginclient.JsonSocketServer @@ -50,6 +52,7 @@ import org.autojs.autojs.util.IntentUtils.App.exit import org.autojs.autojs.util.IntentUtils.App.restart import org.autojs.autojs.util.NetworkUtils import org.autojs.autojs.util.NotificationUtils +import org.autojs.autojs.util.RomUtils import org.autojs.autojs.util.ViewUtils import org.autojs.autojs.util.ViewUtils.MODE import org.autojs.autojs6.BuildConfig @@ -104,6 +107,8 @@ open class DrawerFragment : Fragment() { private lateinit var mUsageStatsPermissionItem: DrawerMenuToggleableItem private lateinit var mIgnoreBatteryOptimizationsItem: DrawerMenuToggleableItem private lateinit var mDisplayOverOtherAppsItem: DrawerMenuToggleableItem + private lateinit var mXiaomiBackgroundPopupPermissionItem: DrawerMenuToggleableItem + private lateinit var mVivoBackgroundPopupPermissionItem: DrawerMenuToggleableItem private lateinit var mWriteSystemSettingsItem: DrawerMenuToggleableItem private lateinit var mWriteSecuritySettingsItem: DrawerMenuToggleableItem private lateinit var mProjectMediaAccessItem: DrawerMenuToggleableItem @@ -158,15 +163,15 @@ open class DrawerFragment : Fragment() { title = R.string.text_a11y_service, descriptionRes = R.string.description_accessibility_service, prefKey = R.string.key_a11y_service, - ) { - it.setOnLaunchManagerListener { d -> + ).also { item -> + item.setOnLaunchManagerListener { d -> if (d != null) { ViewUtils.showSnack(d.view, R.string.text_under_development, 1_200) } else { ViewUtils.showToast(mContext, R.string.text_under_development) } } - it.setOnLaunchSettingsListener { + item.setOnLaunchSettingsListener { Intent().apply { action = Settings.ACTION_ACCESSIBILITY_SETTINGS addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -180,10 +185,8 @@ open class DrawerFragment : Fragment() { title = R.string.text_foreground_service, descriptionRes = R.string.description_foreground_service, prefKey = R.string.key_foreground_service, - ) { - it.setOnLaunchSettingsListener { - NotificationUtils.launchChannelSettings(mContext, MainActivityForegroundService::class.java) - } + ).setOnLaunchSettingsListener { + NotificationUtils.launchChannelSettings(mContext, MainActivityForegroundService::class.java) } mFloatingButtonItem = DrawerMenuToggleableItem( @@ -212,7 +215,7 @@ open class DrawerFragment : Fragment() { title = R.string.text_floating_button, descriptionRes = R.string.description_floating_button, prefKey = R.string.key_floating_menu_shown, - ) { item -> + ).also { item -> item.setOnLaunchSettingsListener { val helper = item.getHelper() as DisplayOverOtherAppsPermission helper.config() @@ -227,7 +230,11 @@ open class DrawerFragment : Fragment() { icon = R.drawable.ic_computer_black_48dp, title = R.string.text_client_mode, descriptionRes = R.string.description_client_mode, - ).also { mClientModeItem = it } + ) { helper -> + // If connecting, show status dialog so user can interrupt. + // zh-CN: 若正在连接, 显示状态对话框以便用户中止连接. + (helper as? JsonSocketClientTool)?.showConnectingStatusDialogIfConnecting() == true + }.also { mClientModeItem = it } val disposable = Observable .combineLatest( @@ -244,20 +251,20 @@ open class DrawerFragment : Fragment() { } drawerItem.setCheckedIfNeeded(state.isConnected()) drawerItem.isProgress = state.isConnecting() - state.exception?.let { e -> + state.exception?.let { drawerItem.subtitle = null - ViewUtils.showToast(mContext, e.message) } } setStateDisposable(disposable) - setOnConnectionException { e: Throwable -> + setOnConnectionException { _: Throwable -> drawerItem.setCheckedIfNeeded(false) - ViewUtils.showToast(context, getString(R.string.error_connect_to_remote, e.message), true) } + setOnConnectionDialogDismissed { drawerItem.setCheckedIfNeeded(false) } + connectIfNotNormallyClosed() } @@ -269,15 +276,16 @@ open class DrawerFragment : Fragment() { icon = R.drawable.ic_smartphone_black_48dp, title = R.string.text_server_mode, descriptionRes = R.string.description_server_mode, - ) { - it.setOnLaunchManagerListener { d -> + ).also { item -> + item.setOnLaunchManagerListener { d -> if (d != null) { ViewUtils.showSnack(d.view, R.string.text_under_development, 1_200) } else { ViewUtils.showToast(mContext, R.string.text_under_development) } } - }.also { mServerModeItem = it } + mServerModeItem = item + } val disposable = Observable .combineLatest( @@ -317,7 +325,7 @@ open class DrawerFragment : Fragment() { icon = R.drawable.ic_ali_notification, title = R.string.text_post_notifications_permission, descriptionRes = R.string.description_post_notifications, - ) { item -> + ).also { item -> item.setOnLaunchSettingsListener { val helper = item.getHelper() as PostNotificationsPermission helper.config() @@ -329,7 +337,7 @@ open class DrawerFragment : Fragment() { icon = R.drawable.ic_ali_notification, title = R.string.text_notification_access_permission, descriptionRes = R.string.description_notification_access, - ) { item -> + ).also { item -> item.setOnLaunchSettingsListener { val helper = item.getHelper() as NotificationService helper.config() @@ -341,7 +349,7 @@ open class DrawerFragment : Fragment() { icon = R.drawable.ic_database_black_48dp, title = R.string.text_all_files_access, descriptionRes = R.string.description_all_files_access, - ) { item -> + ).also { item -> item.setOnLaunchSettingsListener { val helper = item.getHelper() as AllFilesAccessPermission helper.config() @@ -353,7 +361,7 @@ open class DrawerFragment : Fragment() { icon = R.drawable.ic_assessment_black_48dp, title = R.string.text_usage_stats_permission, descriptionRes = R.string.description_usage_stats_access, - ) { item -> + ).also { item -> item.setOnLaunchSettingsListener { val helper = item.getHelper() as UsageStatsPermission helper.config() @@ -365,7 +373,7 @@ open class DrawerFragment : Fragment() { icon = R.drawable.ic_battery_std_black_48dp, title = R.string.text_ignore_battery_optimizations, descriptionRes = R.string.description_ignore_battery_optimizations, - ) { item -> + ).also { item -> item.setOnLaunchSettingsListener { Intent().apply { action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS @@ -379,19 +387,45 @@ open class DrawerFragment : Fragment() { icon = R.drawable.ic_layers_black_48dp, title = R.string.text_display_over_other_app, descriptionRes = R.string.description_display_over_other_app, - ) { item -> + ).also { item -> item.setOnLaunchSettingsListener { val helper = item.getHelper() as DisplayOverOtherAppsPermission helper.config() } } + mXiaomiBackgroundPopupPermissionItem = DrawerMenuToggleableItem( + helper = XiaomiBackgroundPopupPermission(mContext), + icon = R.drawable.ic_layers_black_48dp, + title = R.string.text_xiaomi_background_popup_permission, + descriptionRes = R.string.description_background_popup_permission, + ).also { item -> + item.setOnLaunchSettingsListener { + val helper = item.getHelper() as XiaomiBackgroundPopupPermission + helper.config() + } + item.isHidden = !RomUtils.isMiui() + } + + mVivoBackgroundPopupPermissionItem = DrawerMenuToggleableItem( + helper = VivoBackgroundPopupPermission(mContext), + icon = R.drawable.ic_layers_black_48dp, + title = R.string.text_vivo_background_popup_permission, + descriptionRes = R.string.description_background_popup_permission, + ).also { item -> + item.setOnLaunchSettingsListener { + val helper = item.getHelper() as VivoBackgroundPopupPermission + helper.config() + } + item.isHidden = !RomUtils.isVivo() + } + mWriteSystemSettingsItem = DrawerMenuToggleableItem( helper = WriteSystemSettingsPermission(mContext), icon = R.drawable.ic_settings_black_48dp, title = R.string.text_write_system_settings, descriptionRes = R.string.description_write_system_settings, - ) { item -> + ).also { item -> item.setOnLaunchSettingsListener { val helper = item.getHelper() as WriteSystemSettingsPermission helper.config() @@ -467,14 +501,14 @@ open class DrawerFragment : Fragment() { title = R.string.text_auto_night_mode, descriptionRes = R.string.description_auto_night_mode, prefKey = R.string.key_auto_night_mode_enabled, - ) { item -> - item.isHidden = !ViewUtils.AutoNightMode.isFunctional() + ).also { item -> item.setOnLaunchSettingsListener { Intent().apply { action = Settings.ACTION_DISPLAY_SETTINGS addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }.let { i -> mContext.startActivity(i) } } + item.isHidden = !ViewUtils.AutoNightMode.isFunctional() } mNightModeItem = DrawerMenuToggleableItem( @@ -504,13 +538,11 @@ open class DrawerFragment : Fragment() { title = R.string.text_night_mode, descriptionRes = R.string.description_night_mode, prefKey = R.string.key_night_mode_enabled, - ) { item -> - item.setOnLaunchSettingsListener { - Intent().apply { - action = Settings.ACTION_DISPLAY_SETTINGS - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }.let { i -> mContext.startActivity(i) } - } + ).setOnLaunchSettingsListener { + Intent().apply { + action = Settings.ACTION_DISPLAY_SETTINGS + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }.let { i -> mContext.startActivity(i) } } mKeepScreenOnWhenInForegroundItem = DrawerMenuToggleableItem( @@ -658,6 +690,8 @@ open class DrawerFragment : Fragment() { mUsageStatsPermissionItem, mIgnoreBatteryOptimizationsItem, mDisplayOverOtherAppsItem, + mXiaomiBackgroundPopupPermissionItem, + mVivoBackgroundPopupPermissionItem, mWriteSystemSettingsItem, mWriteSecuritySettingsItem, mProjectMediaAccessItem, @@ -704,6 +738,8 @@ open class DrawerFragment : Fragment() { mUsageStatsPermissionItem, mIgnoreBatteryOptimizationsItem, mDisplayOverOtherAppsItem, + mXiaomiBackgroundPopupPermissionItem, + mVivoBackgroundPopupPermissionItem, mWriteSystemSettingsItem, mWriteSecuritySettingsItem, mProjectMediaAccessItem, diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuDisposableItem.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuDisposableItem.kt index 8c3e96fe..137255fd 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuDisposableItem.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuDisposableItem.kt @@ -2,40 +2,25 @@ package org.autojs.autojs.ui.main.drawer import com.afollestad.materialdialogs.MaterialDialog -class DrawerMenuDisposableItem : DrawerMenuToggleableItem { - - private val helper: SocketItemHelper - - constructor( - helper: SocketItemHelper, - icon: Int, - title: Int, - onTitleContainerClickListener: (MaterialDialog.Builder.(menuItem: DrawerMenuToggleableItem) -> Unit)? = null, - ) : super( - helper, - icon, - title, - onTitleContainerClickListener = onTitleContainerClickListener, - ) { - this.helper = helper - } - - constructor( - helper: SocketItemHelper, - icon: Int, - title: Int, - descriptionRes: Int, - onTitleContainerClickListener: (MaterialDialog.Builder.(menuItem: DrawerMenuToggleableItem) -> Unit)? = null, - ) : super( - helper, - icon, - title, - descriptionRes, - onTitleContainerClickListener = onTitleContainerClickListener, - ) { - this.helper = helper - } - +class DrawerMenuDisposableItem( + private val helper: SocketItemHelper, + icon: Int, + title: Int, + descriptionRes: Int, + onTitleContainerClickListener: (MaterialDialog.Builder.(helper: SocketItemHelper) -> Any?)? = null, +) : DrawerMenuToggleableItem( + helper, + icon, + title, + descriptionRes, + onTitleContainerClickListener = listener@{ + val listener = onTitleContainerClickListener ?: return@listener false + when (val result = listener(this, helper)) { + is Boolean -> result + is Unit -> false + else -> throw IllegalArgumentException("onTitleContainerClickListener must return Boolean or Unit") + } + }, +) { fun dispose() = helper.dispose() - } \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuToggleableItem.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuToggleableItem.kt index a22ebc16..3774a384 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuToggleableItem.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuToggleableItem.kt @@ -19,7 +19,7 @@ open class DrawerMenuToggleableItem : DrawerMenuItem, IToggleableItem { private var mDescription: String? = null private var mDescriptionDialogBuilderProvider: (() -> MaterialDialog.Builder)? = null - private var mOnTitleContainerClickListener: (MaterialDialog.Builder.(menuItem: DrawerMenuToggleableItem) -> Unit)? = null + private var mOnTitleContainerClickListener: (MaterialDialog.Builder.() -> Boolean)? = null private var mOnLauncherManagerListener: ((MaterialDialog?) -> Unit)? = null private var mOnLaunchSettingsListener: ((MaterialDialog) -> Unit)? = null @@ -31,7 +31,7 @@ open class DrawerMenuToggleableItem : DrawerMenuItem, IToggleableItem { icon: Int, title: Int, descriptionRes: Int? = null, - onTitleContainerClickListener: (MaterialDialog.Builder.(menuItem: DrawerMenuToggleableItem) -> Unit)? = null, + onTitleContainerClickListener: (MaterialDialog.Builder.() -> Any?)? = null, ) : super(icon, title, DEFAULT_PREFERENCE_KEY) { init(helper, null, descriptionRes, onTitleContainerClickListener) } @@ -42,7 +42,7 @@ open class DrawerMenuToggleableItem : DrawerMenuItem, IToggleableItem { title: Int, switchOnHintRes: Int? = null, descriptionRes: Int? = null, - onTitleContainerClickListener: (MaterialDialog.Builder.(menuItem: DrawerMenuToggleableItem) -> Unit)? = null, + onTitleContainerClickListener: (MaterialDialog.Builder.() -> Any?)? = null, ) : super(icon, title, DEFAULT_PREFERENCE_KEY) { init(helper, switchOnHintRes, descriptionRes, onTitleContainerClickListener) } @@ -54,7 +54,7 @@ open class DrawerMenuToggleableItem : DrawerMenuItem, IToggleableItem { switchOnHintRes: Int? = null, prefKey: Int = DEFAULT_PREFERENCE_KEY, descriptionRes: Int? = null, - onTitleContainerClickListener: (MaterialDialog.Builder.(menuItem: DrawerMenuToggleableItem) -> Unit)? = null, + onTitleContainerClickListener: (MaterialDialog.Builder.() -> Any?)? = null, ) : super(icon, title, prefKey) { init(helper, switchOnHintRes, descriptionRes, onTitleContainerClickListener) } @@ -63,7 +63,7 @@ open class DrawerMenuToggleableItem : DrawerMenuItem, IToggleableItem { itemHelper: DrawerMenuItemHelper, switchOnHintRes: Int?, descriptionRes: Int?, - onTitleContainerClickListener: (MaterialDialog.Builder.(menuItem: DrawerMenuToggleableItem) -> Unit)? = null, + onTitleContainerClickListener: (MaterialDialog.Builder.() -> Any?)? = null, ) { mItemHelper = itemHelper switchOnHintRes?.let { content = itemHelper.context.getString(it) } @@ -110,7 +110,14 @@ open class DrawerMenuToggleableItem : DrawerMenuItem, IToggleableItem { } } - mOnTitleContainerClickListener = onTitleContainerClickListener + mOnTitleContainerClickListener = listener@{ + val listener = onTitleContainerClickListener ?: return@listener false + when (val result = listener(this)) { + is Boolean -> result + is Unit -> false + else -> throw IllegalArgumentException("onTitleContainerClickListener must return Boolean or Unit") + } + } } fun setOnNotifyItemChangedListener(listener: ((DrawerMenuItem) -> Unit)?) { @@ -129,7 +136,10 @@ open class DrawerMenuToggleableItem : DrawerMenuItem, IToggleableItem { fun onTitleContainerClick() { if (isContextInvalidForDialog()) return val builder = mDescriptionDialogBuilderProvider?.invoke() ?: return - mOnTitleContainerClickListener?.invoke(builder, this) + + val handled = mOnTitleContainerClickListener?.invoke(builder) == true + if (handled) return + runCatching { builder.show() } } @@ -141,11 +151,11 @@ open class DrawerMenuToggleableItem : DrawerMenuItem, IToggleableItem { }.getOrDefault(false) } - fun setOnLaunchManagerListener(onClickListener: (MaterialDialog?) -> Unit) { + fun setOnLaunchManagerListener(onClickListener: (MaterialDialog?) -> Unit) = also { mOnLauncherManagerListener = onClickListener } - fun setOnLaunchSettingsListener(onClickListener: (MaterialDialog) -> Unit) { + fun setOnLaunchSettingsListener(onClickListener: (MaterialDialog) -> Unit) = also { mOnLaunchSettingsListener = onClickListener } diff --git a/app/src/main/java/org/autojs/autojs/util/RomUtils.kt b/app/src/main/java/org/autojs/autojs/util/RomUtils.kt index 11c60357..e8da449c 100644 --- a/app/src/main/java/org/autojs/autojs/util/RomUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/RomUtils.kt @@ -4,10 +4,11 @@ package org.autojs.autojs.util import android.app.AppOpsManager import android.content.Context -import android.net.Uri import android.os.Build import android.os.Process import android.provider.Settings +import androidx.core.net.toUri +import ezy.assist.compat.RomUtil import org.autojs.autojs.app.GlobalAppContext import org.autojs.autojs.core.pref.Language import java.io.BufferedReader @@ -55,7 +56,7 @@ object RomUtils { * 判断 Vivo 后台弹出界面状态. 1: 无权限; 0: 有权限. */ private fun getVivoBgStartPermissionStatus(context: Context): Int { - val uri = Uri.parse("content://com.vivo.permissionmanager.provider.permission/start_bg_activity") + val uri = "content://com.vivo.permissionmanager.provider.permission/start_bg_activity".toUri() val selection = "pkgname = ?" val selectionArgs = arrayOf(context.packageName) var state = 1 @@ -95,6 +96,8 @@ object RomUtils { fun isFlyme() = Build.DISPLAY.lowercase(Language.getPrefLanguage().locale).contains("flyme") + fun isQiku() = RomUtil.isQiku() + private fun getSystemProperty(propName: String): String = try { BufferedReader( InputStreamReader( diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 09342b2d..86aabba7 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -74,6 +74,7 @@ يسمح إذن \"إدارة جميع الملفات\" (أو \"الوصول إلى جميع الملفات\") لـ AutoJs6 بـ [ إنشاء / قراءة / تعديل / حذف ] الملفات مباشرةً عبر مسارات الملفات العادية ضمن مساحة التخزين المشتركة، مما يتيح للسكربتات الوصول إلى \"Internal Storage\" ويُمكّن مدير الملفات من عرض الملفات وإدارتها بشكل صحيح.\n\nعلى أجهزة Android 11+، تُعد هذه الطريقة الأساسية لتحقيق قدرة القراءة والكتابة على كامل التخزين. هذا التفضيل لتغيير لغة العرض لـ AutoJs6 ، بما في ذلك رسائل الاستثناء من تشغيل البرامج النصية.\n\nملاحظة: قد تكون هناك حاجة إلى إعادة تشغيل التطبيق لجعل اللغة تطبيقها كما هو متوقع. عند تفعيل الوضع الليلي التلقائي، سيقوم AutoJs6 بالتبديل تلقائيًا إلى الوضع الليلي وفقًا لإعدادات النظام.\n\nNote: مفتاح الوضع الليلي التلقائي ومفتاح الوضع الليلي مرتبطان ويؤثر كل منهما في الآخر. + يتيح إذن \"النوافذ المنبثقة في الخلفية\" (ويُسمّى أيضًا \"تشغيل الواجهة من الخلفية\" أو \"إظهار الواجهة في الخلفية\") لـ AutoJs6، حتى عندما يكون التطبيق في الخلفية أو دون واجهة مرئية، تشغيل Activity أو فتح صفحة إعدادات محددة. وهو مناسب لسيناريوهات مثل [ فتح واجهة المستخدم عند تشغيل مهمة مجدولة / استئناف التفاعل بعد قفل الشاشة أو وضع السكون / فتح صفحة إعدادات السكربت عبر إشعار أو اختصار ].\n\nNote: على أنظمة مثل Xiaomi (MIUI/HyperOS) و Vivo (OriginOS/Funtouch OS)، قد يكون هذا الإذن مُعطّلًا افتراضيًا. عند عدم منحه، قد يمنع النظام محاولات السكربت لفتح صفحات من الخلفية، وقد يظهر ذلك على شكل [ عدم استجابة / تنفيذ في الخلفية دون إظهار واجهة / فشل الانتقال ].\n\nحتى بعد منحه، قد تظل هناك سياسات أخرى للنظام تؤثر عليه مثل [ تحسين البطارية / قيود التشغيل التلقائي / تجميد الخلفية / وضع الاستعداد للتطبيق ]. يُنصح بضبطه مع الأذونات ذات الصلة أو إعدادات القائمة البيضاء. قم بتغيير مسار الدليل الذي يحتوي على البرامج النصية يحصل AutoJs6 على التحديثات وتنزيلها من GitHub. يُستخدم وضع العميل لتمكين AutoJs6 من الاتصال بشكل نشط بخادم بعيد لتنفيذ [ نقل السكربتات / طباعة السجلات / التحكم عن بُعد ].\n\nعادةً ما يلزم أن يكون الجهاز والخادم ضمن نفس الشبكة المحلية (LAN) أو ضمن بيئة شبكة يمكنهما الوصول فيها إلى بعضهما البعض. @@ -111,7 +112,9 @@ عرض سجل إصدارات AutoJs6 على GitHub وإحصاءات الفئات الرئيسية. إعدادات النظام الآمنة ، التي تحتوي على تفضيلات النظام التي يمكن أن تقرأها التطبيقات ولكن لا يُسمح لها بالكتابة.\nهذه هي لتفضيلات يجب على المستخدم تعديلها بشكل صريح من خلال واجهة المستخدم لتطبيق النظام.\nمع إذن إعدادات النظام الآمن ، يمكن للتطبيقات العادية تعديل الإعدادات الآمنة مباشرة (مثل خدمة إمكانية الوصول). يسمح إذن \"تعديل إعدادات النظام\" لـ AutoJs6 بتعديل بعض إعدادات النظام، مما يتيح للسكربتات تعديل إعدادات مثل [ سطوع الشاشة / التدوير التلقائي / مهلة إيقاف الشاشة ]. + تخلَّ متقدم + تصحيح العنوان @string/text_back يلغي يلغي @@ -119,7 +122,6 @@ نعم اتصال @string/text_continue - @string/dialog_button_continue @string/text_copy بادئة افتراضية تفاصيل @@ -129,12 +131,12 @@ معلومات الملف تاريخ يتجاهل + إيقاف الاتصال إنضم للمجموعة المدير أكثر فتح لوحة الألوان يترك - @string/dialog_button_quit إزالة جلب إعادة المحاولة @@ -214,6 +216,8 @@ يجب أن يتبع القولون عنوان IP صالحًا لا يمكن أن تكون البوصلة لاغية لا يمكن الاتصال بالخادم البعيد: %s + فشل الاتصال بـ \"%1$s\". + انتهت مهلة الاتصال (%d مللي ثانية) يجب استدعاء الطريقة السيرة الذاتية() بعد تعليق() يجب استدعاء طريقة تعليق() مرة واحدة فقط قد لم يتم نشر إصدار GitHub المقابل بعد @@ -267,6 +271,7 @@ بوصلة راحة غير صالحة: %s عنصر خريطة محدد غير صالح: {%s: %s (%s)} لا يمكن أن يكون عنوان IP فارغًا + يجب تحديد منفذ IPv6 بصيغة \"[ipv6]:port\". تم تجاوز مستمعي أقصى: %s قد لا يكون لدى AutoJs6 ملف الجذر لتشغيل ملف \"auto\" %s() دعا مع الوسيطة الفارغة: %s @@ -339,6 +344,7 @@ انقر الطويل على زر \"تشغيل\" لتصحيح الأخطاء تأخير قبل الحلقة 0 للحلقة اللانهائية + يتم دعم IPv4 و IPv6 وأسماء النطاقات. أدخل عنوان URL يشير إلى ملحق (Plugin) بعيد.\nمثال: \"https://example.com/plugin.apk\". آخر استخدام: %1$s فشل خيط \"blob"\ @@ -563,7 +569,10 @@ الاتصال بالكمبيوتر متصل متصل: %1$d + جارٍ الاتصال + جارٍ الاتصال بـ \"%1$s\"... لا يمكن إنشاء الاتصال + فشل الاتصال وحدة التحكم يكمل ينسخ @@ -1156,6 +1165,7 @@ اسم الإصدار (جارٍ الحساب...) عرض عرض المستندات + النوافذ المنبثقة في الخلفية إصدار امتداد VSCode لا يفي بالمتطلبات بانتظار اكتمال معالجة جميع البيانات... مهمة أسبوعية @@ -1163,5 +1173,6 @@ مسار دليل العمل اكتب إعدادات الأمان كتابة إعدادات النظام + النوافذ المنبثقة في الخلفية diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 88b521af..ac7eb713 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -69,6 +69,7 @@ The \"manage all files\" (or \"all files access\") permission allows AutoJs6 to directly [ create / read / modify / delete ] files in shared storage via normal file paths, allowing scripts to access \"Internal Storage\" and enabling the file manager to display and manage files properly.\n\nOn Android 11+ devices, this is the primary way to achieve full file read/write access. Preference for changing the display language of AutoJs6, including exception messages from running scripts.\n\nNote: an app restart may be needed to make language applied as expected. When auto night mode is enabled, AutoJs6 will automatically switch night mode based on system settings.\n\nNote: the auto night mode toggle and the night mode toggle are linked and affect each other. + The \"background pop-up\" permission (also known as \"background activity launch\" or \"launch UI from background\") allows AutoJs6 to proactively start an Activity or open specific settings pages even when the app is in the background or no UI is currently visible. It is useful for scenarios such as [ opening UI when a scheduled task is triggered / resuming interaction after the device is locked or idle / opening a script configuration page from a notification or shortcut ].\n\nNote: on systems like Xiaomi (MIUI/HyperOS) and Vivo (OriginOS/Funtouch OS), this permission may be disabled by default. Without it, attempts to open pages from the background may be blocked by the system, which can appear as [ no response / running in background only without showing UI / navigation failed ].\n\nEven after granting it, the behavior may still be affected by other system policies such as [ battery optimizations / auto-start restrictions / background freezing / app standby ]. Consider adjusting it together with related permissions or whitelist settings. Change the path of the directory containing scripts AutoJs6 gets and downloads updates from GitHub. Client mode allows AutoJs6 to actively connect to a remote server for [ script transfer / log printing / remote control ].\n\nTypically, the device and the server need to be on the same LAN or on a network where they can reach each other. @@ -106,7 +107,9 @@ View the release version history and key category statistics of AutoJs6 on GitHub. Secure system settings, containing system preferences that applications can read but are not allowed to write.\nThese are for preferences that the user must explicitly modify through the UI of a system app.\nWith secure system settings permission, normal applications can directly modify the secure settings (such as accessibility service). The \"write system settings\" permission allows AutoJs6 to modify some system settings, so scripts can change system parameters such as [ screen brightness / auto-rotate / screen timeout ]. + Abandon Advanced + Amend @string/text_back Cancel Cancel @@ -114,7 +117,6 @@ OK Connect @string/text_continue - @string/dialog_button_continue @string/text_copy Def prefix Details @@ -124,12 +126,12 @@ File info History Ignore + Interrupt Join group Manager More Palette Quit - @string/dialog_button_quit Remove Retrieve Retry @@ -209,6 +211,8 @@ Colon must follow a valid IP address Compass cannot be null Cannot connect to the remote server: %s + Failed to connect to \"%1$s\". + Connection timed out after %d ms Method resume() should be called after suspend() Method suspend() should be only called once Corresponding GitHub release may have not been published yet @@ -262,6 +266,7 @@ Invalid rest compass: %s Invalid selector map element: { %s: %s (%s) } IP address can not be empty + IPv6 port must be specified as \"[ipv6]:port\". Max listeners exceeded: %d AutoJs6 may not have root access to run \"auto\" file %s() called with null argument: %s @@ -334,6 +339,7 @@ Long click \"Run\" button to debug Delay before loop 0 for infinite loop + IPv4, IPv6 and domain are supported. Enter a URL pointing to a remote plugin address.\nFor example \"https://example.com/plugin.apk\". Latest used: %1$s \"Blob\" thread request failed @@ -558,7 +564,10 @@ Connect to PC Connected Connected: %1$d + Connecting + Connecting to \"%1$s\"... Connection cannot be established + Connection failed Console Continue Copy @@ -1151,6 +1160,7 @@ Version name (computing...) View View documents + Background pop-ups The version of the VSCode extension does not meet the requirements Waiting for all data processing to complete... Weekly task @@ -1158,5 +1168,6 @@ Working directory path Write security settings Write system settings + Display pop-up windows while running in the background diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 77f5bedc..185117ae 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -72,6 +72,7 @@ El permiso \"administrar todos los archivos\" (o \"acceso a todos los archivos\") permite que AutoJs6 [ cree / lea / modifique / elimine ] archivos directamente mediante rutas de archivo normales en el almacenamiento compartido, de modo que los scripts puedan acceder al \"Internal Storage\" y el explorador de archivos pueda mostrar y administrar archivos correctamente.\n\nEn dispositivos con Android 11+, esta es la forma principal de lograr acceso de lectura y escritura a todo el almacenamiento. Esta preferencia para cambiar el idioma de visualización de AutoJs6, incluyendo los mensajes de excepción de los scripts en ejecución.\n\nNota: puede ser necesario reiniciar la aplicación para que el idioma se aplique como se espera. Cuando el modo nocturno automático está activado, AutoJs6 cambiará automáticamente al modo nocturno según la configuración del sistema.\n\nNote: el interruptor de modo nocturno automático y el interruptor de modo nocturno están vinculados y se afectan mutuamente. + El permiso \"ventanas emergentes en segundo plano\" (también llamado \"iniciar interfaz desde segundo plano\" o \"mostrar interfaz en segundo plano\") permite que AutoJs6, incluso cuando la app está en segundo plano o sin interfaz visible, pueda iniciar una Activity o abrir una página de ajustes específica. Es útil para escenarios como [ abrir la UI al activarse una tarea programada / reanudar la interacción tras el bloqueo o la suspensión / abrir la página de configuración de scripts desde una notificación o un acceso directo ].\n\nNote: en sistemas como Xiaomi (MIUI/HyperOS) y Vivo (OriginOS/Funtouch OS), este permiso puede estar desactivado por defecto. Si no se concede, el sistema puede bloquear que un script abra páginas desde segundo plano, lo que puede manifestarse como [ sin respuesta / ejecución en segundo plano sin mostrar interfaz / fallo al navegar ].\n\nIncluso después de concederlo, puede seguir viéndose afectado por otras políticas del sistema, como [ optimización de batería / restricciones de inicio automático / congelación en segundo plano / modo de espera de la aplicación ]. Se recomienda ajustarlo junto con permisos relacionados o listas blancas. Cambiar la ruta del directorio que contiene los scripts AutoJs6 obtiene y descarga las actualizaciones de GitHub. El modo cliente permite que AutoJs6 se conecte activamente a un servidor remoto para realizar [ transferencia de scripts / impresión de registros / control remoto ].\n\nPor lo general, el dispositivo y el servidor deben estar en la misma red local (LAN) o en una red accesible entre sí. @@ -109,7 +110,9 @@ Ver el historial de versiones publicadas de AutoJs6 en GitHub y las estadísticas de las categorías importantes. Ajustes de seguridad del sistema, que contienen preferencias del sistema que las aplicaciones pueden leer pero no pueden escribir.\nSe trata de preferencias que el usuario debe modificar explícitamente a través de la interfaz de usuario de una aplicación del sistema.\nCon el permiso de configuración segura del sistema, las aplicaciones normales pueden modificar directamente la configuración segura (como el servicio de accesibilidad). El permiso \"modificar ajustes del sistema\" permite que AutoJs6 cambie algunos ajustes del sistema, de modo que los scripts puedan modificar parámetros como [ brillo de pantalla / rotación automática / tiempo de espera de pantalla ]. + Abandonar Avanzado + Corregir dirección @string/text_back Cancelar Cancelar @@ -117,7 +120,6 @@ OK Conectar @string/text_continue - @string/dialog_button_continue @string/text_copy Prefijo def Detalles @@ -127,12 +129,12 @@ Información del archivo Historial Ignorar + Interrumpir conexión Unirse grupo Administrador Más Abrir paleta Salir - @string/dialog_button_quit Eliminar Obtener Reintentar @@ -212,6 +214,8 @@ Dos puntos deben seguir a una dirección IP válida La brújula no puede ser nula No se puede conectar con el servidor remoto: %s + No se pudo conectar a \"%1$s\". + La conexión agotó el tiempo de espera (%d ms) El método resume() debe ser llamado después de suspender() El método suspender() debería llamarse sólo una vez Es posible que la versión correspondiente de GitHub no se haya publicado todavía @@ -265,6 +269,7 @@ Compás de reposo inválido: %s Elemento de mapa selector no válido: { %s: %s (%s) } La dirección IP no puede estar vacía + El puerto IPv6 debe especificarse con el formato \"[ipv6]:port\". Max listeners exceeded: %d AutoJs6 puede no tener acceso a la raíz para ejecutar el archivo \"auto %s() llamada con argumento nulo: %s @@ -337,6 +342,7 @@ Haga un clic largo en el botón \"Ejecutar\" para depurar Retraso antes del bucle 0 para bucle infinito + Se admiten IPv4, IPv6 y dominios. Introduce una URL que apunte a la dirección de un complemento remoto.\nPor ejemplo, \"https://example.com/plugin.apk\". Último uso: %1$s Hilo \"blob\" fallido @@ -561,7 +567,10 @@ Conectar al PC Conectado Conectados: %1$d + Conectando + Conectando a \"%1$s\"... No se puede establecer la conexión + Conexión fallida Consola Continuar Copiar @@ -1154,6 +1163,7 @@ Nombre de la versión (Calculando...) Ver Ver documentos + Ventanas emergentes en segundo plano La versión de la extensión VSCode no cumple los requisitos Esperando a que se complete el procesamiento de todos los datos... Tarea semanal @@ -1161,5 +1171,6 @@ Ruta del directorio de trabajo Escribir la configuración de seguridad Escribir la configuración del sistema + Ventanas emergentes en segundo plano diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 23d7389a..882f5b02 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -72,6 +72,7 @@ L\'autorisation \"gérer tous les fichiers\" (ou \"accès à tous les fichiers\") permet à AutoJs6 d\'[ créer / lire / modifier / supprimer ] des fichiers directement via des chemins de fichiers classiques dans l\'espace de stockage partagé, afin que les scripts puissent accéder à \"Internal Storage\" et que l\'explorateur de fichiers puisse afficher et gérer les fichiers correctement.\n\nSur les appareils Android 11+, c\'est le principal moyen d\'obtenir un accès lecture/écriture à l\'ensemble du stockage. Cette préférence permet de modifier la langue d\'affichage d\'AutoJs6, y compris les messages d\'exception des scripts en cours d\'exécution.\n\nNote : un redémarrage de l\'application peut être nécessaire pour que la langue s\'applique comme prévu. Lorsque le mode nuit automatique est activé, AutoJs6 bascule automatiquement en mode nuit en fonction des paramètres système.\n\nNote: l\'interrupteur de mode nuit automatique et l\'interrupteur de mode nuit sont liés et s\'influencent mutuellement. + L\'autorisation \"fenêtres contextuelles en arrière-plan\" (également appelée \"démarrer une interface en arrière-plan\" ou \"afficher une interface en arrière-plan\") permet à AutoJs6, même lorsque l\'application est en arrière-plan ou sans interface visible, de démarrer une Activity ou d\'ouvrir une page de paramètres spécifique. Utile dans des scénarios tels que [ ouvrir l\'UI lors du déclenchement d\'une tâche planifiée / reprendre l\'interaction après l\'écran verrouillé ou la mise en veille / ouvrir la page de configuration des scripts via une notification ou un raccourci ].\n\nNote: sur des systèmes comme Xiaomi (MIUI/HyperOS) et Vivo (OriginOS/Funtouch OS), cette autorisation peut être désactivée par défaut. Si elle n\'est pas accordée, le système peut bloquer l\'ouverture de pages depuis l\'arrière-plan par un script, ce qui peut se manifester par [ aucune réaction / exécution en arrière-plan sans interface / échec de navigation ].\n\nMême après l\'avoir accordée, d\'autres politiques système peuvent encore s\'appliquer, telles que [ optimisation de la batterie / restrictions d\'auto‑démarrage / gel en arrière-plan / mise en veille de l\'application ]. Il est recommandé de l\'ajuster conjointement avec les autorisations associées ou des réglages de liste blanche. Changer le chemin du répertoire contenant les scripts. AutoJs6 obtient et télécharge les mises à jour depuis GitHub. Le mode client permet à AutoJs6 de se connecter activement à un serveur distant afin d\'effectuer [ transfert de scripts / impression des journaux / contrôle à distance ].\n\nEn général, l\'appareil et le serveur doivent être sur le même réseau local (LAN) ou dans un environnement réseau où ils peuvent s\'atteindre mutuellement. @@ -109,7 +110,9 @@ Consulter l\'historique des versions publiées d\'AutoJs6 sur GitHub ainsi que les statistiques des principales catégories. Paramètres de sécurité du système, contenant les préférences du système que les applications peuvent lire mais ne sont pas autorisées à écrire.\nIl s\'agit des préférences que l\'utilisateur doit explicitement modifier par le biais de l\'interface utilisateur d\'une application système.\nAvec l\'autorisation de paramètres de sécurité du système, les applications normales peuvent directement modifier les paramètres de sécurité (comme le service d\'accessibilité). L\'autorisation \"modifier les paramètres système\" permet à AutoJs6 de modifier certains paramètres système, afin que les scripts puissent changer des réglages tels que [ luminosité de l\'écran / rotation automatique / délai d\'extinction de l\'écran ]. + Abandonner Avancé + Corriger l\'adresse @string/text_back Cancel Annulation @@ -117,7 +120,6 @@ OK Se connecter @string/text_continue - @string/dialog_button_continue @string/text_copy Préfixe def Details @@ -127,12 +129,12 @@ Infos fichier Histoire Ignorer + Interrompre la connexion Joindre groupe Gestionnaire Plus Palette Quit - @string/dialog_button_quit Supprimer Récupérer Retourner @@ -212,6 +214,8 @@ Les deux points doivent suivre une adresse IP valide Compass ne peut pas être null Impossible de se connecter au serveur distant : %s + Échec de la connexion à \"%1$s\". + Délai de connexion dépassé (%d ms) La méthode resume() doit être appelée après suspend(). La méthode suspend() ne devrait être appelée qu\'une seule fois. La version GitHub correspondante n\'a peut-être pas encore été publiée. @@ -265,6 +269,7 @@ Invalid rest compass : %s Elément de carte de sélecteur non valide : { %s : %s (%s) } L\'adresse IP ne peut pas être vide + Le port IPv6 doit être spécifié au format \"[ipv6]:port\". Max listeners exceeded : %d AutoJs6 peut ne pas avoir l\'accès root pour exécuter le fichier \"auto\". %s() appelé avec un argument nul : %s @@ -337,6 +342,7 @@ Cliquez longuement sur le bouton \"Run\" pour déboguer Délai avant boucle 0 pour une boucle infinie + IPv4, IPv6 et les noms de domaine sont pris en charge. Saisissez une URL pointant vers l\'adresse d\'un plugin distant.\nPar exemple \"https://example.com/plugin.apk\". Dernière utilisation : %1$s Échec du thread \"blob\" @@ -561,7 +567,10 @@ Connexion au PC Connecté Connectés : %1$d + Connexion... + Connexion à \"%1$s\"... La connexion ne peut pas être établie + Échec de la connexion Console Continue Copie @@ -1154,6 +1163,7 @@ Nom de la version (en cours de calcul...) Voir Voir les documents + Fenêtres contextuelles en arrière-plan La version de l\'extension VSCode ne répond pas aux exigences En attente de la fin du traitement de toutes les données... Tâche hebdomadaire @@ -1161,5 +1171,6 @@ Chemin du répertoire de travail. Écrire les paramètres de sécurité. Écrire les paramètres système + Fenêtres contextuelles en arrière-plan diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 8e33039a..5aa4dc64 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -73,6 +73,7 @@ \"すべてのファイルを管理\" (または \"すべてのファイルへのアクセス\") 権限により, AutoJs6 は共有ストレージ上で通常のファイルパスを使って直接 [ 作成 / 読み取り / 変更 / 削除 ] を行えます. これによりスクリプトは \"Internal Storage\" にアクセスでき, ファイルマネージャーもファイルを正常に表示・管理できます.\n\nAndroid 11+ では, フルストレージの読み書きを実現する主要な方法です. この環境設定は, 実行中のスクリプトからの例外メッセージを含む, AutoJs6 の表示言語を変更するためのものです.\n\n注: 言語を期待通りに適用するには, アプリの再起動が必要な場合があります 自動ナイトモードを有効にすると, AutoJs6 はシステム設定に従って自動的にナイトモードへ切り替えます.\n\nNote: 自動ナイトモードのスイッチとナイトモードのスイッチは相互に関連しており, 互いに影響します. + \"バックグラウンドでのポップアップ表示\" (\"バックグラウンドでの画面起動\" / \"バックグラウンドでの画面表示\" とも呼ばれます) 権限により/アプリがバックグラウンドにある/または画面が表示されていない場合でも/AutoJs6 が Activity を起動したり特定の設定ページを開いたりできます.[ 定時タスク発火時に UI を開く / ロック画面や待機後に対話を復帰する / 通知やショートカットからスクリプト設定ページを開く ] といったシーンに適しています.\n\nNote: Xiaomi (MIUI/HyperOS)/Vivo (OriginOS/Funtouch OS) などでは/この権限が既定でオフになっている場合があります. 未付与の場合/バックグラウンドからページを開こうとするスクリプトがシステムにブロックされ/[ 無反応 / バックグラウンドでのみ実行され画面が出ない / 遷移失敗 ] のように見えることがあります.\n\n 付与後でも/[ バッテリー最適化 / 自動起動制限 / バックグラウンド凍結 / アプリスタンバイ ] など他のシステムポリシーの影響を受ける可能性があります. 関連する権限やホワイトリスト設定と併せて調整してください. スクリプトを含むディレクトリのパスを変更する AutoJs6 は, GitHub からアップデートを取得し, ダウンロードします. クライアントモードは, AutoJs6 がリモートのサーバーへ能動的に接続し, [ スクリプト転送 / ログ出力 / リモート制御 ] などを行うための機能です.\n\n通常は, 端末とサーバーが同一 LAN か, 相互に到達可能なネットワーク環境にある必要があります. @@ -110,7 +111,9 @@ GitHub 上の AutoJs6 のリリース版変更履歴と主要カテゴリの統計データを確認する. アプリケーションが読み取ることはできるが, 書き込むことはできないシステム環境設定を含む, 安全なシステム設定です.\nこれは, ユーザーがシステムアプリの UI を通じて明示的に変更する必要がある環境設定のためのものです.\nセキュアなシステム設定を許可すると, 通常のアプリケーションはセキュアな設定 (アクセシビリティサービスなど) を直接変更できるようになります \"システム設定の変更\" 権限により, AutoJs6 は一部のシステム設定を変更でき, スクリプトで [ 画面の明るさ / 自動回転 / 画面タイムアウト ] などを調整できます. + 放棄 詳細 + アドレスを修正 @string/text_back 取消 取消 @@ -118,7 +121,6 @@ OK 接続 @string/text_continue - @string/dialog_button_continue @string/text_copy デフォ前置 詳細 @@ -128,12 +130,12 @@ ファイル情報 履歴 無視する + 接続を中止 グループ参加 マネージャー 詳細 パレットを開く 終了する - @string/dialog_button_quit 削除 取得 再試行 @@ -213,6 +215,8 @@ 有効な IP アドレスの後にコロンが続くこと コンパスは null にできません リモートサーバーに接続できません. %s + \"%1$s\" への接続に失敗しました. + 接続がタイムアウトしました (%d ミリ秒) resume() メソッドは suspend() の後に呼び出す必要があります suspend() メソッドは一度だけコールされるべきです 対応する GitHub のリリースがまだ公開されていない可能性があります @@ -266,6 +270,7 @@ 無効な残りコンパスです: %s 無効なセレクタ・マップ要素です. { %s: %s (%s) } です IP アドレスは空であってはならない + IPv6 のポートは \"[ipv6]:port\" 形式で指定する必要があります. リスナーの最大数を超えています: %d AutoJs6 は \"auto\" ファイルを実行するためのルート・アクセス権を持っていない可能性があります null 引数で %s() が呼び出されました: %s @@ -338,6 +343,7 @@ 実行」ボタン長押しでデバッグ ループ前の遅延時間 無限ループの場合は 0 + IPv4/IPv6/およびドメイン名に対応しています. リモートプラグインの URL を入力してください. \n例: \"https://example.com/plugin.apk\" 最終使用時: %1$s \"Blob\" スレッドが失敗 @@ -562,7 +568,10 @@ PC に接続する 接続済み 接続済み: %1$d + 接続中 + \"%1$s\" に接続しています... 接続が確立できない + 接続に失敗しました コンソール 続行 コピー @@ -1155,6 +1164,7 @@ バージョン名 (計算中...) 表示 ドキュメントを見る + バックグラウンドでのポップアップ表示 VSCode 拡張機能のバージョンが要件を満たしていない すべてのデータ処理が完了するのを待機しています... 週次タスク @@ -1162,5 +1172,6 @@ 作業ディレクトリのパス セキュリティ設定の書き込み システム設定の書き込み + バックグラウンドでのポップアップ表示 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 90f0af71..227d7858 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -74,6 +74,7 @@ \"모든 파일 관리\" (또는 \"모든 파일 접근\") 권한은 AutoJs6 가 공유 저장소에서 일반적인 파일 경로를 통해 직접 [ 생성 / 읽기 / 수정 / 삭제 ] 를 수행할 수 있게 하여, 스크립트가 \"Internal Storage\"에 접근할 수 있도록 하고 파일 관리자가 파일을 정상적으로 표시 및 관리할 수 있도록 합니다.\n\nAndroid 11+ 기기에서는 전체 저장소 파일 읽기/쓰기 기능을 구현하는 주요 방식입니다. 실행 스크립트의 예외 메시지를 포함하여 AutoJs6 의 디스플레이 언어를 변경하는이 선호도.\n\n참고: 예상대로 언어를 적용하려면 앱 재시작이 필요할 수 있습니다. 자동 야간 모드를 켜면 AutoJs6 가 시스템 설정에 따라 자동으로 야간 모드를 전환합니다.\n\nNote: 자동 야간 모드 스위치와 야간 모드 스위치는 서로 연동되며 상호 영향을 줍니다. + \"백그라운드 팝업\" (또는 \"백그라운드에서 화면 시작\" / \"백그라운드에서 화면 표시\") 권한은 앱이 백그라운드에 있거나 화면이 표시되지 않는 상태에서도 AutoJs6 가 Activity 를 시작하거나 특정 설정 페이지를 열 수 있도록 합니다. [ 예약 작업 트리거 시 UI 열기 / 잠금 화면 또는 대기 후 상호작용 복원 / 알림 또는 바로가기에서 스크립트 설정 페이지 호출 ] 같은 시나리오에 적합합니다.\n\nNote: Xiaomi (MIUI/HyperOS), Vivo (OriginOS/Funtouch OS) 등의 시스템에서는 이 권한이 기본적으로 꺼져 있을 수 있습니다. 권한이 없으면 스크립트가 백그라운드에서 페이지를 열려고 할 때 시스템이 차단하여 [ 무반응 / 백그라운드에서만 실행되고 화면이 표시되지 않음 / 이동 실패 ] 로 보일 수 있습니다.\n\n 권한을 부여한 후에도 [ 배터리 최적화 / 자동 시작 제한 / 백그라운드 동결 / 앱 대기 ] 등 다른 시스템 정책의 영향을 받을 수 있으므로, 관련 권한이나 화이트리스트 설정과 함께 조정하는 것을 권장합니다. 스크립트가 포함 된 디렉토리의 경로를 변경하십시오 AutoJs6 은 GitHub 에서 업데이트를 가져오고 다운로드합니다. 클라이언트 모드는 AutoJs6 가 원격 서버에 능동적으로 연결하여 [ 스크립트 전송 / 로그 출력 / 원격 제어 ] 등을 수행할 수 있도록 합니다.\n\n 일반적으로 기기와 서버는 동일한 LAN 또는 서로 접근 가능한 네트워크 환경에 있어야 합니다. @@ -111,7 +112,9 @@ GitHub 에서 AutoJs6 릴리스 버전의 변경 기록과 주요 분류 통계 데이터를 확인하기. 애플리케이션이 읽을 수 있지만 쓸 수없는 시스템 환경 설정을 포함하는 보안 시스템 설정.\n이들은 사용자가 시스템 앱의 UI 를 통해 명시 적으로 수정 해야하는 선호도입니다.\n보안 시스템 설정 권한을 사용하면 일반 애플리케이션이 보안 설정 (예: 접근성 서비스)을 직접 수정할 수 있습니다. \"시스템 설정 수정\" 권한은 AutoJs6 가 일부 시스템 설정을 변경할 수 있게 하여, 스크립트가 [ 화면 밝기 / 자동 회전 / 화면 시간 제한 ] 등의 설정을 변경할 수 있도록 합니다. + 포기 고급 + 주소 수정 @string/text_back 취소 취소 @@ -119,7 +122,6 @@ 확인 연결 @string/text_continue - @string/dialog_button_continue @string/text_copy 기본 접두사 세부 @@ -129,12 +131,12 @@ 파일 정보 역사 무시하다 + 연결 중단 그룹 가입 관리자 팔레트 열기 그만두다 - @string/dialog_button_quit 제거하다 가져오기 다시 해 보다 @@ -214,6 +216,8 @@ 콜론은 유효한 IP 주소를 따라야 합니다 나침반은 무효가 될 수 없다 원격 서버에 연결할 수 없습니다: %s + \"%1$s\"에 연결하지 못했습니다. + 연결 시간 초과 (%d 밀리초) 메소드 resume() 가 suspend() 후 호출해야합니다. 메소드 suspend() 는 한 번만 호출해야합니다 해당 GitHub 릴리스는 아직 게시되지 않았을 수 있습니다 @@ -267,6 +271,7 @@ 잘못된 휴식 나침반: %s 잘못된 선택기 맵 요소: { %s: %s (%s) } IP 주소는 비워둘 수 없습니다 + IPv6 포트는 \"[ipv6]:port\" 형식으로 지정해야 합니다. 최대 청취자를 초과했습니다: %d AutoJs6 은 \"auto\" 파일을 실행할 루트 액세스 권한이 없을 수 있습니다. 널 인수로 호출 된 %s(): %s @@ -339,6 +344,7 @@ \"실행\" 버튼을 긴 클릭하여 디버그를 클릭하십시오 루프 전 지연 무한 루프의 경우 0 + IPv4, IPv6 및 도메인을 지원합니다. 원격 플러그인 주소를 가리키는 URL을 입력하세요.\n예: \"https://example.com/plugin.apk\" 최종 사용: %1$s \"Blob\" 스레드 요청 실패 @@ -563,7 +569,10 @@ PC 에 연결하십시오 연결됨 연결됨: %1$d + 연결 중 + \"%1$s\"에 연결하는 중... 연결을 설정할 수 없습니다 + 연결 실패 콘솔 계속하다 복사 @@ -1156,6 +1165,7 @@ 버전 이름 (계산 중...) 보기 문서를 봅니다 + 백그라운드 팝업 VSCode 확장 버전이 요구 사항을 충족하지 않습니다 모든 데이터 처리가 완료될 때까지 대기 중입니다... 주간 과제 @@ -1163,5 +1173,6 @@ 작업 디렉토리 경로 보안 설정을 작성하십시오 시스템 설정을 작성하십시오 + 백그라운드 팝업 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 59c85abe..f8b18a01 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -72,6 +72,7 @@ Разрешение \"управление всеми файлами\" (или \"доступ ко всем файлам\") позволяет AutoJs6 напрямую [ создавать / читать / изменять / удалять ] файлы в общем хранилище через обычные пути файлов, чтобы скрипты могли получать доступ к \"Internal Storage\", а файловый менеджер мог корректно отображать и управлять файлами.\n\nНа устройствах Android 11+ это основной способ обеспечить полный доступ к чтению/записи файлов. Это предпочтение для изменения языка отображения AutoJs6, включая сообщения исключений от запущенных скриптов.\n\nПримечание: может потребоваться перезапуск приложения, чтобы язык был применен так, как ожидается. После включения автоматического ночного режима AutoJs6 будет автоматически переключать ночной режим в соответствии с настройками системы.\n\nNote: переключатель автоматического ночного режима и переключатель ночного режима связаны и взаимно влияют друг на друга. + Разрешение \"всплывающие окна в фоне\" (также известное как \"запуск интерфейса из фона\" или \"показ интерфейса в фоне\") позволяет AutoJs6, даже когда приложение находится в фоне или без видимого интерфейса, запускать Activity или открывать определённую страницу настроек. Подходит для сценариев [ открывать UI при срабатывании запланированной задачи / восстанавливать взаимодействие после блокировки экрана или режима ожидания / открывать страницу настройки скрипта из уведомления или ярлыка ].\n\nNote: на системах Xiaomi (MIUI/HyperOS), Vivo (OriginOS/Funtouch OS) и т. п. это разрешение может быть отключено по умолчанию. Если оно не выдано, система может блокировать попытки скрипта открыть страницу из фона, что может проявляться как [ нет реакции / выполнение только в фоне без интерфейса / неудачный переход ].\n\nДаже после выдачи разрешения на поведение могут влиять другие политики системы, такие как [ оптимизация батареи / ограничения автозапуска / заморозка в фоне / режим ожидания приложения ]. Рекомендуется настраивать совместно с связанными разрешениями или белыми списками. Измените путь к директории, содержащей скрипты AutoJs6 получает и загружает обновления с GitHub. Клиентский режим позволяет AutoJs6 активно подключаться к удалённому серверу для выполнения [ передачи скриптов / вывода логов / удалённого управления ].\n\nОбычно устройство и сервер должны находиться в одной локальной сети (LAN) или в сетевой среде, где они могут обращаться друг к другу. @@ -109,7 +110,9 @@ Просмотреть историю выпусков AutoJs6 на GitHub и статистику по основным категориям. Настройки безопасности системы, содержащие системные предпочтения, которые приложения могут читать, но не имеют права записывать.\nОни предназначены для параметров, которые пользователь должен явно изменить через пользовательский интерфейс системного приложения.\nПри наличии разрешения на безопасные системные настройки обычные приложения могут напрямую изменять безопасные настройки (например, служба доступности). Разрешение \"изменение системных настроек\" позволяет AutoJs6 изменять некоторые параметры системы, чтобы скрипты могли менять такие настройки, как [ яркость экрана / автоповорот / тайм-аут экрана ]. + Отказаться Доп. + Исправить адрес @string/text_back Отменить Отменить @@ -117,7 +120,6 @@ OK Подключиться @string/text_continue - @string/dialog_button_continue @string/text_copy Префикс деф. Подробности @@ -127,12 +129,12 @@ Информация о файле История Игнорировать + Прервать соединение Вступить группу Менеджер Еще Открыть палитру Выйти - @string/dialog_button_quit Удалить Получить Повторная попытка @@ -212,6 +214,8 @@ Двоеточие должно следовать за действительным IP-адресом Компас не может быть нулевым Не удается подключиться к удаленному серверу: %s + Не удалось подключиться к \"%1$s\". + Тайм-аут подключения (%d мс) Метод resume() должен вызываться после suspend() Метод suspend() должен быть вызван только один раз Соответствующий релиз на GitHub может быть еще не опубликован @@ -265,6 +269,7 @@ Неверный остаток компаса: %s Неверный элемент карты селектора: { %s: %s (%s) } IP-адрес не может быть пустым + Порт IPv6 должен быть указан в формате \"[ipv6]:port\". Превышено максимальное количество слушателей: %d AutoJs6 может не иметь root-доступа для запуска файла \"auto\" %s() вызвана с нулевым аргументом: %s @@ -337,6 +342,7 @@ Длительное нажатие кнопки \"Выполнить\" для отладки Задержка перед циклом 0 для бесконечного цикла + Поддерживаются IPv4, IPv6 и доменные имена. Введите URL, указывающий на удалённый плагин.\nНапример: \"https://example.com/plugin.apk\". Последнее использование: %1$s Поток \"blob\" неудачен @@ -561,7 +567,10 @@ Подключитесь к ПК Подключено Подключено: %1$d + Подключение... + Подключение к \"%1$s\"... Соединение не может быть установлено + Не удалось подключиться Консоль Продолжить Скопировать @@ -1154,6 +1163,7 @@ Название версии (Вычисление...) Просмотр Просмотр документов + Всплывающие окна в фоне Версия расширения VSCode не соответствует требованиям Ожидание завершения обработки всех данных... Недельное задание @@ -1161,5 +1171,6 @@ Путь к рабочему каталогу Параметры безопасности записи Запись системных настроек + Всплывающие окна в фоне diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 1e2d0274..54ace4c9 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -71,6 +71,7 @@ \"管理所有文件\" (或 \"所有文件訪問\") 權限允許 AutoJs6 在共享存儲空間中通過常規文件路徑直接 [ 創建 / 讀取 / 修改 / 刪除 ] 文件, 使腳本可以訪問 \"內部存儲 (Internal Storage)\", 使文件管理器可以正常顯示及管理文件.\n\n在 Android 11+ 設備上, 這是實現全盤文件讀寫能力的主要方式. 設置選項用於修改 AutoJs6 應用的文本內容顯示語言, 同時包括腳本運行產生的錯誤消息等.\n\n注: 部分內容可能需要重啓應用才能完成語言切換. 自動夜間模式開啓後, AutoJs6 將根據系統設置自動切換夜間模式.\n\注: 自動夜間模式開關與夜間模式開關互相關聯且互相影響. + \"後台彈出界面\" (也稱 \"後台啓動界面\" 或 \"後台彈出頁面\") 權限允許 AutoJs6 在應用位於後台或未顯示界面時, 仍可主動啓動界面(Activity) 或打開特定設置頁, 適合 [ 定時任務觸發時打開 UI / 在鎖屏或待機後恢復交互 / 由通知或快捷方式喚起腳本配置頁 ] 等場景.\n\n注: 在 Xiaomi (MIUI/HyperOS), Vivo (OriginOS/Funtouch OS) 等系統上, 該權限可能默認關閉. 未授予時, 腳本嘗試從後台打開頁面可能會被系統攔截, 表現為 [ 無響應 / 僅後台執行但不顯示界面 / 跳轉失敗 ].\n\n授予後仍可能受到系統其他策略影響, 如 [ 電池優化 / 自啓動限制 / 後台凍結 / 應用待機 ] 等, 可結合相關權限或白名單設置一起調整. 更改包含腳本的文件夾路徑 AutoJs6 從 GitHub 獲取並下載更新. 客户端模式用於讓 AutoJs6 主動連接到遠端服務端, 以便進行 [ 腳本傳輸 / 打印日誌 / 遠程控制 ] 等.\n\n通常需要設備與服務端處於同一局域網或可互相訪問的網絡環境. @@ -107,7 +108,9 @@ 查看 AutoJs6 在 GitHub 發行版本的歷史更新記錄及重要分類的統計數據. 安全設置包含應用程序可讀但不可寫入的設置選項, 這些選項只能由 UI 或系統級別應用修改.\n被授予 \"修改安全設置權限\" 後, 普通應用可直接修改上述安全設置 (例如無障礙服務). \"修改系統設置\" 權限允許 AutoJs6 修改部分系統設置項, 使腳本可以修改 [ 屏幕亮度 / 自動旋轉 / 屏幕超時 ] 等系統設置參數. + 放棄 高級設置 + 修正地址 @string/text_back 取消 取消下載 @@ -115,7 +118,6 @@ 確定 連接 @string/text_continue - 繼續連接 @string/text_copy 默認前綴 瞭解更多 @@ -125,12 +127,12 @@ 文件信息 歷史記錄 忽略此版本 + 中止連接 加入羣組 管理器 瞭解更多 打開調色盤 放棄 - 放棄連接 移除 獲取 重試 @@ -210,6 +212,8 @@ 冒號需跟隨有效的 IP 地址 羅盤參數不能為 null 連接失敗: %s + 連接至 \"%1$s\" 失敗. + 連接超時 (%d 毫秒) 方法 resume() 調用前需先調用 suspend() 方法 suspend() 只能調用一次 對應的 GitHub 發行版可能還未發佈 @@ -263,6 +267,7 @@ 無效的剩餘羅盤參數: %s 不合法的選擇器集合元素: { %s: %s (%s) } IP 地址不可為空 + IPv6 端口必須使用 \"[ipv6]:port\" 格式. 超出最大監聽器數量限制: %d AutoJs6 可能因缺少 Root 權限而無法運行 \"auto\" 文件 %s() 傳入的 %s 參數為空 @@ -335,6 +340,7 @@ 長按 \"運行\" 圖標可啓動調試 開始循環前的延遲 0 表示無限循環 + 支持 IPv4, IPv6 及域名. 輸入一個指向遠程插件地址的 URL.\n例如 \"https://example.com/plugin.apk\". 最近使用: %1$s 線程 "blob" 請求失敗 @@ -559,7 +565,10 @@ 連接到計算機 已連接 已連接: %1$d + 連接中 + 正在連接至 \"%1$s\"... 無法建立連接 + 連接失敗 控制枱 繼續 複製 @@ -1152,6 +1161,7 @@ 版本名稱 (計算中...) 查看 查看文檔 + 後台彈出界面 VSCode 插件版本不符合要求 正在等待全部數據處理完畢... 每週任務 @@ -1159,5 +1169,6 @@ 工作路徑 修改安全設置 修改系統設置 + 後台彈出界面 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 442003e0..f2501fdd 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -71,6 +71,7 @@ \"管理所有檔案\" (或 \"所有檔案訪問\") 許可權允許 AutoJs6 在共享儲存空間中透過常規檔案路徑直接 [ 建立 / 讀取 / 修改 / 刪除 ] 檔案, 使指令碼可以訪問 \"內部儲存 (Internal Storage)\", 使檔案管理器可以正常顯示及管理檔案.\n\n在 Android 11+ 裝置上, 這是實現全盤檔案讀寫能力的主要方式. 設定選項用於修改 AutoJs6 應用的文字內容顯示語言, 同時包括指令碼執行產生的錯誤訊息等.\n\n注: 部分內容可能需要重啟應用才能完成語言切換. 自動夜間模式開啟後, AutoJs6 將根據系統設定自動切換夜間模式.\n\注: 自動夜間模式開關與夜間模式開關互相關聯且互相影響. + \"後臺彈出介面\" (也稱 \"後臺啟動介面\" 或 \"後臺彈出頁面\") 許可權允許 AutoJs6 在應用位於後臺或未顯示介面時, 仍可主動啟動介面(Activity) 或開啟特定設定頁, 適合 [ 定時任務觸發時開啟 UI / 在鎖屏或待機後恢復互動 / 由通知或快捷方式喚起指令碼配置頁 ] 等場景.\n\n注: 在 Xiaomi (MIUI/HyperOS), Vivo (OriginOS/Funtouch OS) 等系統上, 該許可權可能預設關閉. 未授予時, 指令碼嘗試從後臺開啟頁面可能會被系統攔截, 表現為 [ 無響應 / 僅後臺執行但不顯示介面 / 跳轉失敗 ].\n\n授予後仍可能受到系統其他策略影響, 如 [ 電池最佳化 / 自啟動限制 / 後臺凍結 / 應用待機 ] 等, 可結合相關許可權或白名單設定一起調整. 更改包含指令碼的資料夾路徑 AutoJs6 從 GitHub 獲取並下載更新. 客戶端模式用於讓 AutoJs6 主動連線到遠端服務端, 以便進行 [ 指令碼傳輸 / 列印日誌 / 遠端控制 ] 等.\n\n通常需要裝置與服務端處於同一區域網或可互相訪問的網路環境. @@ -107,7 +108,9 @@ 檢視 AutoJs6 在 GitHub 發行版本的歷史更新記錄及重要分類的統計資料. 安全設定包含應用程式可讀但不可寫入的設定選項, 這些選項只能由 UI 或系統級別應用修改.\n被授予 \"修改安全設定許可權\" 後, 普通應用可直接修改上述安全設定 (例如無障礙服務). \"修改系統設定\" 許可權允許 AutoJs6 修改部分系統設定項, 使指令碼可以修改 [ 螢幕亮度 / 自動旋轉 / 螢幕超時 ] 等系統設定引數. + 放棄 高階設定 + 修正地址 @string/text_back 取消 取消下載 @@ -115,7 +118,6 @@ 確定 連線 @string/text_continue - 繼續連線 @string/text_copy 預設字首 瞭解更多 @@ -125,12 +127,12 @@ 檔案資訊 歷史記錄 忽略此版本 + 中止連線 加入群組 管理器 瞭解更多 開啟調色盤 放棄 - 放棄連線 移除 獲取 重試 @@ -210,6 +212,8 @@ 冒號需跟隨有效的 IP 地址 羅盤引數不能為 null 連線失敗: %s + 連線至 \"%1$s\" 失敗. + 連線超時 (%d 毫秒) 方法 resume() 呼叫前需先呼叫 suspend() 方法 suspend() 只能呼叫一次 對應的 GitHub 發行版可能還未釋出 @@ -263,6 +267,7 @@ 無效的剩餘羅盤引數: %s 不合法的選擇器集合元素: { %s: %s (%s) } IP 地址不可為空 + IPv6 埠必須使用 \"[ipv6]:port\" 格式. 超出最大監聽器數量限制: %d AutoJs6 可能因缺少 Root 許可權而無法執行 \"auto\" 檔案 %s() 傳入的 %s 引數為空 @@ -335,6 +340,7 @@ 長按 \"執行\" 圖示可啟動除錯 開始迴圈前的延遲 0 表示無限迴圈 + 支援 IPv4, IPv6 及域名. 輸入一個指向遠端外掛地址的 URL.\n例如 \"https://example.com/plugin.apk\". 最近使用: %1$s 執行緒 "blob" 請求失敗 @@ -559,7 +565,10 @@ 連線到計算機 已連線 已連線: %1$d + 連線中 + 正在連線至 \"%1$s\"... 無法建立連線 + 連線失敗 控制檯 繼續 複製 @@ -1152,6 +1161,7 @@ 版本名稱 (計算中...) 檢視 檢視文件 + 後臺彈出介面 VSCode 外掛版本不符合要求 正在等待全部資料處理完畢... 每週任務 @@ -1159,5 +1169,6 @@ 工作路徑 修改安全設定 修改系統設定 + 後臺彈出介面 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index e4ba142b..f6c59b34 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -70,6 +70,7 @@ \"管理所有文件\" (或 \"所有文件访问\") 权限允许 AutoJs6 在共享存储空间中通过常规文件路径直接 [ 创建 / 读取 / 修改 / 删除 ] 文件, 使脚本可以访问 \"内部存储 (Internal Storage)\", 使文件管理器可以正常显示及管理文件.\n\n在 Android 11+ 设备上, 这是实现全盘文件读写能力的主要方式. 设置选项用于修改 AutoJs6 应用的文本内容显示语言, 同时包括脚本运行产生的错误消息等.\n\n注: 部分内容可能需要重启应用才能完成语言切换. 自动夜间模式开启后, AutoJs6 将根据系统设置自动切换夜间模式.\n\注: 自动夜间模式开关与夜间模式开关互相关联且互相影响. + \"后台弹出界面\" (也称 \"后台启动界面\" 或 \"后台弹出页面\") 权限允许 AutoJs6 在应用位于后台或未显示界面时, 仍可主动启动界面(Activity) 或打开特定设置页, 适合 [ 定时任务触发时打开 UI / 在锁屏或待机后恢复交互 / 由通知或快捷方式唤起脚本配置页 ] 等场景.\n\n注: 在 Xiaomi (MIUI/HyperOS), Vivo (OriginOS/Funtouch OS) 等系统上, 该权限可能默认关闭. 未授予时, 脚本尝试从后台打开页面可能会被系统拦截, 表现为 [ 无响应 / 仅后台执行但不显示界面 / 跳转失败 ].\n\n授予后仍可能受到系统其他策略影响, 如 [ 电池优化 / 自启动限制 / 后台冻结 / 应用待机 ] 等, 可结合相关权限或白名单设置一起调整. 更改包含脚本的文件夹路径 AutoJs6 从 GitHub 获取并下载更新. 客户端模式用于让 AutoJs6 主动连接到远端服务端, 以便进行 [ 脚本传输 / 打印日志 / 远程控制 ] 等.\n\n通常需要设备与服务端处于同一局域网或可互相访问的网络环境. @@ -107,7 +108,9 @@ 查看 AutoJs6 在 GitHub 发行版本的历史更新记录及重要分类的统计数据. 安全设置包含应用程序可读但不可写入的设置选项, 这些选项只能由 UI 或系统级别应用修改.\n被授予 \"修改安全设置权限\" 后, 普通应用可直接修改上述安全设置 (例如无障碍服务). \"修改系统设置\" 权限允许 AutoJs6 修改部分系统设置项, 使脚本可以修改 [ 屏幕亮度 / 自动旋转 / 屏幕超时 ] 等系统设置参数. + 放弃 高级设置 + 修正地址 @string/text_back 取消 取消下载 @@ -115,7 +118,6 @@ 确定 连接 @string/text_continue - 继续连接 @string/text_copy 默认前缀 了解更多 @@ -125,12 +127,12 @@ 文件信息 历史记录 忽略此版本 + 中止连接 加入群组 管理器 了解更多 打开调色盘 放弃 - 放弃连接 移除 获取 重试 @@ -210,6 +212,8 @@ 冒号需跟随有效的 IP 地址 罗盘参数不能为 null 连接失败: %s + 连接至 \"%1$s\" 失败. + 连接超时 (%d 毫秒) 方法 resume() 调用前需先调用 suspend() 方法 suspend() 只能调用一次 对应的 GitHub 发行版可能还未发布 @@ -263,6 +267,7 @@ 无效的剩余罗盘参数: %s 不合法的选择器集合元素: { %s: %s (%s) } IP 地址不可为空 + IPv6 端口必须使用 \"[ipv6]:port\" 格式. 超出最大监听器数量限制: %d AutoJs6 可能因缺少 Root 权限而无法运行 \"auto\" 文件 %s() 传入的 %s 参数为空 @@ -335,6 +340,7 @@ 长按 \"运行\" 图标可启动调试 开始循环前的延迟 0 表示无限循环 + 支持 IPv4, IPv6 及域名. 输入一个指向远程插件地址的 URL.\n例如 \"https://example.com/plugin.apk\". 最近使用: %1$s 线程 \"blob\" 请求失败 @@ -559,7 +565,10 @@ 连接到计算机 已连接 已连接: %1$d + 连接中 + 正在连接至 \"%1$s\"... 无法建立连接 + 连接失败 控制台 继续 复制 @@ -1152,6 +1161,7 @@ 版本名称 (计算中...) 查看 查看文档 + 后台弹出界面 VSCode 插件版本不符合要求 正在等待全部数据处理完毕... 每周任务 @@ -1159,5 +1169,6 @@ 工作路径 修改安全设置 修改系统设置 + 后台弹出界面 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3292c22b..5b77ce64 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -324,6 +324,7 @@ The \"manage all files\" (or \"all files access\") permission allows AutoJs6 to directly [ create / read / modify / delete ] files in shared storage via normal file paths, allowing scripts to access \"Internal Storage\" and enabling the file manager to display and manage files properly.\n\nOn Android 11+ devices, this is the primary way to achieve full file read/write access. Preference for changing the display language of AutoJs6, including exception messages from running scripts.\n\nNote: an app restart may be needed to make language applied as expected. When auto night mode is enabled, AutoJs6 will automatically switch night mode based on system settings.\n\nNote: the auto night mode toggle and the night mode toggle are linked and affect each other. + The \"background pop-up\" permission (also known as \"background activity launch\" or \"launch UI from background\") allows AutoJs6 to proactively start an Activity or open specific settings pages even when the app is in the background or no UI is currently visible. It is useful for scenarios such as [ opening UI when a scheduled task is triggered / resuming interaction after the device is locked or idle / opening a script configuration page from a notification or shortcut ].\n\nNote: on systems like Xiaomi (MIUI/HyperOS) and Vivo (OriginOS/Funtouch OS), this permission may be disabled by default. Without it, attempts to open pages from the background may be blocked by the system, which can appear as [ no response / running in background only without showing UI / navigation failed ].\n\nEven after granting it, the behavior may still be affected by other system policies such as [ battery optimizations / auto-start restrictions / background freezing / app standby ]. Consider adjusting it together with related permissions or whitelist settings. Change the path of the directory containing scripts AutoJs6 gets and downloads updates from GitHub. Client mode allows AutoJs6 to actively connect to a remote server for [ script transfer / log printing / remote control ].\n\nTypically, the device and the server need to be on the same LAN or on a network where they can reach each other. @@ -361,7 +362,9 @@ View the release version history and key category statistics of AutoJs6 on GitHub. Secure system settings, containing system preferences that applications can read but are not allowed to write.\nThese are for preferences that the user must explicitly modify through the UI of a system app.\nWith secure system settings permission, normal applications can directly modify the secure settings (such as accessibility service). The \"write system settings\" permission allows AutoJs6 to modify some system settings, so scripts can change system parameters such as [ screen brightness / auto-rotate / screen timeout ]. + Abandon Advanced + Amend @string/text_back Cancel Cancel @@ -369,7 +372,6 @@ OK Connect @string/text_continue - @string/dialog_button_continue @string/text_copy Def prefix Details @@ -379,12 +381,12 @@ File info History Ignore + Interrupt Join group Manager More Palette Quit - @string/dialog_button_quit Remove Retrieve Retry @@ -467,6 +469,8 @@ Colon must follow a valid IP address Compass cannot be null Cannot connect to the remote server: %s + Failed to connect to \"%1$s\". + Connection timed out after %d ms Method resume() should be called after suspend() Method suspend() should be only called once Corresponding GitHub release may have not been published yet @@ -520,6 +524,7 @@ Invalid rest compass: %s Invalid selector map element: { %s: %s (%s) } IP address can not be empty + IPv6 port must be specified as \"[ipv6]:port\". Max listeners exceeded: %d AutoJs6 may not have root access to run \"auto\" file %s() called with null argument: %s @@ -592,6 +597,7 @@ Long click \"Run\" button to debug Delay before loop 0 for infinite loop + IPv4, IPv6 and domain are supported. Enter a URL pointing to a remote plugin address.\nFor example \"https://example.com/plugin.apk\". Latest used: %1$s \"Blob\" thread request failed @@ -816,7 +822,10 @@ Connect to PC Connected Connected: %1$d + Connecting + Connecting to \"%1$s\"... Connection cannot be established + Connection failed Console Continue Copy @@ -1409,6 +1418,7 @@ Version name (computing...) View View documents + Background pop-ups The version of the VSCode extension does not meet the requirements Waiting for all data processing to complete... Weekly task @@ -1416,5 +1426,6 @@ Working directory path Write security settings Write system settings + Display pop-up windows while running in the background diff --git a/version.properties b/version.properties index 0213ae13..6ea064ee 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Thu Jan 15 15:31:17 CST 2026 -BUILD_TIME=1768462277223 +#Fri Jan 16 17:47:33 CST 2026 +BUILD_TIME=1768556853657 COMPILE_SDK_VERSION=36 IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_NDK_VERSION=26.1.10909125 @@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=36 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=3614 +VERSION_BUILD=3620 VERSION_NAME=6.7.0 Alpha14 VSCODE_EXT_REQUIRED_VERSION=1.0.13