Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit aacc8f74 authored by Winson Chung's avatar Winson Chung
Browse files

Use intercept layer to prevent touches during animation instead of defocusing

- Instead of adjusting the focusability of the root tasks (which has
  other core side-effects), we can abstract out the intercept layer logic
  used by OffscreenTouchZone to also block touches on the top split
  root for the duration of the swap animation

Fixes: 429058310
Flag: EXEMPT bugfix
Test: Manual, ensure we still ignore touches during swap animation,
      and test with/without bubbled task
Change-Id: Id4a5c5d05cb66df19cc01b62aeaedab4d26a5907
parent 6e9b69dc
Loading
Loading
Loading
Loading
+25 −67
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERL

import static com.android.wm.shell.common.split.SplitLayout.RESTING_TOUCH_LAYER;

import android.app.TaskInfo;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
@@ -57,9 +58,10 @@ public class OffscreenTouchZone {
    private final boolean mIsTopLeft;
    /** The function that will be run when this zone is tapped. */
    private final Runnable mOnClickRunnable;
    private SurfaceControlViewHost mViewHost;
    private SurfaceControl mLeash;

    private TouchInterceptLayer mInterceptLayer;
    private GestureDetector mGestureDetector;

    private final GestureDetector.SimpleOnGestureListener mTapDetector =
            new GestureDetector.SimpleOnGestureListener() {
                @Override
@@ -68,6 +70,7 @@ public class OffscreenTouchZone {
                    return true;
                }
            };

    private final View.OnDragListener mDragListener = new View.OnDragListener() {
        @Override
        public boolean onDrag(View view, DragEvent dragEvent) {
@@ -77,6 +80,14 @@ public class OffscreenTouchZone {
            return false;
        }
    };

    private final View.OnTouchListener mTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            return mGestureDetector.onTouchEvent(motionEvent);
        }
    };

    /**
     * @param isTopLeft Whether the desired touch zone will be on the top/left or the bottom/right
     *                  screen edge.
@@ -88,75 +99,22 @@ public class OffscreenTouchZone {
    }

    /** Sets up a touch zone. */
    public void inflate(Context context, Configuration config, SyncTransactionQueue syncQueue,
            SurfaceControl stageRoot) {
        View touchableView = new View(context);
        mGestureDetector = new GestureDetector(context, mTapDetector);
        touchableView.setOnTouchListener(new OffscreenTouchListener());

        // Set WM flags, tokens, and sizing on the touchable view. It will be the same size as its
        // parent, the stage root.
        // TODO (b/349828130): It's a bit wasteful to have the touch zone cover the whole app
        //  surface, even extending offscreen (keeps buffer active in memory), so can trim it down
        //  to the visible onscreen area in a future patch.
        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.TYPE_INPUT_CONSUMER,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSLUCENT);
        lp.token = new Binder();
        lp.setTitle(TAG);
        lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
        touchableView.setLayoutParams(lp);
        touchableView.setOnDragListener(mDragListener);

        // Create a new leash under our stage leash.
        final SurfaceControl.Builder builder = new SurfaceControl.Builder()
                .setContainerLayer()
                .setName(TAG + (mIsTopLeft ? "TopLeft" : "BottomRight"))
                .setCallsite("OffscreenTouchZone::init");
        builder.setParent(stageRoot);
        SurfaceControl leash = builder.build();
        mLeash = leash;

        // Create a ViewHost that will hold our view.
        WindowlessWindowManager wwm = new WindowlessWindowManager(config, leash, null);
        mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), wwm,
                "SplitTouchZones");
        mViewHost.setView(touchableView, lp);

        // Create a transaction so that we can activate and reposition our surface.
        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
        // Set layer to maximum. We want this surface to be above the app layer, or else touches
        // will be blocked.
        t.setLayer(leash, RESTING_TOUCH_LAYER);
        // Leash starts off hidden, show it.
        t.show(leash);
        syncQueue.runInSync(transaction -> {
            transaction.merge(t);
            t.close();
        });
    public void inflate(Context context, SurfaceControl rootLeash, TaskInfo rootTaskInfo) {
        mInterceptLayer = new TouchInterceptLayer(TAG + (mIsTopLeft ? "TopLeft" : "BottomRight"));
        mInterceptLayer.inflate(context, rootLeash, rootTaskInfo);

        View rootView = mInterceptLayer.getRootView();

        mGestureDetector = new GestureDetector(rootView.getContext(), mTapDetector);
        rootView.setOnTouchListener(mTouchListener);
        rootView.setOnDragListener(mDragListener);
    }

    /** Releases the touch zone when it's no longer needed. */
    void release(SurfaceControl.Transaction t) {
        if (mViewHost != null) {
            mViewHost.release();
        }
        if (mLeash != null) {
            t.remove(mLeash);
            mLeash = null;
        }
    }

    /**
     * Listens for touch events.
     */
    private class OffscreenTouchListener implements View.OnTouchListener {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            return mGestureDetector.onTouchEvent(motionEvent);
        if (mInterceptLayer != null) {
            mInterceptLayer.release();
            mInterceptLayer = null;
        }
    }

+115 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.wm.shell.common.split

import android.app.TaskInfo
import android.content.Context
import android.graphics.PixelFormat
import android.os.Binder
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
import android.view.View
import android.view.WindowManager
import android.view.WindowlessWindowManager

/**
 * Manages a touchable surface that is intended to intercept touches.
 */
