问题现象
在开发 App Ops 管理工具时,你可能会遇到一个诡异的现象:
· 剪贴板、通知这类不弹出系统权限弹窗的权限,修改后提示“成功”,刷新一看,状态又变回去了。
· 相机、定位这类会弹出系统权限弹窗的敏感权限,反而能正常修改。
· 更奇怪的是,appops get 命令明明返回了正确的状态,但你的工具里显示的却是旧的值。
如果你也卡在这个问题上,恭喜你,你已经踩到了 Android AppOps 的 UID 模式 这个大坑。
根本原因
AppOps 的双模式机制
Android 的 App Ops 系统中,每个操作(operation)实际上有两种模式:
- Package mode(包模式):通过 appops set
- Uid mode(UID 模式):通过 appops set --uid
Uid mode 的优先级高于 Package mode。也就是说,当 Uid mode 为非默认值时,Package mode 会被完全忽略。
用 appops get READ_CLIPBOARD: allow; time=2025-05-13 12:00:00 Uid mode: READ_CLIPBOARD: ignore
这里有两个问题需要解决:
· 读取问题:你解析输出时,需要拿到真正生效的 UID mode 状态,而不是低优先级的 Package mode 状态。
· 写入问题:你执行修改时,需要用 --uid 参数直接操作 UID mode,否则改的是 Package mode,系统根本不理会。
为什么敏感权限"反而"能改?
敏感权限(如 CAMERA、RECORD_AUDIO)通常有对应的运行时权限(Runtime Permission)。许多工具在处理敏感权限时,会联动调用 pm grant 或 cmd permission set-permission-flags,这会间接触发系统重新评估权限状态,有时会顺带覆盖 App Ops。这就造成了"敏感权限能改,非敏感权限改不了"的假象。
解决方案
- 正确解析 appops get 的输出
不要简单地按行解析,而是要识别 Uid mode: 开头的行,优先取它的值。
错误做法:简单去重,只保留第一次出现的行。这会把低优先级的 Package mode 当成最终状态。
正确做法:
· 扫描所有行。
· 如果遇到 Uid mode: 行,解析出操作名和状态,存入一个临时 Map。
· 如果遇到普通行(操作名: 状态),只在 Map 中没有对应 UID 记录时才存入。
· 最终用这个 Map 构建显示列表。
参考代码(位于 PermManagerEngine.kt 的 scanFullPermissions 方法):val tempMap = mutableMapOf<String, TempState>() for (line in output.lines()) { if (line.isBlank()) continue if (line.startsWith("Uid mode:")) { // 解析 Uid mode 行 val content = line.removePrefix("Uid mode:").trim() val colonIndex = content.indexOf(':') if (colonIndex == -1) continue val opName = content.substring(0, colonIndex).trim() val stateRaw = content.substring(colonIndex + 1).trim() val (stateStr, timeStr) = parseFullAppOpsState(stateRaw) // UID 模式优先级最高 tempMap[opName] = TempState(stateStr, timeStr, isUid = true) } else { val parts = line.split(":", limit = 2) if (parts.size < 2) continue val opName = parts[0].trim() val stateRaw = parts[1].trim() val (stateStr, timeStr) = parseFullAppOpsState(stateRaw) // 只在没有 UID 记录时才添加 if (!tempMap.containsKey(opName)) { tempMap[opName] = TempState(stateStr, timeStr, isUid = false) } } }
- 使用 --uid 参数执行修改
对于非敏感权限(即在 opToRuntimePermission 映射表中没有对应条目的权限),使用带 --uid 参数的命令直接操作 UID 模式,并加入回退机制。
参考代码(位于 AppPermissionDetailActivity.kt 的 trySetWithUidFallback 方法):private suspend fun trySetWithUidFallback(opName: String, mode: String): Boolean { // 1. 首选:带 --uid 参数 val cmd1 = "cmd appops set --uid $packageName $opName $mode" if (execAndCheck(cmd1)) return true // 2. 次选:不带 --uid 参数 val cmd2 = "cmd appops set $packageName $opName $mode" if (execAndCheck(cmd2)) return true // 3. 备选:强制使用 ignore 模式 val cmd3 = "cmd appops set $packageName $opName ignore" if (execAndCheck(cmd3)) return true // 4. 最后手段:带 --uid 的 ignore val cmd4 = "cmd appops set --uid $packageName $opName ignore" return execAndCheck(cmd4) }
- 敏感权限走联动流程
对于有运行时权限映射的敏感权限(如 CAMERA → android.permission.CAMERA),继续使用现有的联动方法 setFullPermission(),它已经内置了 --uid 参数和回退。
总结
问题 原因 解决方法
界面状态与实际不符 解析去重时保留了低优先级的 Package mode 优先保留 UID mode
修改非敏感权限不生效 只用 appops set 改了低优先级的 Package mode 使用 appops set --uid 修改 UID mode
修改失败没有反馈 没有检测错误 检查命令输出是否包含 Error
某些模式不支持 系统只接受 allow / ignore 失败自动回退到 ignore
只要处理好以上几点,你的 App Ops 管理工具就能像那些成熟方案一样,稳定修改所有权限。



Comments NOTHING