ADB安装与权限管理
执行 adb install app.apk 时,ADB 并不负责解析 manifest、校验签名或决定应用 UID。ADB 的职责是选择传输模式、把安装参数和 APK 字节送到设备,并消费安装结果;真正拥有安装状态的是设备上的 Package Manager 和 Package Installer session。
同样,adb install -g app.apk 与 adb shell pm grant PACKAGE PERMISSION 都可能改变运行时权限,但它们进入系统的时机、作用范围和失败条件不同:-g 是 session 的安装 flag,pm grant 是针对已安装包、指定权限和用户的独立请求。
建议先阅读 ADB架构 和 ADB shell机制。本文从 packages/modules/adb 与 frameworks/base 的真实入口展开安装、卸载和调试场景中的运行时权限,不展开 APK 签名格式、Package Manager 全部扫描过程或应用内权限 API。若要观察安装前后的日志证据,可继续阅读 ADB logcat进阶。
1. 安装命令
adb install app.apk 至少跨越四层:
| 层 | 主要入口 | 拥有的状态 | 直接消费者 |
|---|---|---|---|
| host 命令层 | client/commandline.cpp::adb_commandline | argv、目标 transport | install_app |
| host 安装器 | client/adb_install.cpp | 安装模式、本地文件、fallback | ADB service fd |
| shell 命令层 | PackageManagerShellCommand | install options、session id、用户 | PackageInstaller |
| 安装系统 | PackageInstallerService / PackageInstallerSession | staged files、校验、commit、结果 | PMS、Permission Manager、应用数据目录 |
“ADB 返回 Success”意味着设备安装系统完成了对应 session,并不意味着应用一定能启动、所有权限都已授予,或所有用户都已安装该包。后续状态还受用户、组件启用状态、运行时权限和应用自身逻辑影响。
2. host 安装模式
2.1 安装模式差异
client/adb_install.cpp 定义 INSTALL_PUSH、INSTALL_STREAM、INSTALL_INCREMENTAL 和默认选择。它们描述 APK 字节怎样到设备,不代表设备侧存在四个不同的包管理器。
| host 模式 | 字节路径 | 设备侧入口 | 主要特点 |
|---|---|---|---|
| push | 先同步到 /data/local/tmp | pm install <path> | 兼容旧设备,产生临时文件 |
| streaming | 本地 APK 直接写入远端 fd | cmd package install -S SIZE - | 不要求完整 APK 先落入临时路径 |
| incremental | 先提供签名/索引和部分块 | package install-incremental | Package Manager 可在按需读取块的同时推进安装 |
| fast deploy | 计算并传送 patch | 安装 agent + package command | 面向开发迭代,不适用于所有包类型 |
best_install_mode() 查询 transport feature。设备支持 cmd 时优先 streaming,否则退回 push。incremental 还会检查 abb_exec feature、host 环境变量和设备全局设置;显式 --incremental 失败时不会自动假装成功,默认选择 incremental 时才可以按计算出的 fallback 转入普通模式。
2.2 实现入口
--streaming、--no-streaming、--incremental、--fastdeploy 首先由 host ADB 消费;-r、-t、-d、-g、--user 等参数会继续传给设备 Package Manager。
这一区分可以解释为什么某些错误由 host 立即输出,例如“文件不存在”或“设备不支持 streaming”;另一些错误要等设备返回,例如签名不匹配、版本降级被拒绝、用户不存在或存储不足。
安装参数属于不同处理层
阅读命令帮助时要问“谁消费这个参数”。改变 APK 传输方式的参数通常属于 host ADB;改变 session policy、目标用户和包校验规则的参数属于设备 Package Manager。
3. 流式安装
3.1 host约束
install_app_streamed() 先检查文件扩展名、设备是否支持 APEX,再对本地文件执行 stat() 和 open()。随后构造 package command,并覆盖用户可能传入的 -S:
package install ... -S <实际文件大小>streaming 输入使用 - 表示 stdin。设备侧 PackageManagerShellCommand::doRunInstall() 明确要求 stdin 安装必须给出正数大小,否则 session 无法提前规划空间和数据边界。
3.2 安装命令入口
host 支持 abb_exec 时,参数以结构化 argv 形式发送;不支持时,ADB 拼接 exec:cmd package 并使用 escape_arg() 处理 shell 参数。两条路径最终都进入设备上的 package shell command,但前者避免把参数重新交给 shell 字符串解析。
send_command() 返回远端 fd,copy_to_file() 把本地 APK 全部写入该 fd。写完后 host 通过 read_status_line() 等待设备返回文本;只有以 Success 开头才被视为安装成功。
源码文件:packages/modules/adb/client/adb_install.cpp
相关函数/类型:install_app_streamed
struct stat sb;
if (stat(file, &sb) == -1) {
perror_exit("failed to stat %s", file);
}
unique_fd local_fd(adb_open(file, O_RDONLY | O_CLOEXEC));
if (local_fd < 0) {
perror_exit("failed to open %s", file);
}
std::vector<std::string> cmd_args = {use_abb_exec ? "package" : "exec:cmd package"};
// ... 复制除 APK 路径外的参数
cmd_args.push_back("-S");
cmd_args.push_back(android::base::StringPrintf("%" PRIu64,
static_cast<uint64_t>(sb.st_size)));
unique_fd remote_fd = send_command(cmd_args, &error);
if (remote_fd < 0) error_exit("connect error for write: %s", error.c_str());
if (!copy_to_file(local_fd.get(), remote_fd.get())) {
perror_exit("failed to install: copy_to_file: %s", file);
}
read_status_line(remote_fd.get(), buf, sizeof(buf));
if (strncmp("Success", buf, 7) != 0) {
error_exit("failed to install %s: %s", file, buf);
}host 在发送字节前用 stat 得到大小,并把 -S 放在参数末尾覆盖用户输入;这正是设备侧 streaming session 能预先知道输入边界的原因。Success 只来自设备命令的最终状态行,不能把本地 copy_to_file 成功 误当作安装提交成功。
3.3 Success时间点
普通 APK session 的 doRunInstall() 在 doCommitSession() 成功后输出 Success。如果是 staged session,命令还可能等待 session ready,并提示重启后应用;因此“命令成功”与“新代码已在当前进程运行”不是同一个时间点。
4. push install
旧式 install_app_legacy() 从参数中找到最后一个 APK,把目标路径改成:
/data/local/tmp/<apk basename>随后 do_sync_push() 先完成文件同步,pm_command() 再执行设备安装,最后 delete_device_file() 删除临时 APK。push 成功只代表传输完成;pm install 仍可能在签名、版本、ABI、空间或策略阶段失败。
临时路径属于 shell 可写的中转区,不是应用最终安装目录。Package Manager 会从中读取包并建立自己的 code/data 状态。调试时不要把 /data/local/tmp/app.apk 的存在当作“应用已安装”,也不要把它与 /data/app/... 的最终 code path 混为一谈。
4.1 push三个结果
| 阶段 | 成功标准 | 失败后的状态 |
|---|---|---|
| sync push | 临时 APK 完整写入 | 不应调用 Package Manager;可能需要清理残留 |
pm install | session commit 返回成功 | 包状态不应部分替换,失败信息由 PMS 给出 |
| 临时清理 | 中转文件被删除 | 安装结果可能成功,但留下占用空间的临时文件 |
streaming 省去了显式临时 APK,但没有绕过 Package Installer session,也不会降低签名和权限检查。
5. 安装session
5.1 doRunInstall()
Android 17 的 PackageManagerShellCommand::doRunInstall() 首先检查:
sys.boot_completed是否为 true;- 目标用户是否存在;
- stdin 输入是否提供大小;
- APEX 是否错误地带有多个 split;
- streaming、文件路径和 split 组合是否合法。
通过前置检查后,代码调用 doCreateSession() 得到 session id,再写入一个或多个文件,最后 doCommitSession()。局部变量 abandonSession 初始为 true,只有 commit 成功才改为 false;任何提前返回都在 finally 中尝试 abandon。
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
相关函数/类型:doRunInstall
if (!SystemProperties.getBoolean("sys.boot_completed", false)) {
pw.println("Error: device is still booting.");
return 1;
}
if (fromStdIn && params.sessionParams.sizeBytes == -1) {
pw.println("Error: must either specify a package size or an APK file");
return 1;
}
final int sessionId = doCreateSession(params.sessionParams,
params.installerPackageName, params.userId);
boolean abandonSession = true;
try {
if (isStreaming) {
if (doAddFiles(sessionId, args, params.sessionParams.sizeBytes, isApex,
installArchived) != PackageInstaller.STATUS_SUCCESS) {
return 1;
}
}
// ... 非 streaming 路径写入文件
if (doCommitSession(sessionId, false /*logSuccess*/)
!= PackageInstaller.STATUS_SUCCESS) {
return 1;
}
abandonSession = false;
} finally {
if (abandonSession) {
try {
doAbandonSession(sessionId, false /*logSuccess*/);
} catch (Exception ignore) {
}
}
}session 是设备侧事务 owner:前置检查失败时根本没有 session;session 创建后任何写入或 commit 失败都会 沿 finally abandon。读者可以用不存在的用户或缺少 -S 的 stdin 安装分别触发两个前置错误,断言不会得到 成功 session;用一个合法测试 APK 断言 commit 成功后不再 abandon。这个实验覆盖 shell command 的状态边界, 不外推到所有签名、ABI 或厂商安装策略。
5.2 commit 语义
PackageInstallerSession 持有 session 生命周期,commit 后还要进入包解析、签名和版本关系校验、空间与 ABI 处理、用户安装状态更新等路径。本文不展开 PMS 全部内部流程,但排错必须知道这些检查属于设备端,而不是 ADB 字节复制阶段。
常见 INSTALL_FAILED_* 可以按 owner 分组:
| 错误方向 | 典型原因 | 应查看的设备侧边界 |
|---|---|---|
| 包格式 | APK 无效、split 缺失、manifest 不兼容 | parsing / session validation |
| 更新关系 | 签名不同、版本降级、shared UID 不兼容 | reconcile / signing / version policy |
| 资源环境 | 空间不足、ABI 不支持、共享库缺失 | storage / native library / dependency |
| 安全策略 | verifier 拒绝、用户限制、安装来源策略 | verification / user policy |
| session | 大小不符、write 中断、session 不存在 | PackageInstallerSession |
6. 单 APK
6.1 单 APK
adb install app.apk对于普通完整 APK,host 建立一次安装命令,设备创建一个完整安装 session。-r 在 Android 17 shell parser 中保持替换现有应用的默认行为;使用 -R 才关闭 replaceExisting。更新仍必须满足签名、version code 和包名关系。
6.2 splitAPK约束
adb install-multiple base.apk split_config.arm64_v8a.apk split_config.zh.apkinstall_multiple_app_streamed() 计算所有文件的总大小,先执行 install-create,再为每个 APK 执行 install-write,最后 install-commit。任一文件 stat、open、写入或结果读取失败,代码把 final command 改为 install-abandon。
split 不是独立应用。base 与配置 split 必须属于同一包、签名一致,并满足 split 依赖。只安装一个配置 split 不能生成可运行应用。
6.3 multi-package
adb install-multi-package 可把多个 APK/APEX package 组织在一个 multi-package parent session 下。源码先创建带 --multi-package 的父 session,再创建子 session 并添加到 parent;commit 的目标是整组事务。
它与 install-multiple 的区别是:前者处理多个 package,后者处理一个 package 的多个 APK。若命令中出现 APEX,host 会要求 staged 语义;旧式 push 设备不支持 multi-package。
不要交换 multiple 与 multi-package
install-multiple 解决一个应用的 base/split 组合;install-multi-package 解决多个应用或模块的原子组合。选错命令会让 session 的包边界和提交语义完全不同。
7. Session状态
incremental install 并非“压缩后更快地 push”。client/incremental.cpp 构造 package install-incremental 请求,建立 Incremental Server 所需的数据索引和 block 服务。签名相关文件先传输,Package Manager 可以在文件块尚未全部到达时推进安装,并在运行时按需请求块。
install_app_incremental() 可以通过 --wait 等待 server 进程退出;默认模式失败时,host 可能退回 streaming 或 push。显式 --incremental 表达的是“必须使用 incremental”,不会静默改变为其他模式。
判断 incremental 的成功至少要区分:
- install command 已取得足够数据并返回成功;
- Incremental Server 仍在提供缺失块;
- 应用首次启动触发的块读取是否完成。
因此安装命令结束时间与所有 APK 字节落盘时间可能不同。排错时要记录是 package session 失败、Incremental Server 退出,还是运行时 block 请求无法满足。
8. 安装选项
8.1 -r
| 选项 | 设置的 session 意图 | 仍然不能绕过 |
|---|---|---|
-r | 替换已有应用;Android 17 shell 默认允许 replace | 签名不一致、包名不同 |
-d | 请求允许 version code 降级 | 产品和 debuggable 策略、签名关系 |
-t | 允许 testOnly 包 | 其他安装校验与用户策略 |
这些选项不是“强制成功”。它们只设置 Package Manager 识别的 install flags,后续校验仍可以拒绝 session。
8.2 --user
PackageManagerShellCommand 的 install params 默认面向系统用户语义,并通过 translateUserId() 处理明确的 --user。设备在安装前检查用户是否存在。一个 package 的代码可能在系统中只保存一份,但安装/启用状态、数据目录与权限授予都具有用户维度。
因此下面两个问题必须分开:
- package code 是否已进入系统;
- package 是否为指定用户安装且具有该用户的数据和权限状态。
pm install-existing --user USER PACKAGE 是把系统已有 package 启用/安装给用户,不会重新从 host 传 APK。
8.3 instant、APEX
--instant 设置 instant app session;--full 明确选择完整应用。--apex 把 session 标记为 APEX,APEX 通常涉及 staged 和重启应用,不能与普通 split 规则等同。ADB streaming 代码也明确拒绝设备不支持 APEX feature 的目标,并拒绝 fast deploy APEX。
9. -g与运行时权限
9.1 -g
adb install -g app.apkADB host 不会枚举权限;它把 -g 传给设备。PackageManagerShellCommand::makeInstallParams() 将其转换为 INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS。Package Installer 在安装过程中尝试授予该应用请求且符合条件的运行时权限。
“all” 的边界是应用 manifest 请求的、可由运行时授权机制授予的权限。它不意味着获得 signature、privileged、role、AppOp 或 SELinux 能力,也不能授予应用没有声明的权限。
9.2 pmgrant
adb shell pm grant --user 0 com.example.app android.permission.CAMERA
adb shell pm revoke --user 0 com.example.app android.permission.CAMERArunGrantRevokePermission() 解析用户、package 和 permission,确认包存在,然后调用 PermissionManagerService.grantRuntimePermission() 或 revokeRuntimePermission()。其状态键至少包含 package、permission 和 user;不同用户的授权不能由一次没有正确用户范围的观察替代。
9.3 user-set
pm set-permission-flags 和 clear-permission-flags 可处理 user-set、user-fixed、review-required 等 shell 支持的 flags。flag 描述权限状态如何形成、系统是否应再次询问或允许策略变化;它与 granted/denied 位不是同一字段。
调试时只看 granted=true 可能不够,还要判断是否存在固定状态、兼容撤销或策略限制。反过来,清除 flag 也不会自动把 denied 改成 granted。
10. 权限边界
pm grant 的命令入口并不等于 shell 可以授予任意权限。Permission Manager 会检查:
- package 是否请求了该 permission;
- permission 是否属于可运行时授予的类型;
- package 的 target SDK 和权限定义是否满足条件;
- 用户、设备策略和 fixed flags 是否允许修改;
- 权限是否需要 signature、privileged、role 或其他专属 owner。
例如 signature permission 依据签名关系授予,privileged permission 受系统映像和 allowlist 管理,AppOp 是另一套操作控制状态,SELinux 决定进程域能否访问内核/服务资源。它们都不能由一次 pm grant 统一解决。
不要把权限命令当成安全边界绕过工具
ADB shell 权限管理用于可控测试和诊断。signature、privileged、设备策略与 SELinux 限制属于不同安全层,强行修改系统状态可能导致测试结论失真,也可能破坏多用户和企业策略。
11. 卸载与数据
11.1 adbuninstall
设备支持 streamed command 时,host 构造 cmd package uninstall;旧设备使用 pm uninstall。最终删除 package 和用户状态的是 Package Manager,不是 ADB 删除 /data/app 目录。
11.2 -k特殊保护
adb_install.cpp 对 adb uninstall -k 主动拒绝,并提示若确实要保留 data/cache,应显式运行设备 shell 中的 cmd package uninstall -k。原因是保留数据后,普通 ADB 命令没有独立接口清理这份残留状态;要恢复访问通常必须用同签名包重新安装后再完整卸载。
这说明 -k 不是“安全卸载”。它保留的是与 package 身份和签名关系相关的数据。若后续 APK 签名不同,系统不能把旧数据交给新应用。
11.3 多用户卸载
针对某个用户卸载可能只改变该用户的 installed 状态;系统仍可能为其他用户保留包。判断卸载结果时,应同时确认 package code 是否还存在,以及目标用户的 package 状态是否已删除。
12. 安装失败
| 现象 | 最可能的层 | 首个源码入口 | 下一步证据 |
|---|---|---|---|
| host 提示找不到 APK | host installer | install_app_streamed / legacy | 路径、后缀、stat/open |
| 不支持 streaming/incremental | feature/模式选择 | calculate_install_mode | transport features、是否显式强制模式 |
| push 失败 | sync transport | do_sync_push | transport、空间、临时目录 |
must specify package size | package shell | doRunInstall | stdin 与 -S 是否匹配 |
user doesn't exist | package shell/user manager | doRunInstall | --user、用户列表 |
INSTALL_FAILED_UPDATE_INCOMPATIBLE | PMS reconcile | ReconcilePackageUtils | 包名、签名、已有版本 |
INSTALL_FAILED_VERSION_DOWNGRADE | version policy | PackageManagerServiceUtils | versionCode、-d、debuggable 条件 |
| grant 失败 | Permission Manager | runGrantRevokePermission | 是否请求、权限类型、用户、fixed flags |
| install-multiple 写到一半失败 | session write | install_multiple_app_streamed | 哪个 split、大小、session abandon |
12.1 安装选项排查
把所有选项一起添加会丢失证据:你无法判断原始失败是签名、版本、testOnly 还是权限状态。更可靠的方法是保留原命令和完整 Failure,然后只改变一个有源码依据的条件。
12.2 安装失败测试
一次完整验证至少包含:
- 安装命令返回成功;
pm path PACKAGE能定位 code path;- 指定用户能查询到 package;
- versionCode、签名预期和 split 列表正确;
- 运行时权限状态符合测试前置条件;
- 启动目标组件并观察实际进程行为。
这些检查分别验证 package code、用户状态、权限状态和运行状态,不能互相替代。
13. 安装测试
13.1 权限测试
packages/modules/adb/test_device.py::test_install_argument_escaping 构造包含特殊字符的安装参数,验证 ADB 经 shell command 传递参数时不会错误分词。这能支持 escape_arg() 路径的行为,但不能证明任意 APK 都通过 Package Manager 校验。
ADB 的设备测试还覆盖实际 install/uninstall 场景。这类测试能验证 client、transport 和设备 package service 的组合,但结果仍取决于测试 APK 与测试目标的构建属性。
13.2 安装结果测试
本文的重要边界也直接由失败分支证明:
| 输入 | 源码断言 | 证明的结论 |
|---|---|---|
streaming stdin 没有 -S | doRunInstall 返回 error | 大小是协议/session 前置条件 |
| 目标 user 不存在 | 安装前返回 Failure | 用户校验先于 session 写入 |
| split write 失败 | final command 选择 abandon | 多 APK 不应提交半成品 session |
| 显式 incremental 不支持 | host 直接报错 | 强制模式不会静默 fallback |
pm grant 包不存在 | 输出 package not found | 权限状态依附已安装 package/user |
adb uninstall -k | host 拒绝并提示显式 shell 命令 | 数据保留需要使用者明确承担恢复边界 |
测试和失败分支共同告诉我们“代码允许什么、拒绝什么”。它们不能替代目标 APK 的签名、ABI、空间和产品策略验证。
14. 源码路径
如果要在 Android 17 源码中复现本文,建议按下面的顺序阅读:
packages/modules/adb/client/commandline.cpp:确认install、install-multiple、install-multi-package、uninstall如何分派;client/adb_install.cpp::install_app():确认 mode、fallback 和 fast deploy 的 owner;install_app_streamed()与install_app_legacy():比较 fd streaming 与临时文件 push;install_multiple_app_streamed():跟踪 create、write、commit/abandon;client/incremental.cpp::install():理解 Incremental Server 与 package install-incremental;frameworks/base/.../PackageManagerShellCommand::doRunInstall():确认用户、输入、session 和 finally 清理;makeInstallParams():把-r/-d/-t/-g/--user映射为 session 参数;runGrantRevokePermission()与PermissionManagerService:确认权限状态的 package/user 维度。
这条路径先建立数据传输,再进入设备事务,最后处理权限状态,便于逐层验证,不需要一开始进入庞大的 PackageManagerService 全部代码。
15. 分层实验
15.1 包身份验证
安装测试 APK 前,先从 APK 元数据记录 package name、versionCode、min/target SDK、请求权限和签名摘要。这个顺序看似慢,却能避免把“更新失败”误判成“ADB 传输失败”:-r 只表达 replace 意图,不能使不同签名的包成为合法更新。
一次有边界的实验可以分成四次安装:
adb install app.apk
adb install -r app.apk
adb install -r -d app.apk
adb install -r -t app-test.apk每次只增加一个条件,并保存完整输出。第一条验证全新安装;第二条验证同签名更新;第三条只在 debug/产品策略允许时观察降级分支;第四条验证 testOnly 包的允许条件。若第二条失败而第三条成功,证据指向 version policy,而不是文件传输。
15.2 split session
对 split APK,故意让其中一个文件路径错误,可以观察 host 在 install-write 阶段失败并发出 install-abandon。然后使用正确文件重试,并在成功后检查包的 split 列表。这个实验能够证明“部分写入不会直接变成已安装应用”,也能帮助区分 host 本地 stat/open 失败和设备 session write 失败。
不要只看最终的应用图标。启动器缓存、当前用户和组件启用状态都可能让 UI 观察滞后;pm path、包信息和 session 输出更接近 Package Manager 的真实状态。
15.3 权限实验约束
对一个声明了运行时权限的调试包,先查询用户 0 的权限,再在用户 0 执行 grant/revoke:
adb shell cmd package list packages --user 0 | grep com.example.app
adb shell pm grant --user 0 com.example.app android.permission.CAMERA
adb shell pm revoke --user 0 com.example.app android.permission.CAMERA如果设备存在第二个用户,重复查询并比较两个用户的状态。一次没有 --user 的命令不能作为多用户结论;系统用户、当前用户和 shell 命令默认用户之间存在转换逻辑。若 grant 失败,先确认 manifest 请求和权限 protection level,再检查 fixed flag,而不是不断重复命令。
15.4 卸载实验
完整卸载前记录 pm path、应用数据目录和用户状态;执行普通 adb uninstall 后再次查询,确认 code 和目标用户状态均已消失。若显式使用设备 shell 的 cmd package uninstall -k 保留数据,再安装同签名包,观察旧数据是否恢复;随后执行不保留数据的卸载完成清理。
这个实验说明 -k 的影响不在 ADB 临时文件,而在 Package Manager 的用户数据状态。换签名重装失败时,失败原因是 package identity 和签名关系,而不是 -k 本身把 APK 损坏。
15.5 记录每层证据
一个可复用的安装记录至少包含:
| 层 | 记录内容 |
|---|---|
| host | 完整命令、ADB 版本、选中的 install mode、APK 本地大小 |
| transport | serial、online 状态、是否使用 abb exec、是否发生 fallback |
| session | session id、create/write/commit 或 abandon 结果 |
| package | package name、version、签名、split、code path |
| user | 目标 user、installed/enabled 状态、数据目录 |
| permission | requested、granted/revoked、flags、AppOp/策略相关现象 |
| runtime | 组件解析、进程启动和实际 API 行为 |
缺少其中任一层,都可能把上游成功误写成下游成功。例如 session commit 成功只能证明 package 系统接受了安装事务;它不证明用户 10 已安装,也不证明相机权限和 SELinux 都允许应用打开设备。
16. 状态分层
adb install 的最终心智模型可以压缩为三个相邻但独立的状态:
- 安装状态:PackageInstaller session 是否成功提交,代码与包设置是否建立;
- 用户权限状态:指定 user 下哪些 runtime permissions 被授予、撤销或固定;
- 运行状态:组件能否解析、进程能否启动、AppOp/SELinux/设备策略是否允许实际操作。
-g 只能在安装阶段请求一组符合条件的运行时权限,pm grant/revoke 只能改变 Permission Manager 允许修改的权限状态,二者都不能证明应用最终操作成功。反过来,应用启动失败也不能直接推出 APK 安装失败。
遇到问题时,从 host 文件和安装模式开始,沿 ADB service 到 Package Installer session,再检查 package/user/permission,最后进入应用运行路径。每一层都保留自己的 Success、Failure 和 cleanup,这才是 Android 17 中 ADB 安装与权限管理的完整边界。
