diff --git a/apkbuilder/src/main/java/com/stardust/autojs/apkbuilder/ManifestEditor.java b/apkbuilder/src/main/java/com/stardust/autojs/apkbuilder/ManifestEditor.java index 2889c49a7..2e8d7369a 100755 --- a/apkbuilder/src/main/java/com/stardust/autojs/apkbuilder/ManifestEditor.java +++ b/apkbuilder/src/main/java/com/stardust/autojs/apkbuilder/ManifestEditor.java @@ -98,6 +98,17 @@ private class MutableNodeImpl extends AxmlWriter.NodeImpl { @Override protected void onAttr(AxmlWriter.Attr a) { + // 模板 APK 若由 Android Studio 的 Run 生成,manifest 会带 + // android:testOnly="true"(IDE 注入 android.injected.testOnly)。 + // 打包产物会原样继承,导致系统拒绝普通安装并报 + // "Failed to install test-only apk"。分发用的脚本 APK 不应保留 + // 该标记,这里直接丢弃此属性;AxmlWriter.NodeImpl.onAttr 只做 + // attrs.add(a),跳过不会留下任何副作用。 + if (a.name != null && a.ns != null + && NS_ANDROID.equals(a.ns.data) + && "testOnly".equals(a.name.data)) { + return; + } ManifestEditor.this.onAttr(a); super.onAttr(a); } diff --git a/app/src/main/java/org/autojs/autojs/tool/AccessibilityProxyServiceImpl.kt b/app/src/main/java/org/autojs/autojs/tool/AccessibilityProxyServiceImpl.kt index f90b8e8a8..c22e10343 100644 --- a/app/src/main/java/org/autojs/autojs/tool/AccessibilityProxyServiceImpl.kt +++ b/app/src/main/java/org/autojs/autojs/tool/AccessibilityProxyServiceImpl.kt @@ -6,9 +6,12 @@ import android.content.ServiceConnection import android.os.IBinder import android.os.RemoteException import android.util.Log +import com.stardust.app.GlobalAppContext import com.stardust.app.service.AbstractAutoService import com.stardust.autojs.core.accessibility.AccessibilityProxyService import com.stardust.autojs.core.accessibility.IAccessibilityProxyService +import org.autojs.autojs.tool.RootTool +import org.autojs.autoxjs.R class AccessibilityProxyServiceImpl : AbstractAutoService() { @@ -60,14 +63,24 @@ class AccessibilityProxyServiceImpl : AbstractAutoService() { } override fun ensureEnabled(): Boolean { - return try { - val result = proxyService?.ensureEnabled() ?: false - Log.d(TAG, "enable: $result") - result + val result = try { + proxyService?.ensureEnabled() ?: false } catch (e: RemoteException) { Log.e(TAG, "启用无障碍服务失败", e) false } + Log.d(TAG, "enable: $result") + if (!result) { + // 仅当“应用已获得 root、但 :script 进程未授权”时才提示 root 不完整; + // 完全没有 root 权限时不作任何提示。 + if (RootTool.isRootGranted()) { + GlobalAppContext.toast( + GlobalAppContext.getString(R.string.text_root_permission_incomplete) + + GlobalAppContext.appName + ) + } + } + return result } override fun disable(): Boolean { diff --git a/app/src/main/java/org/autojs/autojs/tool/AccessibilityServiceTool.java b/app/src/main/java/org/autojs/autojs/tool/AccessibilityServiceTool.java index 323c26311..d177e8ec3 100644 --- a/app/src/main/java/org/autojs/autojs/tool/AccessibilityServiceTool.java +++ b/app/src/main/java/org/autojs/autojs/tool/AccessibilityServiceTool.java @@ -12,6 +12,7 @@ import com.stardust.autojs.core.accessibility.AccessibilityService; import com.stardust.autojs.core.util.ProcessShell; import com.stardust.view.accessibility.AccessibilityServiceUtils; +import org.autojs.autojs.tool.RootTool; import java.util.Locale; import java.util.concurrent.CompletableFuture; @@ -67,8 +68,12 @@ public static boolean enableAccessibilityServiceByRoot(Class" }`。 + */ + private fun parseShellResult(json: String): Pair = runCatching { + val obj = JsonParser.parseString(json).asJsonObject + val code = obj.get("code")?.takeIf { it.isJsonPrimitive }?.asInt ?: -1 + val stdout = obj.get("result")?.takeIf { it.isJsonPrimitive }?.asString.orEmpty() + code to stdout + }.getOrDefault(-1 to "") + + /** + * `appops get` 的 stdout 形如 `PROJECT_MEDIA: allow; time=...`。 + * 只有 mode 恰好是 allow 才算「静默允许」;default / foreground / ignore / + * deny 以及部分 ROM 扩展的 ask(5) 都应显示为关闭。不能用 + * contains("allow"),否则包名、JSON 字段或别处的 allow 会误判。 + */ + private fun isAppOpsAllowed(stdout: String): Boolean = + stdout.lineSequence().any { line -> + val mode = line.substringAfter(':', "").trim() + mode == APP_OPS_ALLOW || + mode.startsWith("$APP_OPS_ALLOW;") || + mode.startsWith("$APP_OPS_ALLOW ") + } + suspend fun setAppOpsPermission(op: String, allow: Boolean): Boolean { val packageName = GlobalAppContext.get().packageName if (instance.packageName == null) { instance.setupService(packageName).join() } - val mode = if (allow) "allow" else "ask" + // 关闭必须写 default:AOSP 的 MODE_DEFAULT(3) 是「不留显式覆盖, + // 回落系统默认策略」,对应设置里的「每次都询问」。 + // 原先写 ask,部分 ROM 把它扩展成数值 5,标准工具显示「未知(5)」, + // 而且它和 allow / foreground 一样会留下覆盖,永远回不到默认。 + val mode = if (allow) APP_OPS_ALLOW else APP_OPS_DEFAULT + val command = "appops set $packageName $op $mode" return runCatching { if (instance.available && instance.userPermission) { val service = instance.ensureShizukuService() - val result = service.runShellCommand(0, "appops set $packageName $op $mode") - result.contains("\"code\":0") + val (code, _) = parseShellResult(service.runShellCommand(0, command)) + if (code == 0) true else null } else null }.getOrNull() ?: runCatching { - Runtime.getRuntime().exec(arrayOf("su", "-c", "appops set $packageName $op $mode")).waitFor() == 0 + val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command)) + val code = process.waitFor() + process.destroy() + code == 0 }.getOrDefault(false) } @@ -178,19 +215,20 @@ class ShizukuClient private constructor() : Shizuku.OnRequestPermissionResultLis if (instance.packageName == null) { instance.setupService(packageName).join() } + val command = "appops get $packageName $op" return runCatching { if (instance.available && instance.userPermission) { val service = instance.ensureShizukuService() - val result = service.runShellCommand(0, "appops get $packageName $op") - result.contains("allow") + val (code, stdout) = parseShellResult(service.runShellCommand(0, command)) + if (code == 0) isAppOpsAllowed(stdout) else null } else null }.getOrNull() ?: runCatching { - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "appops get $packageName $op")) - val result = process.inputStream.bufferedReader().readText() - process.waitFor() + val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command)) + val stdout = process.inputStream.bufferedReader().readText() + val code = process.waitFor() process.destroy() - result.contains("allow") + if (code == 0) isAppOpsAllowed(stdout) else false }.getOrDefault(false) } } diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/api/Timers.kt b/autojs/src/main/java/com/stardust/autojs/runtime/api/Timers.kt index 545e4b131..2abed1507 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/api/Timers.kt +++ b/autojs/src/main/java/com/stardust/autojs/runtime/api/Timers.kt @@ -56,16 +56,19 @@ class Timers(private val mRuntime: ScriptRuntime) { return timerForCurrentThread.setInterval(listener, delay, *args.drop(2).toTypedArray()) } - fun clearTimeout(id: Int): Boolean { - return timerForCurrentThread.clearTimeout(id) + fun clearTimeout(id: Any?): Boolean { + val intId = (id as? Number)?.toInt() ?: return false + return timerForCurrentThread.clearTimeout(intId) } - fun clearInterval(id: Int): Boolean { - return timerForCurrentThread.clearInterval(id) + fun clearInterval(id: Any?): Boolean { + val intId = (id as? Number)?.toInt() ?: return false + return timerForCurrentThread.clearInterval(intId) } - fun clearImmediate(id: Int): Boolean { - return timerForCurrentThread.clearImmediate(id) + fun clearImmediate(id: Any?): Boolean { + val intId = (id as? Number)?.toInt() ?: return false + return timerForCurrentThread.clearImmediate(intId) } fun recycle() { diff --git a/autojs/src/main/js/v6-api/src_raw/__globals__.js b/autojs/src/main/js/v6-api/src_raw/__globals__.js index 69424685f..e207516ee 100644 --- a/autojs/src/main/js/v6-api/src_raw/__globals__.js +++ b/autojs/src/main/js/v6-api/src_raw/__globals__.js @@ -4,6 +4,8 @@ module.exports = function (runtime, global) { runtime.toast(text); } + global.toastX = runtime.toastX; + global.toastLog = function (text) { runtime.toast(text); global.log(text); diff --git a/common/src/main/java/com/stardust/app/GlobalAppContext.kt b/common/src/main/java/com/stardust/app/GlobalAppContext.kt index a0fcee3f1..999056485 100644 --- a/common/src/main/java/com/stardust/app/GlobalAppContext.kt +++ b/common/src/main/java/com/stardust/app/GlobalAppContext.kt @@ -3,6 +3,7 @@ package com.stardust.app import android.annotation.SuppressLint import android.app.Application import android.content.Context +import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.util.Log import androidx.core.content.ContextCompat @@ -91,10 +92,11 @@ object GlobalAppContext { val appName: String by lazy { try { val packageManager = get().packageManager - val packageInfo = packageManager.getPackageInfo( - get().packageName, 0 + val appInfo = packageManager.getApplicationInfo( + get().packageName, + PackageManager.GET_META_DATA ) - packageManager.getApplicationLabel(packageInfo.applicationInfo).toString() + packageManager.getApplicationLabel(appInfo).toString() } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) "unknown app" @@ -106,15 +108,16 @@ object GlobalAppContext { @get:Synchronized val appIcon: Drawable? get() { - try { + return try { val packageManager = get().packageManager - val packageInfo = packageManager.getPackageInfo( - get().packageName, 0 + val appInfo = packageManager.getApplicationInfo( + get().packageName, + PackageManager.GET_META_DATA ) - return packageManager.getApplicationIcon(packageInfo.applicationInfo) + packageManager.getApplicationIcon(appInfo) } catch (e: Exception) { - e.printStackTrace() + Log.e(TAG, "Failed to get app icon", e) + null } - return null } } \ No newline at end of file