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