DEV Community

BeWaterMF
BeWaterMF

Posted on Edited on

AOSP 17 Toast 锁屏可见问题分析:WindowManagerPolicy layer 变更引入的 BUG

1. 问题描述

Android 17 中存在一个窗口层级相关的回归问题:默认文本 Toast 和持有 INTERNAL_SYSTEM_WINDOW 权限的系统应用发出的自定义 Toast 在锁屏后仍然可见,而 Android 16 及更早版本中,锁屏时所有 Toast 均会被 Keyguard 正确覆盖。

复现步骤:在任意应用中触发标准文本 Toast 后立即按电源键锁屏,可观察到 Toast 内容持续显示在锁屏界面上方。使用系统签名应用发出自定义 Toast 同样可复现。

2. 根因定位

2.1 相关代码位置

问题位于 frameworks/base/services/core/java/com/android/server/policy/WindowManagerPolicy.java。WindowManagerPolicy 是一个 Java interface,继承自 WindowManagerPolicyConstants。其中的 default 方法 getWindowLayerFromTypeLw 负责将窗口类型映射到 Z 轴层级,决定各窗口在屏幕上的显示顺序。

该方法的完整签名为:

img-1.png

其中 canAddInternalSystemWindow 参数反映窗口所属进程是否持有 android.Manifest.permission.INTERNAL_SYSTEM_WINDOW 权限。该参数由 WindowState.canAddInternalSystemWindow() 提供:

img-2.png

2.2 Android 16 及之前的实现

在 AOSP 16(android-16.0.0_r1)中,TYPE_TOAST 分支直接返回固定值 7,不使用 canAddInternalSystemWindow 参数:

img-3.png

TYPE_TOAST 返回的 layer 值为 7。而锁屏及通知栏对应的 TYPE_NOTIFICATION_SHADE 返回 17。由于 7 < 17,Toast 窗口在 Z 轴上位于锁屏窗口之下,锁屏时会被正确覆盖。

2.3 Android 17 的变更

在 AOSP 17(android-17.0.0_r1)中,TYPE_TOAST 分支被修改为使用 canAddInternalSystemWindow 参数进行条件判断。实际代码如下:

img-4.png

变更逻辑为:

  • 若窗口所属进程持有 INTERNAL_SYSTEM_WINDOW 权限 → 返回 27
  • 否则 → 保持原有的 7

从注释可以看出,该改动的设计意图是区分「旧式 Toast」(legacy toast,可能携带任意 View,存在触控劫持风险)和「系统托管的 Toast」(system-managed toast,安全可控)。旧式 Toast 保持低层级,系统托管的 Toast 获得更高可见性。

2.4 Layer 层级对比

变更前后的关键窗口层级如下:

Layer 窗口类型 说明
7 TYPE_TOAST(无权限) Toast(旧行为及新行为中无权限情形)
17 TYPE_NOTIFICATION_SHADE 通知面板 / 锁屏
27 TYPE_TOAST(有权限) Toast(新行为中有权限情形)

27 > 17,因此被分配 layer 27 的 Toast 窗口在 Z 序上高于锁屏窗口。

2.5 问题的直接原因

layer 27 的分配并非无条件对所有 Toast 生效,而是按窗口的实际创建进程是否满足 canAddInternalSystemWindow 条件来决定。在 Android 17 的架构下,以下两类 Toast 会命中 canAddInternalSystemWindow ? 27 : 7 的 true 分支:

2.5.1 默认文本 Toast:SystemUI 代理渲染(Android 12+)

Android 系统从 Android 12 开始强制应用使用系统的 Toast 渲染管道:当任意应用调用 Toast.makeText().show() 时,Toast 的实际渲染由 SystemUI 进程(com.android.systemui)执行。

因此在 WindowManagerPolicy.getWindowLayerFromTypeLw 中:

  1. Toast 窗口由 SystemUI 进程创建。
  2. WindowState.canAddInternalSystemWindow() 对 SystemUI 返回 true(SystemUI 持有 INTERNAL_SYSTEM_WINDOW 权限)。
  3. TYPE_TOAST 分支命中 canAddInternalSystemWindow ? 27 : 7 的 true 分支。
  4. 返回 layer 27,高于 TYPE_NOTIFICATION_SHADE 的 layer 17。

关键问题在于:权限检查的对象是 Toast 窗口的实际渲染进程(SystemUI),而非发起 Toast 请求的原始应用。无论第三方应用是否有系统权限,其文本 Toast 都会经由 SystemUI 取得 layer 27 的层级提升。

2.5.2 有 INTERNAL_SYSTEM_WINDOW 权限的应用的自定义 Toast

