1. untrusted-touch-events
Android12开始,不受信任的触摸事件会被屏蔽。
Untrusted touch events are blocked
1.1 哪些是isTrustedOverlay?
// com.android.server.wm.WindowState
// Check private trusted overlay flag and window type to set trustedOverlay variable of
// input window handle.
mInputWindowHandle.setTrustedOverlay(
((mAttrs.privateFlags & PRIVATE_FLAG_TRUSTED_OVERLAY) != 0
&& mOwnerCanAddInternalSystemWindow)
|| InputMonitor.isTrustedOverlay(mAttrs.type));
即:①窗口属性privateFlags要包含PRIVATE_FLAG_TRUSTED_OVERLAY,且app具有INTERNAL_SYSTEM_WINDOW权限
<uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW"/>
<!-- @SystemApi Allows an application to open windows that are for use by parts
of the system user interface.
<p>Not for use by third-party applications.
@hide
-->
<permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW"
android:protectionLevel="signature" />
或者②
static boolean isTrustedOverlay(int type) {
return type == TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY
|| type == TYPE_INPUT_METHOD || type == TYPE_INPUT_METHOD_DIALOG
|| type == TYPE_MAGNIFICATION_OVERLAY || type == TYPE_STATUS_BAR
|| type == TYPE_NOTIFICATION_SHADE
|| type == TYPE_NAVIGATION_BAR
|| type == TYPE_NAVIGATION_BAR_PANEL
|| type == TYPE_SECURE_SYSTEM_OVERLAY
|| type == TYPE_DOCK_DIVIDER
|| type == TYPE_ACCESSIBILITY_OVERLAY
|| type == TYPE_INPUT_CONSUMER
|| type == TYPE_VOICE_INTERACTION
|| type == TYPE_STATUS_BAR_ADDITIONAL;
}
android14后新增一个条件③
// Android14后Trusted条件
boolean shouldWindowHandleBeTrusted(Session s) {
return InputMonitor.isTrustedOverlay(mAttrs.type)
|| ((mAttrs.privateFlags & PRIVATE_FLAG_TRUSTED_OVERLAY) != 0
&& s.mCanAddInternalSystemWindow)
|| ((mAttrs.privateFlags & PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY) != 0
&& s.mCanCreateSystemApplicationOverlay);
}
即:窗口属性privateFlags要包含PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY,且app具有SYSTEM_APPLICATION_OVERLAY权限
<uses-permission android:name="android.permission.SYSTEM_APPLICATION_OVERLAY"/>
<!-- @SystemApi @hide Allows an application to create windows using the type
{@link android.view.WindowManager.LayoutParams#TYPE_APPLICATION_OVERLAY},
shown on top of all other apps.
Allows an application to use
{@link android.view.WindowManager.LayoutsParams#setSystemApplicationOverlay(boolean)}
to create overlays that will stay visible, even if another window is requesting overlays to
be hidden through {@link android.view.Window#setHideOverlayWindows(boolean)}.
<p>Not for use by third-party applications. -->
<permission android:name="android.permission.SYSTEM_APPLICATION_OVERLAY"
android:protectionLevel="signature|recents|role|installer"/>
1.2 调试
// 仅限android 12~13 有效
// 关闭单个app的BLOCK_UNTRUSTED_TOUCHES
adb shell am compat disable BLOCK_UNTRUSTED_TOUCHES com.lws.myapplication
// 关闭所有app的BLOCK_UNTRUSTED_TOUCHES
adb shell settings put global block_untrusted_touches 0
// 查看platform_compat状态
adb shell dumpsys platform_compat > platform_compat.dumpsys
android14及以后已废弃调试开关,现已强制启用 block_untrusted_touches .
因此后续如需透传事件,务必遵守上述Trusted的条件。
Cleanup: Block untrusted touches in InputDispatcher
This change removes the BlockUntrustedTouchesMode enum, and related
code, as block_untrusted_touches is now always enforced. Removed code and policy to display a toast when an untrusted touch occurs, as it's no longer used.
Fix: 169067926
Test: atest WindowUntrustedTouchTest
Change-Id: I1f8407f523eb845a7c50a2788553bdb2616a394b
2. ActivityRecordInputSink
Android12L后新增,
Make Activites touch opaque - DO NOT MERGE
Block touches from passing through activities by adding a dedicated
surface that consumes all touches that would otherwise pass through the
bounds availble to the Activity.
// com.android.server.wm.ActivityRecordInputSink
if (allowPassthrough || !mIsCompatEnabled || mActivityRecord.isInTransition()
|| !mActivityRecord.mActivityRecordInputSinkEnabled) {
// Set to non-touchable, so the touch events can pass through.
mInputWindowHandleWrapper.setInputConfigMasked(InputConfig.NOT_TOUCHABLE,
InputConfig.NOT_TOUCHABLE);
} else {
// Set to touchable, so it can block by intercepting the touch events.
mInputWindowHandleWrapper.setInputConfigMasked(0, InputConfig.NOT_TOUCHABLE);
}
可以看到有4个条件,可以让ActivityRecordInputSink不拦截事件,
-
allowPassthrough,当前task中,下面的activity的uid和当前一致
-
!mIsCompatEnabled,兼容性调试使用,Android13开始支持
-
isInTransition,动画过程中
-
!mActivityRecordInputSinkEnabled,app主动关闭,Android14-qpr2支持
2.1 兼容性调试
Android13 ENABLE_TOUCH_OPAQUE_ACTIVITIES 兼容性调试
enable_touch_opaque_activities
// 关闭单个app的ENABLE_TOUCH_OPAQUE_ACTIVITIES
adb shell am compat disable ENABLE_TOUCH_OPAQUE_ACTIVITIES com.lws.myapplication
2.2 app主动关闭ActivityRecordInputSink
// android.app.Activity (android14 qpr2以后)
/**
* Request ActivityRecordInputSink to enable or disable blocking input events.
* @hide
*/
@RequiresPermission(INTERNAL_SYSTEM_WINDOW)
public void setActivityRecordInputSinkEnabled(boolean enabled) {
ActivityClient.getInstance().setActivityRecordInputSinkEnabled(mToken, enabled);
}
3. 壁纸接收触摸事件
3.1 壁纸窗口接收触摸
mWindowFlags默认值是FLAG_NOT_TOUCHABLE,不允许触摸。
可以使用如下方法设置允许触摸
// android.service.wallpaper.WallpaperService.Engine
/**
* Control whether this wallpaper will receive raw touch events
* from the window manager as the user interacts with the window
* that is currently displaying the wallpaper. By default they
* are turned off. If enabled, the events will be received in
* {@link #onTouchEvent(MotionEvent)}.
*/
public void setTouchEventsEnabled(boolean enabled) {
mWindowFlags = enabled
? (mWindowFlags&~WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
: (mWindowFlags|WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
if (mCreated) {
updateSurface(false, false, false);
}
}
去除FLAG_NOT_TOUCHABLE之后,
这个窗口添加显示后,这个窗口对应的inputChannel就可以接收事件了
// android.service.wallpaper.WallpaperService.Engine
InputChannel inputChannel = new InputChannel();
if (mSession.addToDisplay(mWindow, mLayout, View.VISIBLE,
mDisplay.getDisplayId(), WindowInsets.Type.defaultVisible(),
inputChannel, mInsetsState, mTempControls, new Rect(),
new float[1]) < 0) {
Log.w(TAG, "Failed to add window while updating wallpaper surface.");
return;
}
mSession.setShouldZoomOutWallpaper(mWindow, shouldZoomOutWallpaper());
mCreated = true;
mInputEventReceiver = new WallpaperInputEventReceiver(
inputChannel, Looper.myLooper());
3.2 Launcher和壁纸同时接收触摸
当Launcher窗口显示壁纸时,这个窗口就是WallpaperTarget
// com.android.server.wm.InputMonitor
final boolean hasWallpaper = mDisplayContent.mWallpaperController.isWallpaperTarget(w)
&& !mService.mPolicy.isKeyguardShowing()
&& !mDisableWallpaperTouchEvents;
inputWindowHandle.setHasWallpaper(hasWallpaper);
那么一般情况下就会给这个窗口的InputWindowHandle设置hasWallpaper
// com.android.server.wm.InputWindowHandleWrapper
void setHasWallpaper(boolean hasWallpaper) {
if (mHandle.hasWallpaper == hasWallpaper) {
return;
}
mHandle.hasWallpaper = hasWallpaper;
mChanged = true;
}
InputDispatcher在分发事件时,如果前台窗口hasWallpaper,那么就会找会壁纸窗口加入到tempTouchState, 后续事件也会发给壁纸窗口
// native\services\inputflinger\dispatcher\InputDispatcher.cpp
// If this is the first pointer going down and the touched window has a wallpaper
// then also add the touched wallpaper windows so they are locked in for the duration
// of the touch gesture.
// We do not collect wallpapers during HOVER_MOVE or SCROLL because the wallpaper
// engine only supports touch events. We would need to add a mechanism similar
// to View.onGenericMotionEvent to enable wallpapers to handle these events.
if (maskedAction == AMOTION_EVENT_ACTION_DOWN) {
sp<WindowInfoHandle> foregroundWindowHandle =
tempTouchState.getFirstForegroundWindowHandle();
if (foregroundWindowHandle && foregroundWindowHandle->getInfo()->hasWallpaper) {
const std::vector<sp<WindowInfoHandle>>& windowHandles =
getWindowHandlesLocked(displayId);
for (const sp<WindowInfoHandle>& windowHandle : windowHandles) {
const WindowInfo* info = windowHandle->getInfo();
if (info->displayId == displayId &&
windowHandle->getInfo()->type == WindowInfo::Type::WALLPAPER) {
tempTouchState
.addOrUpdateWindow(windowHandle,
InputTarget::FLAG_WINDOW_IS_OBSCURED |
InputTarget::
FLAG_WINDOW_IS_PARTIALLY_OBSCURED |
InputTarget::FLAG_DISPATCH_AS_IS,
BitSet32(0));
}
}
}
}
3.3 Launcher和壁纸只允许一个窗口收到事件
需求:
- 部分事件需要发给Launcher消费(例如滑动卡片),这部分事件不需要发给壁纸。
- 部分事件不需要发给Launcher消费(launcher无内容显示区域),而这部分事件需要发给壁纸消费。
方案
- 常规方案:禁止壁纸接收事件,launcher收到Touch事件,判断这个事件需不需要,如果不需要使用,再把这个事件跨进程发送给wallpaper
- 优化方案:允许壁纸接收事件,launcher部分区域不接收事件,系统直接发给wallpaper,避免launcher和wallpaper之间跨进程通信
上面流程在setHasWallpaper时,有一个特殊条件:!mDisableWallpaperTouchEvents
那么可以设置mDisableWallpaperTouchEvents,给Launcher设置setHasWallpaper(false),从而让事件分发给Launcher时不发给壁纸,如果该区域事件Launcher不接收,让它自然透传给下面的壁纸。
// com.android.server.wm.InputMonitor.UpdateInputForAllWindowsConsumer (android13及以下)
if ((privateFlags & PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS) != 0) {
mDisableWallpaperTouchEvents = true;
}
因此需要给Launcher窗口privateFlags增加PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS
android14以后api变更为
// android.view.WindowManager.LayoutParams (android14+)
/**
* Set whether sending touch events to the system wallpaper (which can be provided by a
* third-party application) should be enabled for windows that show wallpaper in
* background. By default, this is set to {@code true}.
* Check {@link android.view.WindowManager.LayoutParams#FLAG_SHOW_WALLPAPER} for more
* information on showing system wallpaper behind the window.
*
* @param enable whether to enable sending touch events to the system wallpaper.
*/
public void setWallpaperTouchEventsEnabled(boolean enable) {
mWallpaperTouchEventsEnabled = enable;
}
但是设置之后壁纸仍无法接收触摸事件,dumpsys input 可以看到 touchableRegion=<empty>
7: name='Wallpaper BBQ wrapper#190', id=190, displayId=0, inputConfig=NO_INPUT_CHANNEL, alpha=1.00, frame=[0,0][1440,3120], globalScale=1.000000, applicationInfo.name=, applicationInfo.token=<null>, touchableRegion=<empty>, ownerPid=2822, ownerUid=10127, dispatchingTimeout=5000ms, hasToken=false, touchOcclusionMode=BLOCK_UNTRUSTED
transform (ROT_0) (IDENTITY)
8: name='e8f7c03 com.lws.wallpaper.MyWallpaper', id=189, displayId=0, inputConfig=NOT_FOCUSABLE | PREVENT_SPLITTING | IS_WALLPAPER, alpha=1.00, frame=[0,0][0,0], globalScale=1.000000, applicationInfo.name=, applicationInfo.token=<null>, touchableRegion=<empty>, ownerPid=2822, ownerUid=10127, dispatchingTimeout=5000ms, hasToken=true, touchOcclusionMode=BLOCK_UNTRUSTED
transform (ROT_0) (IDENTITY)
而且多了一个Wallpaper BBQ wrapper
壁纸窗口layer,是BufferLayer,但是buffer是空的。
由于这个BufferLayer的buffer为空,在SurfaceFlinger计算input信息时就会把touchableRegion清空,而且没有给这个layer的InputWindowHandle设置replaceTouchableRegionWithCrop,那么最终结果的touchableRegion就是空了。
// Layer::fillInputFrameInfo
Rect tmpBounds = getInputBounds();
if (!tmpBounds.isValid()) {
info.touchableRegion.clear();
// A layer could have invalid input bounds and still expect to receive touch input if it has
// replaceTouchableRegionWithCrop. For that case, the input transform needs to be calculated
// correctly to determine the coordinate space for input events. Use an empty rect so that
// the layer will receive input in its own layer space.
tmpBounds = Rect::EMPTY_RECT;
}
Rect Layer::getInputBounds() const {
return getCroppedBufferSize(getDrawingState());
}
Rect Layer::getCroppedBufferSize(const State& s) const {
Rect size = getBufferSize(s);
Rect crop = getCrop(s);
if (!crop.isEmpty() && size.isValid()) {
size.intersect(crop, &size);
} else if (!crop.isEmpty()) {
size = crop;
}
return size;
}
真正有buffer的是Wallpaper BBQ wrapper
查看源码发现,在Android12之后增加了一个Wallpaper BBQ wrapper,代替原有的SurfaceControl用于壁纸显示,
这个Wallpaper BBQ wrapper挂载在原有的SurfaceControl下面
// android.service.wallpaper.WallpaperService.Engine
if (mBbqSurfaceControl == null) {
mBbqSurfaceControl = new SurfaceControl.Builder()
.setName("Wallpaper BBQ wrapper")
.setHidden(false)
// TODO(b/192291754)
.setMetadata(METADATA_WINDOW_TYPE, TYPE_WALLPAPER)
.setBLASTLayer()
.setParent(mSurfaceControl)
.setCallsite("Wallpaper#relayout")
.build();
}
// android.service.wallpaper.WallpaperService.Engine
private Surface getOrCreateBLASTSurface(int width, int height, int format) {
Surface ret = null;
if (mBlastBufferQueue == null) {
mBlastBufferQueue = new BLASTBufferQueue("Wallpaper", mBbqSurfaceControl,
width, height, format);
// We only return the Surface the first time, as otherwise
// it hasn't changed and there is no need to update.
ret = mBlastBufferQueue.createSurface();
} else {
mBlastBufferQueue.update(mBbqSurfaceControl, width, height, format);
}
return ret;
}
假如想要这个壁纸窗口的touchableregion有效,目前想到两种方案:
- 在wms中,将这个窗口的inputWindowHandle.replaceTouchableRegionWithCrop设置true
// com.android.server.wm.InputMonitor
// populateInputWindowHandle方法中,在setReplaceTouchableRegionWithCrop之前
boolean mIsWallpaper = w.mAttrs.type == TYPE_WALLPAPER;
if (mIsWallpaper) {
useSurfaceBoundsAsTouchRegion = true;
}
- app端屏蔽这个Wallpaper BBQ wrapper,直接使用原有的SurfaceControl
// WallpaperService.Engine onCreate时
val classEngine = Class.forName("android.service.wallpaper.WallpaperService\$Engine")
val fieldSurfaceControl = classEngine.getDeclaredField("mSurfaceControl").apply {
isAccessible = true
}
val fieldBbqSurfaceControl = classEngine.getDeclaredField("mBbqSurfaceControl").apply {
isAccessible = true
}
val mSurfaceControl = fieldSurfaceControl.get(this) as SurfaceControl
fieldBbqSurfaceControl.set(this,mSurfaceControl)
方案总结
-
Launcher窗口设置为TRUSTED_OVERLAY
-
Launcher窗口关闭ActivityRecordInputSink
-
Launcher窗口根据业务需要,动态修改touchableRegion
-
Wallpaper窗口修复touchableRegion