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

Commit dda54eaa authored by Winson Chung's avatar Winson Chung Committed by Android (Google) Code Review
Browse files

Merge "Use intercept layer to prevent touches during animation instead of defocusing" into main

parents cdf3d2ed aacc8f74
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 -> {