1 问题描述
如图,打开google的放大镜功能,然后将该放大镜和权限弹窗部分重合,会发现权限弹窗的按钮如“Allow”,点击无响应。
顺便一提,如果放大镜和权限弹窗完全重合或者完全不重合,是没问题的。
2 问题分析
2.1 分析1
首先权限弹窗View层级结构为:
对应的按钮为“SecureButton”。
打开一些log开关,首先是正常的log:
MotionEvent传到“SecureButton”并且也被这个Buttion处理了。
再看异常log为:
这里的很多log信息都是MTK的log打印的,能知道的信息是,MotionEvent已经传到“SecureButton”了,但是“SecureButton”没有处理,最终处理MotionEvent的是一个父View,LinearLayout。
2.2 分析2
既然事件已经传到了SecureButton了,那么再看具体的View类处理MotionEvent的代码,View.dispatchTouchEvent:
很值得怀疑的一个点就是View.onFilterTouchEventForSecurity是不是拦截了MotionEvent发给onTouch以及onTouchEvent来处理事件,打个断点看看:
果然,这里的View.onFilterTouchEventForSecurity走进了“SecureButton”自己重写的onFilterTouchEventForSecurity方法中,并且返回了false,导致MotionEvent没有被“SecureButton”处理。
反编译apk看到“SecureButton”重写的onFilterTouchEventForSecurity方法为:
看下这里的MotionEvent的flag:
FLAG_WINDOW_IS_OBSCURED和FLAG_WINDOW_IS_PARTIALLY_OBSCURED都表示接收MotionEvent的窗口被另外一个位于它之上的可见窗口遮挡了,但是不同点的是:
1)、FLAG_WINDOW_IS_OBSCURED表示MotionEvent落在了被遮挡的区域。
2)、FLAG_WINDOW_IS_PARTIALLY_OBSCURED表示MotionEvent落在了被遮挡区域以外的区域,也就是没被遮挡的区域。
那么回头再看看异常的log,果然:
这里的MotionEvent的flag为0x2,那么也就是包含了FLAG_WINDOW_IS_PARTIALLY_OBSCURED这个flag,所以传入“SecureButton”自己重写的onFilterTouchEventForSecurity方法后会返回false。
2.3 分析3
最后看下FLAG_WINDOW_IS_PARTIALLY_OBSCURED这个flag是在哪里添加的。
在InputDispatcher.findTouchedWindowTargetsLocked:
如果InputDispatcher.isWindowObscuredLocked返回true,那么就表示找个窗口被遮挡,就要为找个窗口对应的WindowInfo添加WINDOW_IS_PARTIALLY_OBSCURED标志位。
这里的逻辑也比较简单,从上到下找能够遮挡当前Window的WindowInfoHandle,找到当前窗口的时候就结束。
根据我添加的log,看到遮挡的WindowInfoHandle为请求权限弹窗的那个界面窗口克隆出的窗口,被遮挡的自然就是权限弹窗:
符合我们dumpsys input的信息:
input大概示意图为:
1)、开启放大镜后,会为每一个Layer克隆一个Layer出来,并且这些克隆体的层级整体都比真身高。
2)、根据InputDispatcher.canBeObscuredBy的逻辑,如果WindowInfo包含以下信息,则不遮挡:
- NOT_VISIBLE的窗口不遮挡。
- NOT_TOUCHABLE的窗口不遮挡。
- TRUSTED_OVERLAY的窗口不遮挡。
- 相同uid的窗口不遮挡。
- 相同token的窗口不遮挡。
- 不同displayId的窗口不遮挡。
排除以上条件的WindowInfoHandle后,第一个遮挡的窗口就是申请权限弹窗的那个界面的窗口的克隆窗口。
但是pixel没有问题,查看信息后发现,pixel似乎对每一个克隆出来的InputWindowHandle都添加了TRUSTED_OVERLAY这个flag:
下一步需要继续查找这个差异。
2.4 分析4
搜索代码,查看为WindowInfo添加TRUSTED_OVERLAY的位置主要是两处:
1)、一个是旧的流程:在Layer.fillInputInfo中:
2)、一个是新的流程:在LayerSnapshotBuilder.updateInput中:
具体走哪个流程,则是和SurfaceFlinger.commit的以下逻辑有关,受SurfaceFlinger.mLayerLifecycleManagerEnabled的控制:
如果SurfaceFlinger.mLayerLifecycleManagerEnabled为true,那么走新流程,否则走旧流程。
从目前的信息来看:
1)、我们的Android14的机器走的是SurfaceFlinger的旧流程,有问题。
2)、Android14的pixel走的是SurfaceFlinger的新流程,没问题。
3)、我们的Android15的机器走的是SurfaceFlinger的新流程,没问题。
根据是SurfaceFlinger.dumpAll中,可以输出SurfaceFlinger.mLayerLifecycleManagerEnabled的值:
并且Android14的pixel的SF是有这个信息的:
而我们的机器则没有。
因此这个问题应该是原生问题。
2.5 小延伸一下
现在已经知道了权限弹窗没有办法响应MotionEvent是因为,它被请求权限弹窗的那个Activity的窗口的克隆窗口盖住了,即我们刚刚的示意图:
另外如我们最开始提到的,如果放大镜和权限弹窗完全重合或者完全不重合,是没问题的。
完全不重合没问题,这个很好理解,既然不重合了,那就不存在遮挡的情况了,那完全重合为啥也没问题呢?
打印了log后发现其实原理很简单,完全重合后,点击的区域就是权限弹窗的克隆窗口,这个克隆窗口同样也能接收事件,因此事件直接被权限弹窗的克隆窗口接收了: