触摸事件透传的阻碍

86 阅读7分钟

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 调试

block_untrusted_touches

// 仅限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无内容显示区域),而这部分事件需要发给壁纸消费。

方案

  1. 常规方案:禁止壁纸接收事件,launcher收到Touch事件,判断这个事件需不需要,如果不需要使用,再把这个事件跨进程发送给wallpaper
  2. 优化方案:允许壁纸接收事件,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

image.png

壁纸窗口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

image-1.png

查看源码发现,在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有效,目前想到两种方案:

  1. 在wms中,将这个窗口的inputWindowHandle.replaceTouchableRegionWithCrop设置true
// com.android.server.wm.InputMonitor
// populateInputWindowHandle方法中,在setReplaceTouchableRegionWithCrop之前
boolean mIsWallpaper = w.mAttrs.type == TYPE_WALLPAPER;
if (mIsWallpaper) {
    useSurfaceBoundsAsTouchRegion = true;
}
  1. 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)

方案总结

  1. Launcher窗口设置为TRUSTED_OVERLAY

  2. Launcher窗口关闭ActivityRecordInputSink

  3. Launcher窗口根据业务需要,动态修改touchableRegion

  4. Wallpaper窗口修复touchableRegion