class TouchInterceptLayer(
    val name: String = TAG
) {
    private var viewHost: SurfaceControlViewHost? = null
    private var leash: SurfaceControl? = null
    var rootView: View? = null

    /**
     * Creates a touch zone.
     */
    fun inflate(context: Context,
        rootLeash: SurfaceControl,
        rootTaskInfo: TaskInfo
    ) {
        rootView = View(context.createConfigurationContext(rootTaskInfo.configuration))

        // Set WM flags, tokens, and sizing on the touchable view. It will be the same size as its
        // parent
        // TODO (b/349828130): It's a bit wasteful to have the touch zone cover the whole app
        //  surface, even extending offscreen (keeps buffer active in memory), so can trim it down
        //  to the visible onscreen area in a future patch.
        val lp = WindowManager.LayoutParams(
            rootTaskInfo.configuration.windowConfiguration.bounds.width(),
            rootTaskInfo.configuration.windowConfiguration.bounds.height(),
            WindowManager.LayoutParams.TYPE_INPUT_CONSUMER,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSPARENT
        )
        lp.token = Binder()
        lp.setTitle(name)
        lp.privateFlags =
            lp.privateFlags or (WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
                    or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
        rootView?.setLayoutParams(lp)

        // Create a new leash under our stage leash.
        val builder = SurfaceControl.Builder()
            .setContainerLayer()
            .setName(name)
            .setCallsite(name + "[TouchInterceptLayer.inflate]")
        builder.setParent(rootLeash)
        leash = builder.build()

        // Create a ViewHost that will hold our view.
        val wwm = WindowlessWindowManager(rootTaskInfo.configuration, leash, null)
        viewHost = SurfaceControlViewHost(
            context, context.display, wwm,
            name + "[TouchInterceptLayer.inflate]"
        )
        viewHost!!.setView(rootView!!, lp)

        // Request a transparent region (and mark as will not draw to ensure the full view region)
        // as an optimization to SurfaceFlinger so we don't need to render the transparent surface
        rootView?.setWillNotDraw(true)
        rootView?.parent?.requestTransparentRegion(rootView)

        // Create a transaction so that we can activate and reposition our surface.
        val t = SurfaceControl.Transaction()
        // Set layer to maximum. We want this surface to be above the app layer, or else touches
        // will be blocked.
        t.setLayer(leash!!, SplitLayout.RESTING_TOUCH_LAYER)
        // Leash starts off hidden, show it.
        t.show(leash)
        t.apply()
    }

    /**
     * Releases the touch zone when it's no longer needed.
     */
    fun release() {
        if (viewHost != null) {
            viewHost!!.release()
        }
        if (leash != null) {
            val t = SurfaceControl.Transaction()
            t.remove(leash!!)
            t.apply()
            leash = null
        }
    }

    companion object {
        private const val TAG: String = "TouchInterceptLayer"
    }
}
+12 −18
Original line number Diff line number Diff line
@@ -158,6 +158,7 @@ import com.android.wm.shell.common.split.SplitDecorManager;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.common.split.SplitState;
import com.android.wm.shell.common.split.SplitWindowManager;
import com.android.wm.shell.common.split.TouchInterceptLayer;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.desktopmode.data.DesktopRepository;
@@ -382,10 +383,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
                    //  after making SplitLayout display aware.
                    RunningTaskInfo rootTaskInfo =
                            mSplitMultiDisplayHelper.getDisplayRootTaskInfo(DEFAULT_DISPLAY);
                    touchZone.inflate(
                            mContext.createConfigurationContext(rootTaskInfo.configuration),
                            rootTaskInfo.configuration, mSyncQueue,
                            touchZone.isTopLeft() ? topLeftLeash : bottomRightLeash);
                    touchZone.inflate(mContext,
                            touchZone.isTopLeft() ? topLeftLeash : bottomRightLeash, rootTaskInfo);
                }

                @Override
@@ -1515,16 +1514,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
            bottomRightStage =
                    mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
        }
        // Don't allow windows or divider to be focused during animation (mRootTaskInfo is the
        // parent of all 3 leaves). We don't want the user to be able to tap and focus a window
        // while it is moving across the screen, because granting focus also recalculates the
        // layering order, which is in delicate balance during this animation.
        WindowContainerTransaction noFocus = new WindowContainerTransaction();
        // TODO: b/393217881 - replace DEFAULT DISPLAY with the current display id
        RunningTaskInfo rootTaskInfo =
                mSplitMultiDisplayHelper.getDisplayRootTaskInfo(DEFAULT_DISPLAY);
        noFocus.setFocusable(rootTaskInfo.token, false);
        mSyncQueue.queue(noFocus);

        final TouchInterceptLayer touchInterceptLayer = new TouchInterceptLayer();
        touchInterceptLayer.inflate(mContext,
                mSplitMultiDisplayHelper.getDisplayRootTaskLeash(DEFAULT_DISPLAY),
                mSplitMultiDisplayHelper.getDisplayRootTaskInfo(DEFAULT_DISPLAY));

        // Remove touch layers, since offscreen apps coming onscreen will not need their touch
        // layers anymore. populateTouchZones() is called in the end callback to inflate new touch
        // layers in the appropriate places.
@@ -1537,14 +1532,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
                    SplitDecorManager decorManager1 = topLeftStage.getDecorManager();
                    SplitDecorManager decorManager2 = bottomRightStage.getDecorManager();

                    WindowContainerTransaction wct = new WindowContainerTransaction();

                    // Restore focus-ability to the windows and divider
                    wct.setFocusable(rootTaskInfo.token, true);
                    touchInterceptLayer.release();

                    if (enableFlexibleSplit()) {
                        mStageOrderOperator.onDoubleTappedDivider();
                    }

                    WindowContainerTransaction wct = new WindowContainerTransaction();
                    setSideStagePosition(reverseSplitPosition(mSideStagePosition), wct);
                    mSyncQueue.queue(wct);
                    mSyncQueue.runInSync(st -> {