From 38cb69d2364f6dd9d1b2bd5cd156ea3d85176c16 Mon Sep 17 00:00:00 2001 From: Hongwei Wang Date: Thu, 2 Mar 2023 12:58:53 -0800 Subject: [PATCH] Move PiP window upon tabletop mode changes System property persist.wm.debug.enable_move_floating_window_in_tabletop can be used to toggle the feature. Further, System property persist.wm.debug.prefer_top_half_in_tabletop can be used to control which half is preferred. This preferred location is used for moving the PiP and affects the default position as well. This feature depends on the keep-clear-area experiment. Video: http://recall/-/aaaaaabFQoRHlzixHdtY/ccvYWU8bJ7EycW396QEijm Video: http://recall/-/aaaaaabFQoRHlzixHdtY/hvQVa0uXCPm2RoDuadZuCA Bug: 260871991 Test: Manual, toggle the system property for both cases, see videos Merged-In: Ie7ae440847c81d0a49ccd56061673d86977b3b64 Change-Id: Ie7ae440847c81d0a49ccd56061673d86977b3b64 --- .../shell/common/TabletopModeController.java | 46 +++++++++++++++++++ .../wm/shell/dagger/WMShellModule.java | 4 +- .../android/wm/shell/pip/PipBoundsState.java | 23 +++++++++- .../wm/shell/pip/phone/PipController.java | 44 +++++++++++++++++- .../wm/shell/pip/phone/PipControllerTest.java | 6 ++- 5 files changed, 118 insertions(+), 5 deletions(-) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java index bf226283ae543..cb1a6e7ace6b5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java @@ -22,10 +22,12 @@ import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FOLDABLE; +import android.annotation.IntDef; import android.annotation.NonNull; import android.app.WindowConfiguration; import android.content.Context; import android.content.res.Configuration; +import android.os.SystemProperties; import android.util.ArraySet; import android.view.Surface; @@ -34,6 +36,8 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -49,8 +53,34 @@ import java.util.Set; public class TabletopModeController implements DevicePostureController.OnDevicePostureChangedListener, DisplayController.OnDisplaysChangedListener { + /** + * When {@code true}, floating windows like PiP would auto move to the position + * specified by {@link #PREFER_TOP_HALF_IN_TABLETOP} when in tabletop mode. + */ + private static final boolean ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP = + SystemProperties.getBoolean( + "persist.wm.debug.enable_move_floating_window_in_tabletop", false); + + /** + * Prefer the {@link #PREFERRED_TABLETOP_HALF_TOP} if this flag is enabled, + * {@link #PREFERRED_TABLETOP_HALF_BOTTOM} otherwise. + * See also {@link #getPreferredHalfInTabletopMode()}. + */ + private static final boolean PREFER_TOP_HALF_IN_TABLETOP = + SystemProperties.getBoolean("persist.wm.debug.prefer_top_half_in_tabletop", true); + private static final long TABLETOP_MODE_DELAY_MILLIS = 1_000; + @IntDef(prefix = {"PREFERRED_TABLETOP_HALF_"}, value = { + PREFERRED_TABLETOP_HALF_TOP, + PREFERRED_TABLETOP_HALF_BOTTOM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PreferredTabletopHalf {} + + public static final int PREFERRED_TABLETOP_HALF_TOP = 0; + public static final int PREFERRED_TABLETOP_HALF_BOTTOM = 1; + private final Context mContext; private final DevicePostureController mDevicePostureController; @@ -132,6 +162,22 @@ public class TabletopModeController implements } } + /** + * @return {@code true} if floating windows like PiP would auto move to the position + * specified by {@link #getPreferredHalfInTabletopMode()} when in tabletop mode. + */ + public boolean enableMoveFloatingWindowInTabletop() { + return ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP; + } + + /** @return Preferred half for floating windows like PiP when in tabletop mode. */ + @PreferredTabletopHalf + public int getPreferredHalfInTabletopMode() { + return PREFER_TOP_HALF_IN_TABLETOP + ? PREFERRED_TABLETOP_HALF_TOP + : PREFERRED_TABLETOP_HALF_BOTTOM; + } + /** Register {@link OnTabletopModeChangedListener} to listen for tabletop mode change. */ public void registerOnTabletopModeChangedListener( @NonNull OnTabletopModeChangedListener listener) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 45785238fe006..8f1e074ec1834 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -44,6 +44,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellBackgroundThread; @@ -353,6 +354,7 @@ public abstract class WMShellModule { TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayInsetsController displayInsetsController, + TabletopModeController pipTabletopController, Optional oneHandedController, @ShellMainThread ShellExecutor mainExecutor) { return Optional.ofNullable(PipController.create( @@ -362,7 +364,7 @@ public abstract class WMShellModule { pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, displayInsetsController, - oneHandedController, mainExecutor)); + pipTabletopController, oneHandedController, mainExecutor)); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java index 5be18d852990f..8883ce2ddbe57 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java @@ -44,7 +44,9 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; @@ -115,6 +117,12 @@ public class PipBoundsState { * @see android.view.View#setPreferKeepClearRects */ private final Set mUnrestrictedKeepClearAreas = new ArraySet<>(); + /** + * Additional to {@link #mUnrestrictedKeepClearAreas}, allow the caller to append named bounds + * as unrestricted keep clear area. Values in this map would be appended to + * {@link #getUnrestrictedKeepClearAreas()} and this is meant for internal usage only. + */ + private final Map mNamedUnrestrictedKeepClearAreas = new HashMap<>(); private @Nullable Runnable mOnMinimalSizeChangeCallback; private @Nullable TriConsumer mOnShelfVisibilityChangeCallback; @@ -393,6 +401,16 @@ public class PipBoundsState { mUnrestrictedKeepClearAreas.addAll(unrestrictedAreas); } + /** Add a named unrestricted keep clear area. */ + public void addNamedUnrestrictedKeepClearArea(@NonNull String name, Rect unrestrictedArea) { + mNamedUnrestrictedKeepClearAreas.put(name, unrestrictedArea); + } + + /** Remove a named unrestricted keep clear area. */ + public void removeNamedUnrestrictedKeepClearArea(@NonNull String name) { + mNamedUnrestrictedKeepClearAreas.remove(name); + } + @NonNull public Set getRestrictedKeepClearAreas() { return mRestrictedKeepClearAreas; @@ -400,7 +418,10 @@ public class PipBoundsState { @NonNull public Set getUnrestrictedKeepClearAreas() { - return mUnrestrictedKeepClearAreas; + if (mNamedUnrestrictedKeepClearAreas.isEmpty()) return mUnrestrictedKeepClearAreas; + final Set unrestrictedAreas = new ArraySet<>(mUnrestrictedKeepClearAreas); + unrestrictedAreas.addAll(mNamedUnrestrictedKeepClearAreas.values()); + return unrestrictedAreas; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 0d5f1432d2040..a592e79b9581f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -43,6 +43,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.graphics.Point; import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemProperties; @@ -71,6 +72,7 @@ import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; +import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.onehanded.OneHandedController; @@ -145,6 +147,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb private TaskStackListenerImpl mTaskStackListener; private PipParamsChangedForwarder mPipParamsChangedForwarder; private DisplayInsetsController mDisplayInsetsController; + private TabletopModeController mTabletopModeController; private Optional mOneHandedController; private final ShellCommandHandler mShellCommandHandler; private final ShellController mShellController; @@ -400,6 +403,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayInsetsController displayInsetsController, + TabletopModeController pipTabletopController, Optional oneHandedController, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { @@ -414,7 +418,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, - displayInsetsController, oneHandedController, mainExecutor) + displayInsetsController, pipTabletopController, oneHandedController, mainExecutor) .mImpl; } @@ -440,6 +444,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayInsetsController displayInsetsController, + TabletopModeController tabletopModeController, Optional oneHandedController, ShellExecutor mainExecutor ) { @@ -472,6 +477,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb .getInteger(R.integer.config_pipEnterAnimationDuration); mPipParamsChangedForwarder = pipParamsChangedForwarder; mDisplayInsetsController = displayInsetsController; + mTabletopModeController = tabletopModeController; shellInit.addInitCallback(this::onInit, this); } @@ -655,6 +661,42 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); + mTabletopModeController.registerOnTabletopModeChangedListener((isInTabletopMode) -> { + if (!mTabletopModeController.enableMoveFloatingWindowInTabletop()) return; + final String tag = "tabletop-mode"; + if (!isInTabletopMode) { + mPipBoundsState.removeNamedUnrestrictedKeepClearArea(tag); + return; + } + + // To prepare for the entry bounds. + final Rect displayBounds = mPipBoundsState.getDisplayBounds(); + if (mTabletopModeController.getPreferredHalfInTabletopMode() + == TabletopModeController.PREFERRED_TABLETOP_HALF_TOP) { + // Prefer top, avoid the bottom half of the display. + mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( + displayBounds.left, displayBounds.centerY(), + displayBounds.right, displayBounds.bottom)); + } else { + // Prefer bottom, avoid the top half of the display. + mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( + displayBounds.left, displayBounds.top, + displayBounds.right, displayBounds.centerY())); + } + + // Try to move the PiP window if we have entered PiP mode. + if (mPipTransitionState.hasEnteredPip()) { + final Rect pipBounds = mPipBoundsState.getBounds(); + final Point edgeInsets = mPipSizeSpecHandler.getScreenEdgeInsets(); + if ((pipBounds.height() + 2 * edgeInsets.y) > (displayBounds.height() / 2)) { + // PiP bounds is too big to fit either half, bail early. + return; + } + mMainExecutor.removeCallbacks(mMovePipInResponseToKeepClearAreasChangeCallback); + mMainExecutor.execute(mMovePipInResponseToKeepClearAreasChangeCallback); + } + }); + mOneHandedController.ifPresent(controller -> { controller.registerTransitionCallback( new OneHandedTransitionCallback() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 4a68287a4486e..116affe62db0c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -53,6 +53,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.PipAnimationController; @@ -113,6 +114,7 @@ public class PipControllerTest extends ShellTestCase { @Mock private Optional mMockOneHandedController; @Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder; @Mock private DisplayInsetsController mMockDisplayInsetsController; + @Mock private TabletopModeController mMockTabletopModeController; @Mock private DisplayLayout mMockDisplayLayout1; @Mock private DisplayLayout mMockDisplayLayout2; @@ -135,7 +137,7 @@ public class PipControllerTest extends ShellTestCase { mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, mMockTaskStackListener, mMockPipParamsChangedForwarder, mMockDisplayInsetsController, - mMockOneHandedController, mMockExecutor); + mMockTabletopModeController, mMockOneHandedController, mMockExecutor); mShellInit.init(); when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm); when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper); @@ -226,7 +228,7 @@ public class PipControllerTest extends ShellTestCase { mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, mMockTaskStackListener, mMockPipParamsChangedForwarder, mMockDisplayInsetsController, - mMockOneHandedController, mMockExecutor)); + mMockTabletopModeController, mMockOneHandedController, mMockExecutor)); } @Test