1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.pip.tv; 18 19 import static android.view.KeyEvent.KEYCODE_DPAD_UP; 20 21 import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU; 22 import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU; 23 import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU; 24 25 import static org.junit.Assert.assertTrue; 26 import static org.junit.Assume.assumeTrue; 27 import static org.mockito.ArgumentMatchers.eq; 28 import static org.mockito.Mockito.doReturn; 29 import static org.mockito.Mockito.mock; 30 import static org.mockito.Mockito.never; 31 import static org.mockito.Mockito.times; 32 import static org.mockito.Mockito.verify; 33 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.view.SurfaceControl; 37 import android.view.ViewTreeObserver; 38 import android.view.ViewTreeObserver.OnWindowFocusChangeListener; 39 40 import com.android.wm.shell.ShellTestCase; 41 import com.android.wm.shell.common.SystemWindows; 42 43 import org.junit.Before; 44 import org.junit.Test; 45 import org.mockito.ArgumentCaptor; 46 import org.mockito.Mock; 47 import org.mockito.MockitoAnnotations; 48 49 public class TvPipMenuControllerTest extends ShellTestCase { 50 private static final int TEST_MOVE_KEYCODE = KEYCODE_DPAD_UP; 51 52 @Mock 53 private TvPipMenuController.Delegate mMockDelegate; 54 @Mock 55 private TvPipBoundsState mMockTvPipBoundsState; 56 @Mock 57 private SystemWindows mMockSystemWindows; 58 @Mock 59 private TvPipMenuView mMockTvPipMenuView; 60 @Mock 61 private TvPipBackgroundView mMockTvPipBackgroundView; 62 63 private Handler mMainHandler; 64 private TvPipMenuController mTvPipMenuController; 65 private OnWindowFocusChangeListener mFocusChangeListener; 66 67 @Before setUp()68 public void setUp() { 69 assumeTrue(isTelevision()); 70 71 MockitoAnnotations.initMocks(this); 72 mMainHandler = new Handler(Looper.getMainLooper()); 73 74 final ViewTreeObserver mockMenuTreeObserver = mock(ViewTreeObserver.class); 75 doReturn(mockMenuTreeObserver).when(mMockTvPipMenuView).getViewTreeObserver(); 76 77 mTvPipMenuController = new TestTvPipMenuController(); 78 mTvPipMenuController.setDelegate(mMockDelegate); 79 mTvPipMenuController.setTvPipActionsProvider(mock(TvPipActionsProvider.class)); 80 mTvPipMenuController.attach(mock(SurfaceControl.class)); 81 mFocusChangeListener = captureFocusChangeListener(mockMenuTreeObserver); 82 } 83 captureFocusChangeListener( ViewTreeObserver mockTreeObserver)84 private OnWindowFocusChangeListener captureFocusChangeListener( 85 ViewTreeObserver mockTreeObserver) { 86 final ArgumentCaptor<OnWindowFocusChangeListener> focusChangeListenerCaptor = 87 ArgumentCaptor.forClass(OnWindowFocusChangeListener.class); 88 verify(mockTreeObserver).addOnWindowFocusChangeListener( 89 focusChangeListenerCaptor.capture()); 90 return focusChangeListenerCaptor.getValue(); 91 } 92 93 @Test testMenuNotOpenByDefault()94 public void testMenuNotOpenByDefault() { 95 assertMenuIsOpen(false); 96 } 97 98 @Test testSwitch_FromNoMenuMode_ToMoveMode()99 public void testSwitch_FromNoMenuMode_ToMoveMode() { 100 showAndAssertMoveMenu(true); 101 } 102 103 @Test testSwitch_FromNoMenuMode_ToAllActionsMode()104 public void testSwitch_FromNoMenuMode_ToAllActionsMode() { 105 showAndAssertAllActionsMenu(true); 106 } 107 108 @Test testSwitch_FromMoveMode_ToAllActionsMode()109 public void testSwitch_FromMoveMode_ToAllActionsMode() { 110 showAndAssertMoveMenu(true); 111 showAndAssertAllActionsMenu(false); 112 verify(mMockDelegate, times(2)).onInMoveModeChanged(); 113 } 114 115 @Test testSwitch_FromAllActionsMode_ToMoveMode()116 public void testSwitch_FromAllActionsMode_ToMoveMode() { 117 showAndAssertAllActionsMenu(true); 118 showAndAssertMoveMenu(false); 119 } 120 121 @Test testCloseMenu_NoMenuMode()122 public void testCloseMenu_NoMenuMode() { 123 mTvPipMenuController.closeMenu(); 124 assertMenuIsOpen(false); 125 verify(mMockDelegate, never()).onMenuClosed(); 126 } 127 128 @Test testCloseMenu_MoveMode()129 public void testCloseMenu_MoveMode() { 130 showAndAssertMoveMenu(true); 131 132 closeMenuAndAssertMenuClosed(true); 133 verify(mMockDelegate, times(2)).onInMoveModeChanged(); 134 } 135 136 @Test testCloseMenu_AllActionsMode()137 public void testCloseMenu_AllActionsMode() { 138 showAndAssertAllActionsMenu(true); 139 140 closeMenuAndAssertMenuClosed(true); 141 } 142 143 @Test testCloseMenu_MoveModeFollowedByMoveMode()144 public void testCloseMenu_MoveModeFollowedByMoveMode() { 145 showAndAssertMoveMenu(true); 146 showAndAssertMoveMenu(false); 147 148 closeMenuAndAssertMenuClosed(true); 149 verify(mMockDelegate, times(2)).onInMoveModeChanged(); 150 } 151 152 @Test testCloseMenu_MoveModeFollowedByAllActionsMode()153 public void testCloseMenu_MoveModeFollowedByAllActionsMode() { 154 showAndAssertMoveMenu(true); 155 showAndAssertAllActionsMenu(false); 156 verify(mMockDelegate, times(2)).onInMoveModeChanged(); 157 158 closeMenuAndAssertMenuClosed(true); 159 } 160 161 @Test testCloseMenu_AllActionsModeFollowedByMoveMode()162 public void testCloseMenu_AllActionsModeFollowedByMoveMode() { 163 showAndAssertAllActionsMenu(true); 164 showAndAssertMoveMenu(false); 165 166 closeMenuAndAssertMenuClosed(true); 167 verify(mMockDelegate, times(2)).onInMoveModeChanged(); 168 } 169 170 @Test testCloseMenu_AllActionsModeFollowedByAllActionsMode()171 public void testCloseMenu_AllActionsModeFollowedByAllActionsMode() { 172 showAndAssertAllActionsMenu(true); 173 showAndAssertAllActionsMenu(false); 174 175 closeMenuAndAssertMenuClosed(true); 176 verify(mMockDelegate, never()).onInMoveModeChanged(); 177 } 178 179 @Test testExitMenuMode_NoMenuMode()180 public void testExitMenuMode_NoMenuMode() { 181 mTvPipMenuController.onExitCurrentMenuMode(); 182 assertMenuIsOpen(false); 183 verify(mMockDelegate, never()).onMenuClosed(); 184 verify(mMockDelegate, never()).onInMoveModeChanged(); 185 } 186 187 @Test testExitMenuMode_MoveMode()188 public void testExitMenuMode_MoveMode() { 189 showAndAssertMoveMenu(true); 190 191 mTvPipMenuController.onExitCurrentMenuMode(); 192 mFocusChangeListener.onWindowFocusChanged(false); 193 assertMenuClosed(); 194 verify(mMockDelegate, times(2)).onInMoveModeChanged(); 195 } 196 197 @Test testExitMenuMode_AllActionsMode()198 public void testExitMenuMode_AllActionsMode() { 199 showAndAssertAllActionsMenu(true); 200 201 mTvPipMenuController.onExitCurrentMenuMode(); 202 mFocusChangeListener.onWindowFocusChanged(false); 203 assertMenuClosed(); 204 } 205 206 @Test testExitMenuMode_AllActionsModeFollowedByMoveMode()207 public void testExitMenuMode_AllActionsModeFollowedByMoveMode() { 208 showAndAssertAllActionsMenu(true); 209 showAndAssertMoveMenu(false); 210 211 mTvPipMenuController.onExitCurrentMenuMode(); 212 assertSwitchedToAllActionsMode(2); 213 verify(mMockDelegate, times(2)).onInMoveModeChanged(); 214 215 mTvPipMenuController.onExitCurrentMenuMode(); 216 mFocusChangeListener.onWindowFocusChanged(false); 217 assertMenuClosed(); 218 } 219 220 @Test testExitMenuMode_AllActionsModeFollowedByAllActionsMode()221 public void testExitMenuMode_AllActionsModeFollowedByAllActionsMode() { 222 showAndAssertAllActionsMenu(true); 223 showAndAssertAllActionsMenu(false); 224 225 mTvPipMenuController.onExitCurrentMenuMode(); 226 mFocusChangeListener.onWindowFocusChanged(false); 227 assertMenuClosed(); 228 verify(mMockDelegate, never()).onInMoveModeChanged(); 229 } 230 231 @Test testExitMenuMode_MoveModeFollowedByAllActionsMode()232 public void testExitMenuMode_MoveModeFollowedByAllActionsMode() { 233 showAndAssertMoveMenu(true); 234 235 showAndAssertAllActionsMenu(false); 236 verify(mMockDelegate, times(2)).onInMoveModeChanged(); 237 238 mTvPipMenuController.onExitCurrentMenuMode(); 239 mFocusChangeListener.onWindowFocusChanged(false); 240 assertMenuClosed(); 241 } 242 243 @Test testExitMenuMode_MoveModeFollowedByMoveMode()244 public void testExitMenuMode_MoveModeFollowedByMoveMode() { 245 showAndAssertMoveMenu(true); 246 showAndAssertMoveMenu(false); 247 248 mTvPipMenuController.onExitCurrentMenuMode(); 249 mFocusChangeListener.onWindowFocusChanged(false); 250 assertMenuClosed(); 251 verify(mMockDelegate, times(2)).onInMoveModeChanged(); 252 } 253 254 @Test testOnPipMovement_NoMenuMode()255 public void testOnPipMovement_NoMenuMode() { 256 moveAndAssertMoveSuccessful(false); 257 } 258 259 @Test testOnPipMovement_MoveMode()260 public void testOnPipMovement_MoveMode() { 261 showAndAssertMoveMenu(true); 262 moveAndAssertMoveSuccessful(true); 263 } 264 265 @Test testOnPipMovement_AllActionsMode()266 public void testOnPipMovement_AllActionsMode() { 267 showAndAssertAllActionsMenu(true); 268 moveAndAssertMoveSuccessful(false); 269 } 270 271 @Test testUnexpectedFocusChanges()272 public void testUnexpectedFocusChanges() { 273 mFocusChangeListener.onWindowFocusChanged(true); 274 assertSwitchedToAllActionsMode(1); 275 276 mFocusChangeListener.onWindowFocusChanged(false); 277 assertMenuClosed(); 278 279 showAndAssertMoveMenu(true); 280 mFocusChangeListener.onWindowFocusChanged(false); 281 assertMenuClosed(2); 282 verify(mMockDelegate, times(2)).onInMoveModeChanged(); 283 } 284 285 @Test testAsyncScenario_AllActionsModeRequestFollowedByAsyncMoveModeRequest()286 public void testAsyncScenario_AllActionsModeRequestFollowedByAsyncMoveModeRequest() { 287 mTvPipMenuController.showMenu(); 288 // Artificially delaying the focus change update and adding a move request to simulate an 289 // async problematic situation. 290 mTvPipMenuController.showMovementMenu(); 291 // The first focus change update arrives 292 mFocusChangeListener.onWindowFocusChanged(true); 293 294 // We expect that the TvPipMenuController will directly switch to the "pending" menu mode 295 // - MODE_MOVE_MENU, because no change of focus is needed. 296 assertSwitchedToMoveMode(); 297 } 298 299 @Test testAsyncScenario_MoveModeRequestFollowedByAsyncAllActionsModeRequest()300 public void testAsyncScenario_MoveModeRequestFollowedByAsyncAllActionsModeRequest() { 301 mTvPipMenuController.showMovementMenu(); 302 mTvPipMenuController.showMenu(); 303 304 mFocusChangeListener.onWindowFocusChanged(true); 305 assertSwitchedToAllActionsMode(1); 306 verify(mMockDelegate, never()).onInMoveModeChanged(); 307 } 308 309 @Test testAsyncScenario_DropObsoleteIntermediateModeSwitchRequests()310 public void testAsyncScenario_DropObsoleteIntermediateModeSwitchRequests() { 311 mTvPipMenuController.showMovementMenu(); 312 mTvPipMenuController.closeMenu(); 313 314 // Focus change from showMovementMenu() call. 315 mFocusChangeListener.onWindowFocusChanged(true); 316 assertSwitchedToMoveMode(); 317 verify(mMockDelegate).onInMoveModeChanged(); 318 319 // Focus change from closeMenu() call. 320 mFocusChangeListener.onWindowFocusChanged(false); 321 assertMenuClosed(); 322 verify(mMockDelegate, times(2)).onInMoveModeChanged(); 323 324 // Unexpected focus gain should open MODE_ALL_ACTIONS_MENU. 325 mFocusChangeListener.onWindowFocusChanged(true); 326 assertSwitchedToAllActionsMode(1); 327 328 mTvPipMenuController.closeMenu(); 329 mTvPipMenuController.showMovementMenu(); 330 331 assertSwitchedToMoveMode(2); 332 333 mFocusChangeListener.onWindowFocusChanged(false); 334 assertMenuClosed(2); 335 336 // Closing the menu resets the default menu mode, so the next focus gain opens the menu in 337 // the default mode - MODE_ALL_ACTIONS_MENU. 338 mFocusChangeListener.onWindowFocusChanged(true); 339 assertSwitchedToAllActionsMode(2); 340 verify(mMockDelegate, times(4)).onInMoveModeChanged(); 341 342 } 343 showAndAssertMoveMenu(boolean focusChange)344 private void showAndAssertMoveMenu(boolean focusChange) { 345 mTvPipMenuController.showMovementMenu(); 346 if (focusChange) { 347 mFocusChangeListener.onWindowFocusChanged(true); 348 } 349 assertSwitchedToMoveMode(); 350 } 351 assertSwitchedToMoveMode()352 private void assertSwitchedToMoveMode() { 353 assertSwitchedToMoveMode(1); 354 } 355 assertSwitchedToMoveMode(int times)356 private void assertSwitchedToMoveMode(int times) { 357 assertMenuIsInMoveMode(); 358 verify(mMockDelegate, times(2 * times - 1)).onInMoveModeChanged(); 359 verify(mMockTvPipMenuView, times(times)).transitionToMenuMode(eq(MODE_MOVE_MENU)); 360 verify(mMockTvPipBackgroundView, times(times)).transitionToMenuMode(eq(MODE_MOVE_MENU)); 361 } 362 showAndAssertAllActionsMenu(boolean focusChange)363 private void showAndAssertAllActionsMenu(boolean focusChange) { 364 showAndAssertAllActionsMenu(focusChange, 1); 365 } 366 showAndAssertAllActionsMenu(boolean focusChange, int times)367 private void showAndAssertAllActionsMenu(boolean focusChange, int times) { 368 mTvPipMenuController.showMenu(); 369 if (focusChange) { 370 mFocusChangeListener.onWindowFocusChanged(true); 371 } 372 373 assertSwitchedToAllActionsMode(times); 374 } 375 assertSwitchedToAllActionsMode(int times)376 private void assertSwitchedToAllActionsMode(int times) { 377 assertMenuIsInAllActionsMode(); 378 verify(mMockTvPipMenuView, times(times)) 379 .transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU)); 380 verify(mMockTvPipBackgroundView, times(times)) 381 .transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU)); 382 } 383 closeMenuAndAssertMenuClosed(boolean focusChange)384 private void closeMenuAndAssertMenuClosed(boolean focusChange) { 385 mTvPipMenuController.closeMenu(); 386 if (focusChange) { 387 mFocusChangeListener.onWindowFocusChanged(false); 388 } 389 assertMenuClosed(); 390 } 391 moveAndAssertMoveSuccessful(boolean expectedSuccess)392 private void moveAndAssertMoveSuccessful(boolean expectedSuccess) { 393 mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE); 394 verify(mMockDelegate, times(expectedSuccess ? 1 : 0)).movePip(eq(TEST_MOVE_KEYCODE)); 395 } 396 assertMenuClosed()397 private void assertMenuClosed() { 398 assertMenuClosed(1); 399 } 400 assertMenuClosed(int times)401 private void assertMenuClosed(int times) { 402 assertMenuIsOpen(false); 403 verify(mMockDelegate, times(times)).onMenuClosed(); 404 verify(mMockTvPipMenuView, times(times)).transitionToMenuMode(eq(MODE_NO_MENU)); 405 verify(mMockTvPipBackgroundView, times(times)).transitionToMenuMode(eq(MODE_NO_MENU)); 406 } 407 assertMenuIsOpen(boolean open)408 private void assertMenuIsOpen(boolean open) { 409 assertTrue("The TV PiP menu should " + (open ? "" : "not ") + "be open, but it" 410 + " is in mode " + mTvPipMenuController.getMenuModeString(), 411 mTvPipMenuController.isMenuOpen() == open); 412 } 413 assertMenuIsInMoveMode()414 private void assertMenuIsInMoveMode() { 415 assertTrue("Expected MODE_MOVE_MENU, but got " + mTvPipMenuController.getMenuModeString(), 416 mTvPipMenuController.isInMoveMode()); 417 assertMenuIsOpen(true); 418 } 419 assertMenuIsInAllActionsMode()420 private void assertMenuIsInAllActionsMode() { 421 assertTrue("Expected MODE_ALL_ACTIONS_MENU, but got " 422 + mTvPipMenuController.getMenuModeString(), 423 mTvPipMenuController.isInAllActionsMode()); 424 assertMenuIsOpen(true); 425 } 426 427 private class TestTvPipMenuController extends TvPipMenuController { 428 TestTvPipMenuController()429 TestTvPipMenuController() { 430 super(mContext, mMockTvPipBoundsState, mMockSystemWindows, mMainHandler); 431 } 432 433 @Override createTvPipMenuView()434 TvPipMenuView createTvPipMenuView() { 435 return mMockTvPipMenuView; 436 } 437 438 @Override createTvPipBackgroundView()439 TvPipBackgroundView createTvPipBackgroundView() { 440 return mMockTvPipBackgroundView; 441 } 442 } 443 } 444