Skip to content

有害应用警告

追踪 Android 17 有害应用警告的权限入口、用户状态、启动拦截、Intent 恢复和持久化。

基于android-17.0.0_r1
AndroidPMSHarmfulAppWarningActivityStartInterceptorPackageUserState

有害应用警告 ​

本文面向已经读过 组件标签图标覆盖、PackageStateMutator 事务提交 和 PackageFreezer 并发与解冻 的读者。本文只讨论 PMS 保存的 harmful-app warning 如何在启动前变成用户可见的确认页,不讨论扫描器如何判定 APK 恶意,也不把 warning 与 suspension、distraction restriction 混为一谈。

1. 警告的所有权 ​

源码文件:

  • frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
  • frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
  • frameworks/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。

java
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。

java
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。

java
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。

java
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 不写属性。

java
if (ustate.getHarmfulAppWarning() != null) {
    serializer.attribute(null, ATTR_HARMFUL_APP_WARNING,
            ustate.getHarmfulAppWarning());
}

写盘是异步调度的。系统在写盘前重启时,启动拦截可能回到旧 warning;这与 PackageState mutation 已成功并不矛盾。

9. 与其他限制的区别 ​

机制状态启动行为消费者
harmful warning每 user 一段文本替换为警告 ActivityActivityStartInterceptor
distraction speedbumpbitmask可选确认页ActivityStartInterceptor
suspension多 suspender 参数通常阻止启动PMS/Activity 相关路径
disabled stateenabled/component 集合resolver 排除组件Computer/ComponentResolver

同一个启动可能先经过多种 interceptor;命中顺序由 ActivityStartInterceptor 的调用顺序决定。warning Activity 自身不会再次把原始包拦截成 warning,因为当前 mAInfo 已被替换为警告组件。

10. 失败定位 ​

现象检查点解释
设置 warning 抛 SecurityExceptioncross-user、UID、SET_HARMFUL_APP_WARNINGS两层权限均需满足
读取抛 Unknown packagegetPackageStateInternal包不存在,不是无 warning
有 warning 但未弹页ActivityStartInterceptor 调用顺序、warning Activity resolve读取异常或警告 Activity 不可解析会放行
确认后原 Activity 未恢复one-shot IntentSender、Activity 可见性sender 只能消费一次,后台启动还受其他策略限制
user 0 有、user 10 无PackageUserState userIdwarning 不跨用户共享
重启后 warning 消失restrictions 写盘调度内存 mutation 与持久化时机不同

11. 源码练习 ​

  1. 从 setHarmfulAppWarning 追到 interceptHarmfulAppIfNeeded,标出写权限、user state、snapshot 和启动替换的边界。
  2. 构造 warning 为 null 的清除请求,判断 XML 属性、读取返回值和 ActivityStartInterceptor 行为。
  3. 对比 harmful warning 与 distraction speedbump,说明两者如何都使用 IntentSender,但触发来源和状态字段不同。
  4. 给定 user 0 警告已持久化、user 10 没有警告,分别从 PMS XML、ComputerEngine 参数和 ActivityStartInterceptor userId 验证结果。