Merge "Introduce TvPipMenuMode" into udc-dev

This commit is contained in:
Galia Peycheva
2023-03-28 15:39:34 +00:00
committed by Android (Google) Code Review
3 changed files with 572 additions and 148 deletions

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}