diff options
| author | 2023-03-28 15:39:34 +0000 | |
|---|---|---|
| committer | 2023-03-28 15:39:34 +0000 | |
| commit | 2835276515a0b9cc53a6b0435d91f6fb35e4bed5 (patch) | |
| tree | 9ba86f04bd17f2f50485b7a820d9e88dc4ef90e2 | |
| parent | 16717f7461a3b7417a89c80526abd190742a2bb5 (diff) | |
| parent | 656d052e0d51561ee3a092d482a0bf0e5736819a (diff) | |
Merge "Introduce TvPipMenuMode" into udc-dev
3 files changed, 572 insertions, 148 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 73123b153382..2c6ca1af62a6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -18,7 +18,7 @@ package com.android.wm.shell.pip.tv; import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; -import android.app.ActivityManager; +import android.annotation.IntDef; import android.app.RemoteAction; import android.content.BroadcastReceiver; import android.content.Context; @@ -36,6 +36,7 @@ import android.window.SurfaceSyncGroup; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.SystemWindows; @@ -61,13 +62,36 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private SurfaceControl mLeash; private TvPipMenuView mPipMenuView; private View mPipBackgroundView; + private boolean mMenuIsFocused; - private boolean mMenuIsOpen; - // User can actively move the PiP via the DPAD. - private boolean mInMoveMode; - // Used when only showing the move menu since we want to close the menu completely when - // exiting the move menu instead of showing the regular button menu. - private boolean mCloseAfterExitMoveMenu; + @TvPipMenuMode + private int mCurrentMenuMode = MODE_NO_MENU; + @TvPipMenuMode + private int mPrevMenuMode = MODE_NO_MENU; + + @IntDef(prefix = { "MODE_" }, value = { + MODE_NO_MENU, + MODE_MOVE_MENU, + MODE_ALL_ACTIONS_MENU, + }) + public @interface TvPipMenuMode {} + + /** + * In this mode the PiP menu is not focused and no user controls are displayed. + */ + static final int MODE_NO_MENU = 0; + + /** + * In this mode the PiP menu is focused and the user can use the DPAD controls to move the PiP + * to a different position on the screen. We draw arrows in all possible movement directions. + */ + static final int MODE_MOVE_MENU = 1; + + /** + * In this mode the PiP menu is focused and we display an array of actions that the user can + * select. See {@link TvPipActionsProvider} for the types of available actions. + */ + static final int MODE_ALL_ACTIONS_MENU = 2; public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState, SystemWindows systemWindows, Handler mainHandler) { @@ -143,11 +167,16 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis "%s: Actions provider is not set", TAG); return; } - mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider); + mPipMenuView = createTvPipMenuView(); setUpViewSurfaceZOrder(mPipMenuView, 1); addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE); } + @VisibleForTesting + TvPipMenuView createTvPipMenuView() { + return new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider); + } + private void attachPipBackgroundView() { mPipBackgroundView = LayoutInflater.from(mContext) .inflate(R.layout.tv_pip_menu_background, null); @@ -188,37 +217,14 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis void showMovementMenu() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showMovementMenuOnly()", TAG); - setInMoveMode(true); - if (mMenuIsOpen) { - mPipMenuView.showMoveMenu(mTvPipBoundsState.getTvPipGravity()); - } else { - mCloseAfterExitMoveMenu = true; - showMenuInternal(); - } + "%s: showMovementMenu()", TAG); + switchToMenuMode(MODE_MOVE_MENU); } @Override public void showMenu() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG); - setInMoveMode(false); - mCloseAfterExitMoveMenu = false; - showMenuInternal(); - } - - private void showMenuInternal() { - if (mPipMenuView == null) { - return; - } - - mMenuIsOpen = true; - grantPipMenuFocus(true); - if (mInMoveMode) { - mPipMenuView.showMoveMenu(mTvPipBoundsState.getTvPipGravity()); - } else { - mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ false); - } - mPipMenuView.updateBounds(mTvPipBoundsState.getBounds()); + switchToMenuMode(MODE_ALL_ACTIONS_MENU, true); } void onPipTransitionToTargetBoundsStarted(Rect targetBounds) { @@ -228,9 +234,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } void updateGravity(int gravity) { - if (mInMoveMode) { - mPipMenuView.showMovementHints(gravity); - } + mPipMenuView.setPipGravity(gravity); } private Rect calculateMenuSurfaceBounds(Rect pipBounds) { @@ -240,58 +244,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis void closeMenu() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: closeMenu()", TAG); - - if (mPipMenuView == null) { - return; - } - - mMenuIsOpen = false; - mPipMenuView.hideAllUserControls(); - grantPipMenuFocus(false); - mDelegate.onMenuClosed(); - } - - boolean isInMoveMode() { - return mInMoveMode; - } - - private void setInMoveMode(boolean moveMode) { - if (mInMoveMode == moveMode) { - return; - } - mInMoveMode = moveMode; - if (mDelegate != null) { - mDelegate.onInMoveModeChanged(); - } - } - - @Override - public boolean onExitMoveMode() { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onExitMoveMode - %b, close when exiting move menu: %b", - TAG, mInMoveMode, mCloseAfterExitMoveMenu); - - if (mInMoveMode) { - setInMoveMode(false); - if (mCloseAfterExitMoveMenu) { - mCloseAfterExitMoveMenu = false; - closeMenu(); - } else { - mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ true); - } - return true; - } - return false; - } - - @Override - public boolean onPipMovement(int keycode) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipMovement - %b", TAG, mInMoveMode); - if (mInMoveMode) { - mDelegate.movePip(keycode); - } - return mInMoveMode; + switchToMenuMode(MODE_NO_MENU); } @Override @@ -422,13 +375,90 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(), menuBounds.height())); if (mPipMenuView != null) { - mPipMenuView.updateBounds(pipBounds); + mPipMenuView.setPipBounds(pipBounds); + } + } + + // Start methods handling {@link TvPipMenuMode} + + @VisibleForTesting + boolean isMenuOpen() { + return mCurrentMenuMode != MODE_NO_MENU; + } + + @VisibleForTesting + boolean isInMoveMode() { + return mCurrentMenuMode == MODE_MOVE_MENU; + } + + @VisibleForTesting + boolean isInAllActionsMode() { + return mCurrentMenuMode == MODE_ALL_ACTIONS_MENU; + } + + private void switchToMenuMode(@TvPipMenuMode int menuMode) { + switchToMenuMode(menuMode, false); + } + + private void switchToMenuMode(@TvPipMenuMode int menuMode, boolean resetMenu) { + // Note: we intentionally don't return early here, because the TvPipMenuView needs to + // refresh the Ui even if there is no menu mode change. + mPrevMenuMode = mCurrentMenuMode; + mCurrentMenuMode = menuMode; + + ProtoLog.i(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: switchToMenuMode: setting mCurrentMenuMode=%s, mPrevMenuMode=%s", TAG, + getMenuModeString(), getMenuModeString(mPrevMenuMode)); + + updateUiOnNewMenuModeRequest(resetMenu); + updateDelegateOnNewMenuModeRequest(); + } + + private void updateUiOnNewMenuModeRequest(boolean resetMenu) { + if (mPipMenuView == null) return; + + mPipMenuView.setPipGravity(mTvPipBoundsState.getTvPipGravity()); + mPipMenuView.transitionToMenuMode(mCurrentMenuMode, resetMenu); + grantPipMenuFocus(mCurrentMenuMode != MODE_NO_MENU); + } + + private void updateDelegateOnNewMenuModeRequest() { + if (mPrevMenuMode == mCurrentMenuMode) return; + if (mDelegate == null) return; + + if (mPrevMenuMode == MODE_MOVE_MENU || isInMoveMode()) { + mDelegate.onInMoveModeChanged(); + } + + if (mCurrentMenuMode == MODE_NO_MENU) { + mDelegate.onMenuClosed(); + } + } + + @VisibleForTesting + String getMenuModeString() { + return getMenuModeString(mCurrentMenuMode); + } + + static String getMenuModeString(@TvPipMenuMode int menuMode) { + switch(menuMode) { + case MODE_NO_MENU: + return "MODE_NO_MENU"; + case MODE_MOVE_MENU: + return "MODE_MOVE_MENU"; + case MODE_ALL_ACTIONS_MENU: + return "MODE_ALL_ACTIONS_MENU"; + default: + return "Unknown"; } } + // Start {@link TvPipMenuView.Delegate} methods + @Override - public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onFocusTaskChanged", TAG); + public void onCloseEduText() { + mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); + mDelegate.closeEduText(); } @Override @@ -439,9 +469,35 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } @Override - public void onCloseEduText() { - mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); - mDelegate.closeEduText(); + public boolean onExitMoveMode() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onExitMoveMode - mCurrentMenuMode=%s", TAG, getMenuModeString()); + + final int saveMenuMode = mCurrentMenuMode; + if (isInMoveMode()) { + switchToMenuMode(mPrevMenuMode); + } + return saveMenuMode == MODE_MOVE_MENU; + } + + @Override + public boolean onPipMovement(int keycode) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipMovement - mCurrentMenuMode=%s", TAG, getMenuModeString()); + if (isInMoveMode()) { + mDelegate.movePip(keycode); + } + return isInMoveMode(); + } + + @Override + public void onPipWindowFocusChanged(boolean focused) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipWindowFocusChanged - focused=%b", TAG, focused); + mMenuIsFocused = focused; + if (!focused && isMenuOpen()) { + closeMenu(); + } } interface Delegate { @@ -455,6 +511,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void grantPipMenuFocus(boolean grantFocus) { + if (mMenuIsFocused == grantFocus) return; + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: grantWindowFocus(%b)", TAG, grantFocus); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index 56c602a1d4f3..ccf65c299613 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -26,6 +26,9 @@ import static android.view.KeyEvent.KEYCODE_DPAD_UP; import static android.view.KeyEvent.KEYCODE_ENTER; import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU; import android.content.Context; import android.graphics.Rect; @@ -86,9 +89,9 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L private final ImageView mArrowLeft; private final TvWindowMenuActionButton mA11yDoneButton; - private Rect mCurrentPipBounds; - private boolean mMoveMenuIsVisible; - private boolean mButtonMenuIsVisible; + private @TvPipMenuController.TvPipMenuMode int mCurrentMenuMode = MODE_NO_MENU; + private final Rect mCurrentPipBounds = new Rect(); + private int mCurrentPipGravity; private boolean mSwitchingOrientation; private final AccessibilityManager mA11yManager; @@ -172,7 +175,7 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L return; } - if (mButtonMenuIsVisible) { + if (mCurrentMenuMode == MODE_ALL_ACTIONS_MENU) { mSwitchingOrientation = true; mActionButtonsRecyclerView.animate() .alpha(0) @@ -217,19 +220,14 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L /** * Also updates the button gravity. */ - void updateBounds(Rect updatedBounds) { + void setPipBounds(Rect updatedPipBounds) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: updateLayout, width: %s, height: %s", TAG, updatedBounds.width(), - updatedBounds.height()); - mCurrentPipBounds = updatedBounds; - updatePipFrameBounds(); - } + "%s: updateLayout, width: %s, height: %s", TAG, updatedPipBounds.width(), + updatedPipBounds.height()); + if (updatedPipBounds.equals(mCurrentPipBounds)) return; - Rect getPipMenuContainerBounds(Rect pipBounds) { - final Rect menuUiBounds = new Rect(pipBounds); - menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace); - menuUiBounds.bottom += mEduTextDrawer.getHeight(); - return menuUiBounds; + mCurrentPipBounds.set(updatedPipBounds); + updatePipFrameBounds(); } /** @@ -264,12 +262,39 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L } } - /** - * @param gravity for the arrow hints - */ - void showMoveMenu(int gravity) { + Rect getPipMenuContainerBounds(Rect pipBounds) { + final Rect menuUiBounds = new Rect(pipBounds); + menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace); + menuUiBounds.bottom += mEduTextDrawer.getHeight(); + return menuUiBounds; + } + + void transitionToMenuMode(int menuMode, boolean resetMenu) { + switch (menuMode) { + case MODE_NO_MENU: + hideAllUserControls(); + break; + case MODE_MOVE_MENU: + showMoveMenu(); + break; + case MODE_ALL_ACTIONS_MENU: + showAllActionsMenu(resetMenu); + break; + default: + throw new IllegalArgumentException( + "Unknown TV PiP menu mode: " + + TvPipMenuController.getMenuModeString(mCurrentMenuMode)); + } + + mCurrentMenuMode = menuMode; + } + + private void showMoveMenu() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG); - showMovementHints(gravity); + + if (mCurrentMenuMode == MODE_MOVE_MENU) return; + + showMovementHints(); setMenuButtonsVisible(false); setFrameHighlighted(true); @@ -278,32 +303,38 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L mEduTextDrawer.closeIfNeeded(); } - - void showButtonsMenu(boolean exitingMoveMode) { + private void showAllActionsMenu(boolean resetMenu) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showButtonsMenu(), exitingMoveMode %b", TAG, exitingMoveMode); + "%s: showAllActionsMenu(), resetMenu %b", TAG, resetMenu); + + if (resetMenu) { + scrollToFirstAction(); + } + + if (mCurrentMenuMode == MODE_ALL_ACTIONS_MENU) return; + setMenuButtonsVisible(true); hideMovementHints(); setFrameHighlighted(true); animateAlphaTo(1f, mDimLayer); mEduTextDrawer.closeIfNeeded(); - if (exitingMoveMode) { - scrollAndRefocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE), - /* alwaysScroll= */ false); - } else { - scrollAndRefocusButton(0, /* alwaysScroll= */ true); + if (mCurrentMenuMode == MODE_MOVE_MENU) { + refocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE)); } - } - private void scrollAndRefocusButton(int position, boolean alwaysScroll) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: scrollAndRefocusButton, target: %d", TAG, position); + } - if (alwaysScroll || !refocusButton(position)) { - mButtonLayoutManager.scrollToPositionWithOffset(position, 0); - mActionButtonsRecyclerView.post(() -> refocusButton(position)); + private void scrollToFirstAction() { + // Clearing the focus here is necessary to allow a smooth scroll even if the first action + // is currently not visible. + final View focusedChild = mActionButtonsRecyclerView.getFocusedChild(); + if (focusedChild != null) { + focusedChild.clearFocus(); } + + mButtonLayoutManager.scrollToPosition(0); + mActionButtonsRecyclerView.post(() -> refocusButton(0)); } /** @@ -311,6 +342,9 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L * the view for the position not being available (scrolling beforehand will be necessary). */ private boolean refocusButton(int position) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: refocusButton, position: %d", TAG, position); + View itemToFocus = mButtonLayoutManager.findViewByPosition(position); if (itemToFocus != null) { itemToFocus.requestFocus(); @@ -319,21 +353,29 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L return itemToFocus != null; } - void hideAllUserControls() { + private void hideAllUserControls() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideAllUserControls()", TAG); + + if (mCurrentMenuMode == MODE_NO_MENU) return; + setMenuButtonsVisible(false); hideMovementHints(); setFrameHighlighted(false); animateAlphaTo(0f, mDimLayer); } + void setPipGravity(int gravity) { + mCurrentPipGravity = gravity; + if (mCurrentMenuMode == MODE_MOVE_MENU) { + showMovementHints(); + } + } + @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); - if (!hasWindowFocus) { - hideAllUserControls(); - } + mListener.onPipWindowFocusChanged(hasWindowFocus); } private void animateAlphaTo(float alpha, View view) { @@ -399,15 +441,13 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L /** * Shows user hints for moving the PiP, e.g. arrows. */ - public void showMovementHints(int gravity) { + public void showMovementHints() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity)); - mMoveMenuIsVisible = true; - - animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp); - animateAlphaTo(checkGravity(gravity, Gravity.TOP) ? 1f : 0f, mArrowDown); - animateAlphaTo(checkGravity(gravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft); - animateAlphaTo(checkGravity(gravity, Gravity.LEFT) ? 1f : 0f, mArrowRight); + "%s: showMovementHints(), position: %s", TAG, Gravity.toString(mCurrentPipGravity)); + animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp); + animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.TOP) ? 1f : 0f, mArrowDown); + animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft); + animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.LEFT) ? 1f : 0f, mArrowRight); boolean a11yEnabled = mA11yManager.isEnabled(); setArrowA11yEnabled(mArrowUp, a11yEnabled, KEYCODE_DPAD_UP); @@ -446,10 +486,7 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideMovementHints()", TAG); - if (!mMoveMenuIsVisible) { - return; - } - mMoveMenuIsVisible = false; + if (mCurrentMenuMode != MODE_MOVE_MENU) return; animateAlphaTo(0, mArrowUp); animateAlphaTo(0, mArrowRight); @@ -464,7 +501,6 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L private void setMenuButtonsVisible(boolean visible) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showUserActions: %b", TAG, visible); - mButtonMenuIsVisible = visible; animateAlphaTo(visible ? 1 : 0, mActionButtonsRecyclerView); } @@ -534,5 +570,11 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L * @return whether pip movement was handled. */ boolean onPipMovement(int keycode); + + /** + * Called when the TvPipMenuView loses focus. This also means that the TV PiP menu window + * has lost focus. + */ + void onPipWindowFocusChanged(boolean focused); } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java new file mode 100644 index 000000000000..7c6037c96360 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2023 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.pip.tv; + +import static android.view.KeyEvent.KEYCODE_DPAD_UP; + +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.os.Handler; +import android.view.SurfaceControl; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.SystemWindows; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class TvPipMenuControllerTest extends ShellTestCase { + private static final int TEST_MOVE_KEYCODE = KEYCODE_DPAD_UP; + + @Mock + private TvPipMenuController.Delegate mMockDelegate; + @Mock + private TvPipBoundsState mMockTvPipBoundsState; + @Mock + private SystemWindows mMockSystemWindows; + @Mock + private SurfaceControl mMockPipLeash; + @Mock + private Handler mMockHandler; + @Mock + private TvPipActionsProvider mMockActionsProvider; + @Mock + private TvPipMenuView mMockTvPipMenuView; + + private TvPipMenuController mTvPipMenuController; + + @Before + public void setUp() { + assumeTrue(isTelevision()); + + MockitoAnnotations.initMocks(this); + + mTvPipMenuController = new TestTvPipMenuController(); + mTvPipMenuController.setDelegate(mMockDelegate); + mTvPipMenuController.setTvPipActionsProvider(mMockActionsProvider); + mTvPipMenuController.attach(mMockPipLeash); + } + + @Test + public void testMenuNotOpenByDefault() { + assertMenuIsOpen(false); + } + + @Test + public void testSwitch_FromNoMenuMode_ToMoveMode() { + showAndAssertMoveMenu(); + } + + @Test + public void testSwitch_FromNoMenuMode_ToAllActionsMode() { + showAndAssertAllActionsMenu(); + } + + @Test + public void testSwitch_FromMoveMode_ToAllActionsMode() { + showAndAssertMoveMenu(); + showAndAssertAllActionsMenu(); + } + + @Test + public void testSwitch_FromAllActionsMode_ToMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + } + + @Test + public void testCloseMenu_NoMenuMode() { + mTvPipMenuController.closeMenu(); + assertMenuIsOpen(false); + verify(mMockDelegate, never()).onMenuClosed(); + } + + @Test + public void testCloseMenu_MoveMode() { + showAndAssertMoveMenu(); + + closeMenuAndAssertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testCloseMenu_AllActionsMode() { + showAndAssertAllActionsMenu(); + + closeMenuAndAssertMenuClosed(); + } + + @Test + public void testCloseMenu_MoveModeFollowedByAllActionsMode() { + showAndAssertMoveMenu(); + showAndAssertAllActionsMenu(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + + closeMenuAndAssertMenuClosed(); + } + + @Test + public void testCloseMenu_AllActionsModeFollowedByMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + + closeMenuAndAssertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testExitMoveMode_NoMenuMode() { + mTvPipMenuController.onExitMoveMode(); + assertMenuIsOpen(false); + verify(mMockDelegate, never()).onMenuClosed(); + } + + @Test + public void testExitMoveMode_MoveMode() { + showAndAssertMoveMenu(); + + mTvPipMenuController.onExitMoveMode(); + assertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testExitMoveMode_AllActionsMode() { + showAndAssertAllActionsMenu(); + + mTvPipMenuController.onExitMoveMode(); + assertMenuIsInAllActionsMode(); + + } + + @Test + public void testExitMoveMode_AllActionsModeFollowedByMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + + mTvPipMenuController.onExitMoveMode(); + assertMenuIsInAllActionsMode(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(false)); + } + + @Test + public void testOnBackPress_NoMenuMode() { + mTvPipMenuController.onBackPress(); + assertMenuIsOpen(false); + verify(mMockDelegate, never()).onMenuClosed(); + } + + @Test + public void testOnBackPress_MoveMode() { + showAndAssertMoveMenu(); + + pressBackAndAssertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testOnBackPress_AllActionsMode() { + showAndAssertAllActionsMenu(); + + pressBackAndAssertMenuClosed(); + } + + @Test + public void testOnBackPress_MoveModeFollowedByAllActionsMode() { + showAndAssertMoveMenu(); + showAndAssertAllActionsMenu(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + + pressBackAndAssertMenuClosed(); + } + + @Test + public void testOnBackPress_AllActionsModeFollowedByMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + + mTvPipMenuController.onBackPress(); + assertMenuIsInAllActionsMode(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(false)); + + pressBackAndAssertMenuClosed(); + } + + @Test + public void testOnPipMovement_NoMenuMode() { + assertPipMoveSuccessful(false, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE)); + } + + @Test + public void testOnPipMovement_MoveMode() { + showAndAssertMoveMenu(); + assertPipMoveSuccessful(true, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE)); + verify(mMockDelegate).movePip(eq(TEST_MOVE_KEYCODE)); + } + + @Test + public void testOnPipMovement_AllActionsMode() { + showAndAssertAllActionsMenu(); + assertPipMoveSuccessful(false, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE)); + } + + @Test + public void testOnPipWindowFocusChanged_NoMenuMode() { + mTvPipMenuController.onPipWindowFocusChanged(false); + assertMenuIsOpen(false); + } + + @Test + public void testOnPipWindowFocusChanged_MoveMode() { + showAndAssertMoveMenu(); + mTvPipMenuController.onPipWindowFocusChanged(false); + assertMenuClosed(); + } + + @Test + public void testOnPipWindowFocusChanged_AllActionsMode() { + showAndAssertAllActionsMenu(); + mTvPipMenuController.onPipWindowFocusChanged(false); + assertMenuClosed(); + } + + private void showAndAssertMoveMenu() { + mTvPipMenuController.showMovementMenu(); + assertMenuIsInMoveMode(); + verify(mMockDelegate).onInMoveModeChanged(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_MOVE_MENU), eq(false)); + } + + private void showAndAssertAllActionsMenu() { + mTvPipMenuController.showMenu(); + assertMenuIsInAllActionsMode(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(true)); + } + + private void closeMenuAndAssertMenuClosed() { + mTvPipMenuController.closeMenu(); + assertMenuClosed(); + } + + private void pressBackAndAssertMenuClosed() { + mTvPipMenuController.onBackPress(); + assertMenuClosed(); + } + + private void assertMenuClosed() { + assertMenuIsOpen(false); + verify(mMockDelegate).onMenuClosed(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_NO_MENU), eq(false)); + } + + private void assertMenuIsOpen(boolean open) { + assertTrue("The TV PiP menu should " + (open ? "" : "not ") + "be open, but it" + + " is in mode " + mTvPipMenuController.getMenuModeString(), + mTvPipMenuController.isMenuOpen() == open); + } + + private void assertMenuIsInMoveMode() { + assertTrue("Expected MODE_MOVE_MENU, but got " + mTvPipMenuController.getMenuModeString(), + mTvPipMenuController.isInMoveMode()); + assertMenuIsOpen(true); + } + + private void assertMenuIsInAllActionsMode() { + assertTrue("Expected MODE_ALL_ACTIONS_MENU, but got " + + mTvPipMenuController.getMenuModeString(), + mTvPipMenuController.isInAllActionsMode()); + assertMenuIsOpen(true); + } + + private void assertPipMoveSuccessful(boolean expected, boolean actual) { + assertTrue("Should " + (expected ? "" : "not ") + "move PiP when the menu is in mode " + + mTvPipMenuController.getMenuModeString(), expected == actual); + } + + private class TestTvPipMenuController extends TvPipMenuController { + + TestTvPipMenuController() { + super(mContext, mMockTvPipBoundsState, mMockSystemWindows, mMockHandler); + } + + @Override + TvPipMenuView createTvPipMenuView() { + return mMockTvPipMenuView; + } + } +} |