Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -67,8 +68,12 @@ public static boolean enableAccessibilityServiceByRoot(Class<? extends android.a
String msg = e.getMessage();
if (msg != null && (msg.contains("error=13") || msg.contains("Permission denied"))) {
Log.e("AccessibilityService", "su命令无权限 (error=13)");
Context context = GlobalAppContext.get();
GlobalAppContext.toast(context.getString(R.string.text_root_permission_incomplete) + context.getString(R.string.app_name));
// 仅当应用已获得 root、但当前进程(:script)未授权时才提示“root 不完整”;
// 完全没有 root 权限时不作任何提示。
if (RootTool.isRootGranted()) {
Context context = GlobalAppContext.get();
GlobalAppContext.toast(context.getString(R.string.text_root_permission_incomplete) + context.getString(R.string.app_name));
}
return false;
}
return false;
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/org/autojs/autojs/tool/RootTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ public static boolean isRootAvailable() {
}
}

/**
* 真正以 root 身份执行命令,判断「当前进程(主进程)是否已获得 root」。
* 用于区分两种失败:
* 1) 应用完全没有 root 权限(主进程也拿不到 root);
* 2) 应用有 root,但 :script 进程未被授权(Magisk 按进程名拒绝)。
*/
public static boolean isRootGranted() {
try {
com.stardust.autojs.core.util.ProcessShell.Result result =
com.stardust.autojs.core.util.ProcessShell.execCommand("id", true);
return result.code == 0 && result.result != null && result.result.contains("uid=0");
} catch (Exception e) {
return false;
}
}

private static final String cmd = "enabled=$(settings get system pointer_location)\n" +
"if [[ $enabled == 1 ]]\n" +
"then\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,9 @@ public static boolean enableAccessibilityServiceByRoot(Class<? extends android.a
} catch (Exception e) {
String msg = e.getMessage();
if (msg != null && (msg.contains("error=13") || msg.contains("Permission denied"))) {
Log.e("AccessibilityService", "su命令无权限 (error=13)");
Context context = GlobalAppContext.get();
GlobalAppContext.toast(context.getString(R.string.text_root_permission_incomplete) + context.getString(R.string.app_name));
return false;
// 仅记录日志,不再在此处提示“root权限不完整”。
// 具体提示(完全没有 root / 仅 :script 未授权)由主进程统一判断。
Log.e("AccessibilityService", "su命令无权限 (error=13):应用未获得 root,或 root 未授权给 :script 进程");
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.google.gson.JsonParser
import com.stardust.app.GlobalAppContext
import com.stardust.autojs.BuildConfig
import com.stardust.autojs.core.console.ConsoleImpl
Expand Down Expand Up @@ -155,21 +156,57 @@ class ShizukuClient private constructor() : Shizuku.OnRequestPermissionResultLis
client
}

private const val APP_OPS_ALLOW = "allow"
private const val APP_OPS_DEFAULT = "default"

/**
* 解析 ShizukuUserService.runShellCommand 返回的 JSON(AbstractShell.Result):
* `{ "code": 0, "error": "...", "result": "<stdout>" }`。
*/
private fun parseShellResult(json: String): Pair<Int, String> = 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)
}

Expand All @@ -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)
}
}
Expand Down
15 changes: 9 additions & 6 deletions autojs/src/main/java/com/stardust/autojs/runtime/api/Timers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions autojs/src/main/js/v6-api/src_raw/__globals__.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
21 changes: 12 additions & 9 deletions common/src/main/java/com/stardust/app/GlobalAppContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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
}
}
Loading