From ea68283638e6dc6ba760bd306171b0fa4f8d1ee5 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 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 | 8 +++- 5 files changed, 120 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 ba0f07376468f..7a83d101578fe 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 @@ -45,6 +45,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; @@ -357,6 +358,7 @@ public abstract class WMShellModule { TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayInsetsController displayInsetsController, + TabletopModeController pipTabletopController, Optional oneHandedController, @ShellMainThread ShellExecutor mainExecutor) { return Optional.ofNullable(PipController.create( @@ -366,7 +368,7 @@ public abstract class WMShellModule { pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, - displayInsetsController, oneHandedController, mainExecutor)); + displayInsetsController, 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 f6648085075dc..f08742db8ebfa 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 @@ -43,7 +43,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; @@ -113,6 +115,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; @@ -378,6 +386,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; @@ -385,7 +403,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 db6ef1dffc9c7..be9b529ae2459 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; @@ -147,6 +149,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; @@ -403,6 +406,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)) { @@ -417,7 +421,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb pipDisplayLayoutState, pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, - displayInsetsController, oneHandedController, mainExecutor) + displayInsetsController, pipTabletopController, oneHandedController, mainExecutor) .mImpl; } @@ -444,6 +448,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayInsetsController displayInsetsController, + TabletopModeController tabletopModeController, Optional oneHandedController, ShellExecutor mainExecutor ) { @@ -477,6 +482,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb .getInteger(R.integer.config_pipEnterAnimationDuration); mPipParamsChangedForwarder = pipParamsChangedForwarder; mDisplayInsetsController = displayInsetsController; + mTabletopModeController = tabletopModeController; shellInit.addInitCallback(this::onInit, this); } @@ -659,6 +665,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 0e14c69bdc006..108e273d75a50 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; @@ -115,6 +116,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; @@ -137,7 +139,8 @@ public class PipControllerTest extends ShellTestCase { mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, mMockTaskStackListener, mMockPipParamsChangedForwarder, - mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor); + mMockDisplayInsetsController, mMockTabletopModeController, + mMockOneHandedController, mMockExecutor); mShellInit.init(); when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm); when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper); @@ -228,7 +231,8 @@ public class PipControllerTest extends ShellTestCase { mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, mMockTaskStackListener, mMockPipParamsChangedForwarder, - mMockDisplayInsetsController, mMockOneHandedController, mMockExecutor)); + mMockDisplayInsetsController, mMockTabletopModeController, + mMockOneHandedController, mMockExecutor)); } @Test