对于通过 setView() 设置自定义视图的 Toast,窗口仍由应用自身进程创建(不走 SystemUI 代理)。此时 canAddInternalSystemWindow 直接反映调用进程的权限:

  1. 持有 INTERNAL_SYSTEM_WINDOW 权限的系统签名应用 / privileged 应用调用 Toast.setView().show()。
  2. WindowState.canAddInternalSystemWindow() 对调用进程返回 true。
  3. TYPE_TOAST 分支命中 canAddInternalSystemWindow ? 27 : 7 的 true 分支。
  4. 返回 layer 27,高于 TYPE_NOTIFICATION_SHADE 的 layer 17。

而对于普通第三方应用发出的自定义 Toast,canAddInternalSystemWindow 返回 false,layer 保持 7,行为与 Android 16 一致——锁屏时仍被遮挡。

2.6 第二块拼图:DisplayPolicy.applyKeyguardPolicy 如何将 layer 变更兑现为可见性

2.3 节回答了「为什么 Toast 的 layer 变成了 27」,本节回答「layer 27 为什么导致锁屏可见」——后者是 DisplayPolicy.applyKeyguardPolicy() 的职责。

2.6.1 applyKeyguardPolicy —— 隐藏决策入口

DisplayPolicy.applyKeyguardPolicy() 在锁屏状态变化时遍历所有窗口,调用 canBeHiddenByKeyguard() 逐窗口判断是否应被 Keyguard 遮挡:

img-5.png

2.6.2 canBeHiddenByKeyguard —— 16 和 17 代码一致,未变更

本节关键结论:canBeHiddenByKeyguard() 在 Android 16 和 17 中的代码完全相同,并未发生任何修改。两版源码一致为:

img-6.png

TYPE_TOAST 不在 switch 的任何 case 分支中,落入 default。default 的语义为:

窗口的实际 layer 低于锁屏窗口(TYPE_NOTIFICATION_SHADE)的 layer 时,才被隐藏。

这是 Android 16 就已经存在的逻辑。

2.6.3 为什么 16 上没问题、17 上出问题了?

答案很简单:layer 变了。

版本 Toast Layer 比较 layer < 17 canBeHiddenByKeyguard 返回值 锁屏行为
Android 16 7(固定) 7 < 17 = true true Toast 被隐藏 ✅
Android 17(无权限) 7 7 < 17 = true true Toast 被隐藏 ✅
Android 17(SystemUI / 系统签名应用) 27 27 < 17 = false false Toast 不被隐藏 ❌

layer 从 7 升至 27 是唯一的变更点。canBeHiddenByKeyguard() 自身未变——它始终做 layer 比较。在 16 上比较结果是 true,在 17 上当 canAddInternalSystemWindow = true 时比较结果变为 false。

canAddInternalSystemWindow() 的实现只是成员变量 getter,在窗口创建时由进程权限决定:

img-7.png

SystemUI 进程持有 INTERNAL_SYSTEM_WINDOW 权限 → mOwnerCanAddInternalSystemWindow = true → getWindowLayerFromTypeLw 返回 27 → 27 < 17 = false → canBeHiddenByKeyguard 返回 false → Toast 穿透锁屏。

2.6.4 完整因果链

1. App 调用 Toast.makeText().show()       ↓2. SystemUI 代理渲染 Toast 窗口       ↓3. WindowManagerPolicy.getWindowLayerFromTypeLw(TYPE_TOAST, canAddInternalSystemWindow=true)   ★ 唯一变更点:AOSP 16 固定返回 7 → AOSP 17 改为 canAddInternalSystemWindow ? 27 : 7       ↓4. Toast 窗口获得 layer = 27(高于 TYPE_NOTIFICATION_SHADE 的 17)       ↓5. DisplayPolicy.applyKeyguardPolicy() 遍历窗口       ↓6. WindowState.canBeHiddenByKeyguard() 判断   → default 分支:layer (27) < TYPE_NOTIFICATION_SHADE layer (17) ?   → 27 < 17 = false   → 返回 false(不隐藏)       ↓7. Toast 穿透锁屏,锁屏后仍可见 ❌
Enter fullscreen mode Exit fullscreen mode

整条链路的唯一变更点在第三步——getWindowLayerFromTypeLw 中 TYPE_TOAST 分支从固定返回 7 改为 canAddInternalSystemWindow ? 27 : 7。后续所有方法代码均未变动,只是输入(layer 值)不同导致输出不同。

3. 影响范围

该问题影响以下两类 Toast:

  1. 所有应用发出的默认文本 Toast(Toast.makeText())—— 因为 Android 12+ 强制 SystemUI 代理所有文本 Toast 的渲染,SystemUI 持有 INTERNAL_SYSTEM_WINDOW 权限,所有文本 Toast 被分配 layer 27。
  2. 持有 INTERNAL_SYSTEM_WINDOW 权限的应用发出的自定义 Toast —— 此类应用(系统签名应用 / privileged 应用)自身即满足 canAddInternalSystemWindow 条件,其自定义 Toast 同样被分配 layer 27。

