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

Commit 38eeee3d authored by Jeremy Sim's avatar Jeremy Sim Committed by Android (Google) Code Review
Browse files

Merge "Flexible 2-app split: Touch zones" into main

parents 29a9da26 4789cc2d
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -90,8 +90,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
    private int mHandleRegionHeight;

    /**
     * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with
     * insets.
     * This is not the visible bounds you see on screen, but the actual behind-the-scenes window
     * bounds, which is larger.
     */
    private final Rect mDividerBounds = new Rect();
    private final Rect mTempRect = new Rect();
+148 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;

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

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.os.Binder;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;

import com.android.wm.shell.common.SyncTransactionQueue;

/**
 * Holds and manages a single touchable surface. These are used in offscreen split layouts, where
 * we use them as a signal that the user wants to bring an offscreen app back onscreen.
 * <br>
 *                       Split root
 *                    /      |       \
 *         Stage root      Divider      Stage root
 *           /   \
 *      Task       *this class*
 *
 */
public class OffscreenTouchZone {
    private static final String TAG = "OffscreenTouchZone";

    /**
     * Whether this touch zone is on the top/left or the bottom/right screen edge.
     */
    private final boolean mIsTopLeft;
    /** The function that will be run when this zone is tapped. */
    private final Runnable mOnClickRunnable;
    private SurfaceControlViewHost mViewHost;

    /**
     * @param isTopLeft Whether the desired touch zone will be on the top/left or the bottom/right
     *                  screen edge.
     * @param runnable The function to run when the touch zone is tapped.
     */
    OffscreenTouchZone(boolean isTopLeft, Runnable runnable) {
        mIsTopLeft = isTopLeft;
        mOnClickRunnable = runnable;
    }

    /** Sets up a touch zone. */
    public void inflate(Context context, Configuration config, SyncTransactionQueue syncQueue,
            SurfaceControl stageRoot) {
        View touchableView = new View(context);
        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);

        // 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();

        // 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();
        });
    }

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

    /**
     * Listens for touch events.
     * TODO (b/349828130): Update for mouse click events as well, and possibly keyboard?
     */
    private class OffscreenTouchListener implements View.OnTouchListener {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
                mOnClickRunnable.run();
                return true;
            }
            return false;
        }
    }

    /**
     * Returns {@code true} if this touch zone represents an offscreen app on the top/left edge of
     * the display, {@code false} for bottom/right.
     */
    public boolean isTopLeft() {
        return mIsTopLeft;
    }
}
+13 −3
Original line number Diff line number Diff line
@@ -23,8 +23,8 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;

import static com.android.wm.shell.common.split.SplitLayout.BEHIND_APP_VEIL_LAYER;
import static com.android.wm.shell.common.split.SplitLayout.FRONT_APP_VEIL_LAYER;
import static com.android.wm.shell.common.split.SplitLayout.ANIMATING_BACK_APP_VEIL_LAYER;
import static com.android.wm.shell.common.split.SplitLayout.ANIMATING_FRONT_APP_VEIL_LAYER;
import static com.android.wm.shell.shared.split.SplitScreenConstants.FADE_DURATION;
import static com.android.wm.shell.shared.split.SplitScreenConstants.VEIL_DELAY_DURATION;

@@ -66,6 +66,13 @@ import java.util.function.Consumer;
 * Currently, we show a veil when:
 *  a) Task is resizing down from a fullscreen window.
 *  b) Task is being stretched past its original bounds.
 * <br>
 *                       Split root
 *                    /      |       \
 *         Stage root      Divider      Stage root
 *           /   \
 *      Task       *this class*
 *
 */
