有害应用警告
本文面向已经读过 组件标签图标覆盖、PackageStateMutator 事务提交 和 PackageFreezer 并发与解冻 的读者。本文只讨论 PMS 保存的 harmful-app warning 如何在启动前变成用户可见的确认页,不讨论扫描器如何判定 APK 恶意,也不把 warning 与 suspension、distraction restriction 混为一谈。
1. 警告的所有权
源码文件:
frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.javaframeworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.javaframeworks/base/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
warning 文本保存在 PackageUserStateImpl,因此它属于 (packageName, userId),不是 APK 全局属性。PMS 负责授权、写入和持久化;ActivityTaskManager 的 ActivityStartInterceptor 负责在真正启动 Activity 前消费它。
2. 写入权限
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;符号:IPackageManagerImpl.setHarmfulAppWarning。
final int callingUid = Binder.getCallingUid();
final int callingAppId = UserHandle.getAppId(callingUid);
final Computer snapshot = snapshotComputer();
snapshot.enforceCrossUserPermission(callingUid, userId,
true /* requireFullPermission */, true /* checkShell */,
"setHarmfulAppInfo");
if (!PackageManagerServiceUtils.isSystemOrRoot(callingAppId)
&& snapshot.checkUidPermission(
SET_HARMFUL_APP_WARNINGS, callingUid)
!= PERMISSION_GRANTED) {
throw new SecurityException("Caller must have SET_HARMFUL_APP_WARNINGS");
}检查分成两层:调用者能否操作目标 user,以及是否为 system/root 或持有专用 SET_HARMFUL_APP_WARNINGS 权限。shell 只有在 cross-user 检查允许时才能继续,不能因为是 shell 自动绕过 warning 权限。
3. 状态提交
PMS 把 CharSequence 转成 String,通过按包 mutation 写入 user state;传入 null 表示清除 warning。
PackageStateMutator.Result result = commitPackageStateMutation(
null, packageName, packageState ->
packageState.userState(userId)
.setHarmfulAppWarning(
warning == null ? null : warning.toString()));
if (result.isSpecificPackageNull()) {
throw new IllegalArgumentException(
"Unknown package: " + packageName);
}
scheduleWritePackageRestrictions(userId);mutation 成功只代表内存 user state 改变;scheduleWritePackageRestrictions 负责稍后写盘。调用者不能在 setter 返回后假定 ActivityStartInterceptor 已经读取到新 warning,尤其是在已有 Computer snapshot 的线程中。
4. 读取边界
源码文件:frameworks/base/services/core/java/com/android/server/pm/ComputerEngine.java;符号:getHarmfulAppWarning。
读取接口执行与写入相同的 cross-user 和 permission 检查。包不存在会抛 IllegalArgumentException,而不是返回 null;包存在但没有 warning 才返回 null。
PackageStateInternal packageState =
getPackageStateInternal(packageName);
if (packageState == null) {
throw new IllegalArgumentException(
"Unknown package: " + packageName);
}
return packageState.getUserStateOrDefault(userId)
.getHarmfulAppWarning();这一区分对调试很重要:null warning 表示当前 user 没有警告;异常表示包身份查询失败或调用者没有权限。
5. 启动拦截
源码文件:frameworks/base/services/core/java/com/android/server/wm/ActivityStartInterceptor.java;符号:interceptHarmfulAppIfNeeded。
启动拦截在 lock-task、profile 等其他拦截之后检查 warning。读取异常或 warning 为 null 时返回 false;有文本时创建一次性、不可变的 IntentSender 保存原始启动,再把当前 intent 替换为 HarmfulAppWarningActivity。
CharSequence warning;
try {
warning = mService.getPackageManager()
.getHarmfulAppWarning(mAInfo.packageName, mUserId);
} catch (RemoteException | IllegalArgumentException ex) {
return false;
}
if (warning == null) {
return false;
}
IntentSender target = createIntentSenderForOriginalIntent(
mCallingUid, FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT | FLAG_IMMUTABLE);
mIntent = HarmfulAppWarningActivity.createHarmfulAppWarningIntent(
mServiceContext, mAInfo.packageName, target, warning);拦截器随后以真实 calling pid/uid 重新解析 warning Activity。warning 本身不修改组件 enabled state,也不改变 package visibility;它只是把一次启动换成安全提示流程。
6. Intent 恢复
HarmfulAppWarningActivity 得到原始 Intent 的 IntentSender。用户确认后,系统发送该 sender,恢复原始启动;用户取消则启动结束。因为 sender 使用 FLAG_ONE_SHOT | FLAG_IMMUTABLE,它不能被重复消费或被中途改写。
这条恢复路径解释了为什么 ActivityStartInterceptor 能返回 true 却不丢失原始启动参数:原始 intent、calling UID 和必要 flags 被封装在 sender,而不是存入 PackageManager 状态。
7. 用户隔离
同一包可以在 user 0 有 warning、在 user 10 没有 warning。ActivityStartInterceptor 传入自己的 mUserId,PMS 读取对应 PackageUserState;warning 不会跨 user 自动传播。
如果应用被卸载并重新安装,普通安装/状态重建路径会重新创建 user state;旧 warning 是否保留取决于卸载是否使用 KEEP_DATA 以及 PackageUserState 重建逻辑,不能只根据包名推断。
8. 持久化
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java。
非空 warning 写入 user package restrictions 的 harmfulAppWarning 属性,读取时恢复到 PackageUserStateImpl;null 不写属性。
if (ustate.getHarmfulAppWarning() != null) {
serializer.attribute(null, ATTR_HARMFUL_APP_WARNING,
ustate.getHarmfulAppWarning());
}写盘是异步调度的。系统在写盘前重启时,启动拦截可能回到旧 warning;这与 PackageState mutation 已成功并不矛盾。
9. 与其他限制的区别
| 机制 | 状态 | 启动行为 | 消费者 |
|---|---|---|---|
| harmful warning | 每 user 一段文本 | 替换为警告 Activity | ActivityStartInterceptor |
| distraction speedbump | bitmask | 可选确认页 | ActivityStartInterceptor |
| suspension | 多 suspender 参数 | 通常阻止启动 | PMS/Activity 相关路径 |
| disabled state | enabled/component 集合 | resolver 排除组件 | Computer/ComponentResolver |
同一个启动可能先经过多种 interceptor;命中顺序由 ActivityStartInterceptor 的调用顺序决定。warning Activity 自身不会再次把原始包拦截成 warning,因为当前 mAInfo 已被替换为警告组件。
10. 失败定位
| 现象 | 检查点 | 解释 |
|---|---|---|
| 设置 warning 抛 SecurityException | cross-user、UID、SET_HARMFUL_APP_WARNINGS | 两层权限均需满足 |
| 读取抛 Unknown package | getPackageStateInternal | 包不存在,不是无 warning |
| 有 warning 但未弹页 | ActivityStartInterceptor 调用顺序、warning Activity resolve | 读取异常或警告 Activity 不可解析会放行 |
| 确认后原 Activity 未恢复 | one-shot IntentSender、Activity 可见性 | sender 只能消费一次,后台启动还受其他策略限制 |
| user 0 有、user 10 无 | PackageUserState userId | warning 不跨用户共享 |
| 重启后 warning 消失 | restrictions 写盘调度 | 内存 mutation 与持久化时机不同 |
11. 源码练习
- 从
setHarmfulAppWarning追到interceptHarmfulAppIfNeeded,标出写权限、user state、snapshot 和启动替换的边界。 - 构造 warning 为 null 的清除请求,判断 XML 属性、读取返回值和 ActivityStartInterceptor 行为。
- 对比 harmful warning 与 distraction speedbump,说明两者如何都使用 IntentSender,但触发来源和状态字段不同。
- 给定 user 0 警告已持久化、user 10 没有警告,分别从 PMS XML、ComputerEngine 参数和 ActivityStartInterceptor userId 验证结果。