不受影响:普通第三方应用通过 setView() 发出的自定义 Toast(layer 保持 7,仍被锁屏遮挡)。

当上述两类 Toast 显示期间用户锁屏时,Toast 内容将暴露在锁屏界面之上。

4. 修复建议

问题的根因是 getWindowLayerFromTypeLw 中 TYPE_TOAST 被分配了高于锁屏的 layer 27,而 canBeHiddenByKeyguard() 的 default 分支仅做 layer 比较,未对 Toast 做特殊处理,导致 layer 27 的 Toast 穿透锁屏。

最简洁的修复方案是在 canBeHiddenByKeyguard() 中为 TYPE_TOAST 新增一个 case 分支,直接返回 true——因为 Android 16 中 Toast 的实际行为就是始终被 Keyguard 隐藏。这样无需改动 layer 分配逻辑,也不影响 layer 27 在其他场景下的作用(如非锁屏时的 Z 序提升)。

修改位置:

img-8.png

为什么不在 getWindowLayerFromTypeLw 中回退 layer?

getWindowLayerFromTypeLw 中的 layer 分配影响 Z 序(非锁屏场景下决定谁覆盖谁),有其独立的设计意图——系统托管的 Toast 在正常使用场景下确实应该高于某些悬浮窗。在 canBeHiddenByKeyguard 中处理,语义上更精确:「Toast 在锁屏时应当被隐藏」是一个锁屏策略决策,而非层级分配问题。

5. 总结

这不是一个由多条独立链路并发变更导致的复杂回归。根因只有一个:

唯一变更点:WindowManagerPolicy.getWindowLayerFromTypeLw 中 TYPE_TOAST 分支

AOSP 17 将 TYPE_TOAST 分支从固定返回 7 改为 canAddInternalSystemWindow ? 27 : 7。这一变更的意图是合理的——区分旧式 Toast(legacy,存在触控劫持风险)和系统托管的 Toast(system-managed,安全可控),给后者更高层级。

为什么波及默认文本 Toast 和有 INTERNAL_SYSTEM_WINDOW 权限的应用?

Android 12+ 强制 SystemUI 代理所有文本 Toast 的渲染。此外,持有 INTERNAL_SYSTEM_WINDOW 权限的系统签名应用发出的自定义 Toast 也直接满足条件。两者的共同点是 canAddInternalSystemWindow 为 true,因此 getWindowLayerFromTypeLw 返回 27。

canAddInternalSystemWindow 的取值来源于窗口的实际创建进程(SystemUI)或持权应用本身,因此所有文本 Toast 和有权限的自定义 Toast 都被分配 layer 27。

为什么 layer 27 导致锁屏可见?

DisplayPolicy.applyKeyguardPolicy() 通过 canBeHiddenByKeyguard() 判断是否隐藏窗口。后者在 Android 16/17 中代码完全一致,其逻辑为:窗口的实际 layer < 锁屏层(17)时隐藏。

  • layer 7 → 7 < 17 = true → 隐藏(Android 16 行为)
  • layer 27 → 27 < 17 = false → 不隐藏(Android 17 问题)

canBeHiddenByKeyguard 本身未变,变更的只是传入的 layer 值。

完整因果链

Toast.makeText().show()  
   → SystemUI 代理渲染    
     → WindowManagerPolicy.getWindowLayerFromTypeLw()      
       → canAddInternalSystemWindow = true(SystemUI 持有权限)        
         →返回 layer 27(AOSP 17 新增逻辑;AOSP 16 固定返回 7)          
           → DisplayPolicy.applyKeyguardPolicy()            
            → canBeHiddenByKeyguard()              
              → layer(27) < LOCK_SCREEN_LAYER(17) ?                
                 → false → 不隐藏 → Toast 穿透锁屏 ❌
Enter fullscreen mode Exit fullscreen mode

在 AOSP 源码中可通过以下路径定位相关代码:

  • 文件:frameworks/base/services/core/java/com/android/server/policy/WindowManagerPolicy.java
  • 方法:getWindowLayerFromTypeLw(int type, boolean canAddInternalSystemWindow, boolean roundedCornerOverlay)(变更点)
  • 文件:frameworks/base/services/core/java/com/android/server/wm/WindowState.java

• canAddInternalSystemWindow() 方法

• canBeHiddenByKeyguard() 方法(16/17 代码未变)

  • 文件:frameworks/base/services/core/java/com/android/server/wm/DisplayPolicy.java

• applyKeyguardPolicy() 方法

  • 层级常量:frameworks/base/core/java/android/view/WindowManagerPolicyConstants.java

本文基于 AOSP android-16.0.0_r1 与 android-17.0.0_r1 源码对比分析。

Top comments (0)