public class SplitDecorManager extends WindowlessWindowManager {
    private static final String TAG = SplitDecorManager.class.getSimpleName();
@@ -77,6 +84,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
    private Drawable mIcon;
    private ImageView mVeilIconView;
    private SurfaceControlViewHost mViewHost;
    /** The parent surface that this is attached to. Should be the stage root. */
    private SurfaceControl mHostLeash;
    private SurfaceControl mIconLeash;
    private SurfaceControl mBackgroundLeash;
@@ -389,7 +397,9 @@ public class SplitDecorManager extends WindowlessWindowManager {
        mOffsetX = (int) iconOffsetX;
        mOffsetY = (int) iconOffsetY;

        t.setLayer(leash, isGoingBehind ? BEHIND_APP_VEIL_LAYER : FRONT_APP_VEIL_LAYER);
        t.setLayer(leash, isGoingBehind
                ? ANIMATING_BACK_APP_VEIL_LAYER
                : ANIMATING_FRONT_APP_VEIL_LAYER);

        if (!mShown) {
            if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
+102 −26
Original line number Diff line number Diff line
@@ -33,6 +33,8 @@ import static com.android.wm.shell.shared.animation.Interpolators.LINEAR;
import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_10_45_45;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_45_45_10;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -72,6 +74,7 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
@@ -80,6 +83,7 @@ import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget;
import com.android.wm.shell.common.split.SplitWindowManager.ParentContainerCallbacks;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
@@ -88,6 +92,7 @@ import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition;
import com.android.wm.shell.splitscreen.StageTaskListener;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

@@ -112,15 +117,19 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
    public static final float OFFSCREEN_ASYMMETRIC_RATIO = 0.1f;

    // Here are some (arbitrarily decided) layer definitions used during animations to make sure the
    // layers stay in order. Note: This does not affect any other layer numbering systems because
    // the layer system in WindowManager is local within sibling groups. So, for example, each
    // "veil layer" defined here actually has two sub-layers; and *their* layer values, which we set
    // in SplitDecorManager, are only important relative to each other.
    public static final int DIVIDER_LAYER = 0;
    public static final int FRONT_APP_VEIL_LAYER = DIVIDER_LAYER + 20;
    public static final int FRONT_APP_LAYER = DIVIDER_LAYER + 10;
    public static final int BEHIND_APP_VEIL_LAYER = DIVIDER_LAYER - 10;
    public static final int BEHIND_APP_LAYER = DIVIDER_LAYER - 20;
    // layers stay in order. (During transitions, everything is reparented onto a transition root
    // and can be freely relayered.)
    public static final int ANIMATING_DIVIDER_LAYER = 0;
    public static final int ANIMATING_FRONT_APP_VEIL_LAYER = ANIMATING_DIVIDER_LAYER + 20;
    public static final int ANIMATING_FRONT_APP_LAYER = ANIMATING_DIVIDER_LAYER + 10;
    public static final int ANIMATING_BACK_APP_VEIL_LAYER = ANIMATING_DIVIDER_LAYER - 10;
    public static final int ANIMATING_BACK_APP_LAYER = ANIMATING_DIVIDER_LAYER - 20;
    // The divider is on the split root, and is sibling with the stage roots. We want to keep it
    // above the app stages.
    public static final int RESTING_DIVIDER_LAYER = Integer.MAX_VALUE;
    // The touch layer is on a stage root, and is sibling with things like the app activity itself
    // and the app veil. We want it to be above all those.
    public static final int RESTING_TOUCH_LAYER = Integer.MAX_VALUE;

    // Animation specs for the swap animation
    private static final int SWAP_ANIMATION_TOTAL_DURATION = 500;
@@ -155,10 +164,16 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
    // The temp bounds outside of display bounds for side stage when split screen inactive to avoid
    // flicker next time active split screen.
    private final Rect mInvisibleBounds = new Rect();
    /**
     * Areas on the screen that the user can touch to shift the layout, bringing offscreen apps
     * onscreen. If n apps are offscreen, there should be n such areas. Empty otherwise.
     */
    private final List<OffscreenTouchZone> mOffscreenTouchZones = new ArrayList<>();
    private final SplitLayoutHandler mSplitLayoutHandler;
    private final SplitWindowManager mSplitWindowManager;
    private final DisplayController mDisplayController;
    private final DisplayImeController mDisplayImeController;
    private final ParentContainerCallbacks mParentContainerCallbacks;
    private final ImePositionProcessor mImePositionProcessor;
    private final ResizingEffectPolicy mSurfaceEffectPolicy;
    private final ShellTaskOrganizer mTaskOrganizer;
@@ -199,6 +214,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
        mSplitLayoutHandler = splitLayoutHandler;
        mDisplayController = displayController;
        mDisplayImeController = displayImeController;
        mParentContainerCallbacks = parentContainerCallbacks;
        mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration,
                parentContainerCallbacks);
        mTaskOrganizer = taskOrganizer;
@@ -269,18 +285,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
        return new Rect(mRootBounds);
    }

    /** Gets bounds of divider window with screen based coordinate. */
    public Rect getDividerBounds() {
        return new Rect(mDividerBounds);
    }

    /** Gets bounds of divider window with parent based coordinate. */
    public Rect getRefDividerBounds() {
        final Rect outBounds = getDividerBounds();
        outBounds.offset(-mRootBounds.left, -mRootBounds.top);
        return outBounds;
    }

    /** Copies the top/left bounds to the provided Rect (screen-based coordinates). */
    public void copyTopLeftBounds(Rect rect) {
        rect.set(getTopLeftBounds());
@@ -319,12 +323,36 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
        return mContentBounds.getLast();
    }

    /** Gets bounds of divider window with screen based coordinate on the param Rect. */
    /**
     * Gets the bounds of divider window, in screen-based coordinates. This is not the visible
     * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger.
     */
    public Rect getDividerBounds() {
        return new Rect(mDividerBounds);
    }

    /**
     * Gets the bounds of divider window, in parent-based coordinates. This is not the visible
     * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger.
     */
    public Rect getRefDividerBounds() {
        final Rect outBounds = getDividerBounds();
        outBounds.offset(-mRootBounds.left, -mRootBounds.top);
        return outBounds;
    }

    /**
     * Gets the bounds of divider window, in screen-based coordinates. This is not the visible
     * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger.
     */
    public void getDividerBounds(Rect rect) {
        rect.set(mDividerBounds);
    }

    /** Gets bounds of divider window with parent based coordinate on the param Rect. */
    /**
     * Gets the bounds of divider window, in parent-based coordinates. This is not the visible
     * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger.
     */
    public void getRefDividerBounds(Rect rect) {
        getDividerBounds(rect);
        rect.offset(-mRootBounds.left, -mRootBounds.top);
@@ -372,6 +400,46 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
                mIsLeftRightSplit ? 0 : mRootBounds.bottom);
    }

    /**
     * (Re)calculates and activates any needed touch zones, so the user can tap them and retrieve
     * offscreen apps.
     */
    public void populateTouchZones() {
        if (!Flags.enableFlexibleTwoAppSplit()) {
            return;
        }

        if (!mOffscreenTouchZones.isEmpty()) {
            removeTouchZones();
        }

        int currentPosition = calculateCurrentSnapPosition();
        switch (currentPosition) {
            case SNAP_TO_2_10_90:
            case SNAP_TO_3_10_45_45:
                mOffscreenTouchZones.add(new OffscreenTouchZone(true /* isTopLeft */,
                        () -> flingDividerToOtherSide(currentPosition)));
                break;
            case SNAP_TO_2_90_10:
            case SNAP_TO_3_45_45_10:
                mOffscreenTouchZones.add(new OffscreenTouchZone(false /* isTopLeft */,
                        () -> flingDividerToOtherSide(currentPosition)));
                break;
        }

        mOffscreenTouchZones.forEach(mParentContainerCallbacks::inflateOnStageRoot);
    }

    /** Removes all touch zones. */
    public void removeTouchZones() {
        if (!Flags.enableFlexibleTwoAppSplit()) {
            return;
        }

        mOffscreenTouchZones.forEach(OffscreenTouchZone::release);
        mOffscreenTouchZones.clear();
    }

    /** Applies new configuration, returns {@code false} if there's no effect to the layout. */
    public boolean updateConfiguration(Configuration configuration) {
        // Update the split bounds when necessary. Besides root bounds changed, split bounds need to
@@ -509,6 +577,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
        if (mInitialized) return;
        mInitialized = true;
        mSplitWindowManager.init(this, mInsetsState, false /* isRestoring */);
        populateTouchZones();
        mDisplayImeController.addPositionProcessor(mImePositionProcessor);
    }

@@ -517,6 +586,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
        if (!mInitialized) return;
        mInitialized = false;
        mSplitWindowManager.release(t);
        removeTouchZones();
        mDisplayImeController.removePositionProcessor(mImePositionProcessor);
        mImePositionProcessor.reset();
        if (mDividerFlingAnimator != null) {
@@ -540,6 +610,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
            mImePositionProcessor.reset();
        }
        mSplitWindowManager.init(this, mInsetsState, true /* isRestoring */);
        populateTouchZones();
        // Update the surface positions again after recreating the divider in case nothing else
        // triggers it
        mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
@@ -782,6 +853,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
     *  DividerSnapAlgorithm will need to be refactored, and this function will change as well.
     */
    public void flingDividerToOtherSide(@PersistentSnapPosition int currentSnapPosition) {
        // If a fling animation is already running, just return.
        if (mDividerFlingAnimator != null) return;

        switch (currentSnapPosition) {
            case SNAP_TO_2_10_90 ->
                    snapToTarget(mDividerPosition, mDividerSnapAlgorithm.getLastSplitTarget(),
@@ -1018,9 +1092,11 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange

            // Set layers
            if (taskInfo != null) {
                t.setLayer(leash, isGoingBehind ? BEHIND_APP_LAYER : FRONT_APP_LAYER);
                t.setLayer(leash, isGoingBehind
                        ? ANIMATING_BACK_APP_LAYER
                        : ANIMATING_FRONT_APP_LAYER);
            } else {
                t.setLayer(leash, DIVIDER_LAYER);
                t.setLayer(leash, ANIMATING_DIVIDER_LAYER);
            }

            if (offsetX == 0 && offsetY == 0) {
@@ -1079,7 +1155,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
            getRefDividerBounds(mTempRect);
            t.setPosition(dividerLeash, mTempRect.left, mTempRect.top);
            // Resets layer of divider bar to make sure it is always on top.
            t.setLayer(dividerLeash, Integer.MAX_VALUE);
            t.setLayer(dividerLeash, RESTING_DIVIDER_LAYER);
        }
        copyTopLeftRefBounds(mTempRect);
        t.setPosition(leash1, mTempRect.left, mTempRect.top)
+2 −0
Original line number Diff line number Diff line
@@ -67,6 +67,8 @@ public final class SplitWindowManager extends WindowlessWindowManager {
    public interface ParentContainerCallbacks {
        void attachToParentSurface(SurfaceControl.Builder b);
        void onLeashReady(SurfaceControl leash);
        /** Inflates the given touch zone on the appropriate stage root. */
        void inflateOnStageRoot(OffscreenTouchZone touchZone);
    }

    public SplitWindowManager(String windowName, Context context, Configuration config,
Loading