• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.car.rotary;
18 
19 import static android.view.Display.DEFAULT_DISPLAY;
20 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
21 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED;
22 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED;
23 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
24 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
25 import static android.view.accessibility.AccessibilityWindowInfo.TYPE_APPLICATION;
26 
27 import static com.android.car.ui.utils.DirectManipulationHelper.DIRECT_MANIPULATION;
28 import static com.android.car.ui.utils.RotaryConstants.ACTION_RESTORE_DEFAULT_FOCUS;
29 
30 import static com.google.common.truth.Truth.assertThat;
31 
32 import static org.mockito.ArgumentMatchers.any;
33 import static org.mockito.ArgumentMatchers.anyList;
34 import static org.mockito.Mockito.doAnswer;
35 import static org.mockito.Mockito.mock;
36 import static org.mockito.Mockito.times;
37 import static org.mockito.Mockito.verify;
38 import static org.mockito.Mockito.when;
39 import static org.testng.AssertJUnit.assertNull;
40 
41 import android.app.Activity;
42 import android.app.UiAutomation;
43 import android.car.CarOccupantZoneManager;
44 import android.car.input.CarInputManager;
45 import android.car.input.RotaryEvent;
46 import android.content.ComponentName;
47 import android.content.Intent;
48 import android.hardware.input.InputManager;
49 import android.view.KeyEvent;
50 import android.view.View;
51 import android.view.accessibility.AccessibilityEvent;
52 import android.view.accessibility.AccessibilityNodeInfo;
53 import android.view.accessibility.AccessibilityWindowInfo;
54 import android.widget.Button;
55 
56 import androidx.annotation.LayoutRes;
57 import androidx.test.ext.junit.runners.AndroidJUnit4;
58 import androidx.test.platform.app.InstrumentationRegistry;
59 import androidx.test.rule.ActivityTestRule;
60 
61 import com.android.car.ui.FocusParkingView;
62 import com.android.car.ui.utils.DirectManipulationHelper;
63 
64 import org.junit.After;
65 import org.junit.Before;
66 import org.junit.BeforeClass;
67 import org.junit.Test;
68 import org.junit.runner.RunWith;
69 import org.mockito.MockitoAnnotations;
70 import org.mockito.Spy;
71 
72 import java.util.ArrayList;
73 import java.util.Collections;
74 import java.util.List;
75 
76 @RunWith(AndroidJUnit4.class)
77 public class RotaryServiceTest {
78 
79     private final static String HOST_APP_PACKAGE_NAME = "host.app.package.name";
80     private final static String CLIENT_APP_PACKAGE_NAME = "client.app.package.name";
81     private static final int ROTATION_ACCELERATION_2X_MS = 50;
82     private static final int ROTATION_ACCELERATION_3X_MS = 25;
83 
84     private static UiAutomation sUiAutomation;
85 
86     private final List<AccessibilityNodeInfo> mNodes = new ArrayList<>();
87 
88     private AccessibilityNodeInfo mWindowRoot;
89     private ActivityTestRule<NavigatorTestActivity> mActivityRule;
90     private Intent mIntent;
91     private NodeBuilder mNodeBuilder;
92 
93     private @Spy
94     RotaryService mRotaryService;
95     private @Spy
96     Navigator mNavigator;
97 
98     @BeforeClass
setUpClass()99     public static void setUpClass() {
100         sUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
101     }
102 
103     @Before
setUp()104     public void setUp() {
105         mActivityRule = new ActivityTestRule<>(NavigatorTestActivity.class);
106         mIntent = new Intent();
107         mIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
108 
109         MockitoAnnotations.initMocks(this);
110         mRotaryService.setNavigator(mNavigator);
111         mRotaryService.setNodeCopier(MockNodeCopierProvider.get());
112         mRotaryService.setInputManager(mock(InputManager.class));
113         mRotaryService.setRotateAcceleration(ROTATION_ACCELERATION_2X_MS,
114                 ROTATION_ACCELERATION_3X_MS);
115         mNodeBuilder = new NodeBuilder(new ArrayList<>());
116     }
117 
118     @After
tearDown()119     public void tearDown() {
120         mActivityRule.finishActivity();
121         Utils.recycleNode(mWindowRoot);
122         Utils.recycleNodes(mNodes);
123     }
124 
125     /**
126      * Tests {@link RotaryService#initFocus()} in the following view tree:
127      * <pre>
128      *                      root
129      *                     /    \
130      *                    /      \
131      *       focusParkingView   focusArea
132      *                        /     |     \
133      *                       /      |       \
134      *               button1  defaultFocus  button3
135      *                         (focused)
136      * </pre>
137      * and {@link RotaryService#mFocusedNode} is already set to defaultFocus.
138      */
139     @Test
testInitFocus_alreadyInitialized()140     public void testInitFocus_alreadyInitialized() {
141         initActivity(R.layout.rotary_service_test_1_activity);
142 
143         AccessibilityWindowInfo window = new WindowBuilder()
144                 .setRoot(mWindowRoot)
145                 .setBoundsInScreen(mWindowRoot.getBoundsInScreen())
146                 .build();
147         List<AccessibilityWindowInfo> windows = Collections.singletonList(window);
148         when(mRotaryService.getWindows()).thenReturn(windows);
149 
150         AccessibilityNodeInfo defaultFocusNode = createNode("defaultFocus");
151         assertThat(defaultFocusNode.isFocused()).isTrue();
152         mRotaryService.setFocusedNode(defaultFocusNode);
153         assertThat(mRotaryService.getFocusedNode()).isEqualTo(defaultFocusNode);
154 
155         boolean consumed = mRotaryService.initFocus();
156         assertThat(consumed).isFalse();
157         assertThat(mRotaryService.getFocusedNode()).isEqualTo(defaultFocusNode);
158     }
159 
160     /**
161      * Tests {@link RotaryService#initFocus()} in the following view tree:
162      * <pre>
163      *                      root
164      *                     /    \
165      *                    /      \
166      *       focusParkingView   focusArea
167      *                        /     |     \
168      *                       /      |       \
169      *               button1  defaultFocus  button3
170      *                                      (focused)
171      * </pre>
172      * and {@link RotaryService#mFocusedNode} is not initialized.
173      */
174     @Test
testInitFocus_focusOnAlreadyFocusedView()175     public void testInitFocus_focusOnAlreadyFocusedView() {
176         initActivity(R.layout.rotary_service_test_1_activity);
177 
178         AccessibilityWindowInfo window = new WindowBuilder()
179                 .setRoot(mWindowRoot)
180                 .setBoundsInScreen(mWindowRoot.getBoundsInScreen())
181                 .build();
182         List<AccessibilityWindowInfo> windows = Collections.singletonList(window);
183         when(mRotaryService.getWindows()).thenReturn(windows);
184 
185         Activity activity = mActivityRule.getActivity();
186         Button button3 = activity.findViewById(R.id.button3);
187         button3.post(() -> button3.requestFocus());
188         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
189         assertThat(button3.isFocused()).isTrue();
190         assertNull(mRotaryService.getFocusedNode());
191 
192         boolean consumed = mRotaryService.initFocus();
193         AccessibilityNodeInfo button3Node = createNode("button3");
194         assertThat(mRotaryService.getFocusedNode()).isEqualTo(button3Node);
195         assertThat(consumed).isFalse();
196     }
197 
198     /**
199      * Tests {@link RotaryService#initFocus()} in the following view tree:
200      * <pre>
201      *                      root
202      *                     /    \
203      *                    /      \
204      *       focusParkingView   focusArea
205      *          (focused)      /     |     \
206      *                       /      |       \
207      *               button1  defaultFocus  button3
208      * </pre>
209      * and {@link RotaryService#mFocusedNode} is null.
210      */
211     @Test
testInitFocus_focusOnDefaultFocusView()212     public void testInitFocus_focusOnDefaultFocusView() {
213         initActivity(R.layout.rotary_service_test_1_activity);
214 
215         AccessibilityWindowInfo window = new WindowBuilder()
216                 .setRoot(mWindowRoot)
217                 .setBoundsInScreen(mWindowRoot.getBoundsInScreen())
218                 .build();
219         List<AccessibilityWindowInfo> windows = Collections.singletonList(window);
220         when(mRotaryService.getWindows()).thenReturn(windows);
221         when(mRotaryService.getRootInActiveWindow())
222                 .thenReturn(MockNodeCopierProvider.get().copy(mWindowRoot));
223 
224         // Move focus to the FocusParkingView.
225         Activity activity = mActivityRule.getActivity();
226         FocusParkingView fpv = activity.findViewById(R.id.focusParkingView);
227         fpv.setShouldRestoreFocus(false);
228         fpv.post(() -> fpv.requestFocus());
229         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
230         assertThat(fpv.isFocused()).isTrue();
231         assertNull(mRotaryService.getFocusedNode());
232 
233         boolean consumed = mRotaryService.initFocus();
234         AccessibilityNodeInfo defaultFocusNode = createNode("defaultFocus");
235         assertThat(mRotaryService.getFocusedNode()).isEqualTo(defaultFocusNode);
236         assertThat(consumed).isTrue();
237     }
238 
239     /**
240      * Tests {@link RotaryService#initFocus()} in the following view tree:
241      * <pre>
242      *                      root
243      *                     /    \
244      *                    /      \
245      *       focusParkingView   focusArea
246      *          (focused)      /     |     \
247      *                       /      |       \
248      *               button1  defaultFocus  button3
249      *                         (disabled)  (last touched)
250      * </pre>
251      * and {@link RotaryService#mFocusedNode} is null.
252      */
253     @Test
testInitFocus_focusOnLastTouchedView()254     public void testInitFocus_focusOnLastTouchedView() {
255         initActivity(R.layout.rotary_service_test_1_activity);
256 
257         AccessibilityWindowInfo window = new WindowBuilder()
258                 .setRoot(mWindowRoot)
259                 .setBoundsInScreen(mWindowRoot.getBoundsInScreen())
260                 .build();
261         List<AccessibilityWindowInfo> windows = Collections.singletonList(window);
262         when(mRotaryService.getWindows()).thenReturn(windows);
263         when(mRotaryService.getRootInActiveWindow())
264                 .thenReturn(MockNodeCopierProvider.get().copy(mWindowRoot));
265 
266         // The user touches button3. In reality it should enter touch mode therefore no view will
267         // be focused. To emulate this case, this test just moves focus to the FocusParkingView
268         // and sets last touched node to button3.
269         Activity activity = mActivityRule.getActivity();
270         FocusParkingView fpv = activity.findViewById(R.id.focusParkingView);
271         fpv.setShouldRestoreFocus(false);
272         fpv.post(fpv::requestFocus);
273         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
274         assertThat(fpv.isFocused()).isTrue();
275         AccessibilityNodeInfo button3Node = createNode("button3");
276         mRotaryService.setLastTouchedNode(button3Node);
277         assertNull(mRotaryService.getFocusedNode());
278 
279         boolean consumed = mRotaryService.initFocus();
280         assertThat(mRotaryService.getFocusedNode()).isEqualTo(button3Node);
281         assertThat(consumed).isTrue();
282     }
283 
284     /**
285      * Tests {@link RotaryService#initFocus()} in the following view tree:
286      * <pre>
287      *                      root
288      *                     /    \
289      *                    /      \
290      *       focusParkingView   focusArea
291      *          (focused)      /     |     \
292      *                       /      |       \
293      *               button1  defaultFocus  button3
294      *                         (disabled)
295      * </pre>
296      * and {@link RotaryService#mFocusedNode} is null.
297      */
298     @Test
testInitFocus_focusOnFirstFocusableView()299     public void testInitFocus_focusOnFirstFocusableView() {
300         initActivity(R.layout.rotary_service_test_1_activity);
301 
302         AccessibilityWindowInfo window = new WindowBuilder()
303                 .setRoot(mWindowRoot)
304                 .setBoundsInScreen(mWindowRoot.getBoundsInScreen())
305                 .build();
306         List<AccessibilityWindowInfo> windows = Collections.singletonList(window);
307         when(mRotaryService.getWindows()).thenReturn(windows);
308         when(mRotaryService.getRootInActiveWindow())
309                 .thenReturn(MockNodeCopierProvider.get().copy(mWindowRoot));
310 
311         // Move focus to the FocusParkingView and disable the default focus view.
312         Activity activity = mActivityRule.getActivity();
313         FocusParkingView fpv = activity.findViewById(R.id.focusParkingView);
314         Button defaultFocus = activity.findViewById(R.id.defaultFocus);
315         fpv.setShouldRestoreFocus(false);
316         fpv.post(() -> {
317             fpv.requestFocus();
318             defaultFocus.setEnabled(false);
319 
320         });
321         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
322         assertThat(fpv.isFocused()).isTrue();
323         assertThat(defaultFocus.isEnabled()).isFalse();
324         assertNull(mRotaryService.getFocusedNode());
325 
326         boolean consumed = mRotaryService.initFocus();
327         AccessibilityNodeInfo button1Node = createNode("button1");
328         assertThat(mRotaryService.getFocusedNode()).isEqualTo(button1Node);
329         assertThat(consumed).isTrue();
330     }
331 
332     /**
333      * Tests {@link RotaryService#initFocus()} in the following node tree:
334      * <pre>
335      *                  clientAppRoot
336      *                     /    \
337      *                    /      \
338      *              button1  surfaceView(focused)
339      *                             |
340      *                        hostAppRoot
341      *                           /    \
342      *                         /       \
343      *            focusParkingView     button2(focused)
344      * </pre>
345      * and {@link RotaryService#mFocusedNode} is null.
346      */
347     @Test
testInitFocus_focusOnHostNode()348     public void testInitFocus_focusOnHostNode() {
349         initActivity(R.layout.rotary_service_test_1_activity);
350 
351         mNavigator.addClientApp(CLIENT_APP_PACKAGE_NAME);
352         mNavigator.mSurfaceViewHelper.mHostApp = HOST_APP_PACKAGE_NAME;
353 
354         AccessibilityNodeInfo clientAppRoot = mNodeBuilder
355                 .setPackageName(CLIENT_APP_PACKAGE_NAME)
356                 .build();
357         AccessibilityNodeInfo button1 = mNodeBuilder
358                 .setParent(clientAppRoot)
359                 .setPackageName(CLIENT_APP_PACKAGE_NAME)
360                 .build();
361         AccessibilityNodeInfo surfaceView = mNodeBuilder
362                 .setParent(clientAppRoot)
363                 .setFocused(true)
364                 .setPackageName(CLIENT_APP_PACKAGE_NAME)
365                 .setClassName(Utils.SURFACE_VIEW_CLASS_NAME)
366                 .build();
367 
368         AccessibilityNodeInfo hostAppRoot = mNodeBuilder
369                 .setParent(surfaceView)
370                 .setPackageName(HOST_APP_PACKAGE_NAME)
371                 .build();
372         AccessibilityNodeInfo focusParkingView = mNodeBuilder
373                 .setParent(hostAppRoot)
374                 .setPackageName(HOST_APP_PACKAGE_NAME)
375                 .setFpv()
376                 .build();
377         AccessibilityNodeInfo button2 = mNodeBuilder
378                 .setParent(hostAppRoot)
379                 .setFocused(true)
380                 .setPackageName(HOST_APP_PACKAGE_NAME)
381                 .build();
382 
383         AccessibilityWindowInfo window = new WindowBuilder().setRoot(clientAppRoot).build();
384         List<AccessibilityWindowInfo> windows = Collections.singletonList(window);
385         when(mRotaryService.getWindows()).thenReturn(windows);
386 
387         boolean consumed = mRotaryService.initFocus();
388         assertThat(mRotaryService.getFocusedNode()).isEqualTo(button2);
389         assertThat(consumed).isFalse();
390     }
391 
392     /**
393      * Tests {@link RotaryService#onRotaryEvents} in the following view tree:
394      * <pre>
395      *                      root
396      *                     /    \
397      *                    /      \
398      *       focusParkingView   focusArea
399      *          (focused)      /     |     \
400      *                       /      |       \
401      *               button1  defaultFocus  button3
402      * </pre>
403      */
404     @Test
testOnRotaryEvents_withoutFocusedView()405     public void testOnRotaryEvents_withoutFocusedView() {
406         initActivity(R.layout.rotary_service_test_1_activity);
407 
408         AccessibilityWindowInfo window = new WindowBuilder()
409                 .setRoot(mWindowRoot)
410                 .setBoundsInScreen(mWindowRoot.getBoundsInScreen())
411                 .build();
412         List<AccessibilityWindowInfo> windows = Collections.singletonList(window);
413         when(mRotaryService.getWindows()).thenReturn(windows);
414         when(mRotaryService.getRootInActiveWindow())
415                 .thenReturn(MockNodeCopierProvider.get().copy(mWindowRoot));
416 
417         // Move focus to the FocusParkingView.
418         Activity activity = mActivityRule.getActivity();
419         FocusParkingView fpv = activity.findViewById(R.id.focusParkingView);
420         fpv.setShouldRestoreFocus(false);
421         fpv.post(() -> fpv.requestFocus());
422         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
423         assertThat(fpv.isFocused()).isTrue();
424         assertNull(mRotaryService.getFocusedNode());
425 
426         // Since there is no non-FocusParkingView focused, rotating the controller should
427         // initialize the focus.
428 
429         int inputType = CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION;
430         boolean clockwise = true;
431         long[] timestamps = new long[]{0};
432         RotaryEvent rotaryEvent = new RotaryEvent(inputType, clockwise, timestamps);
433         List<RotaryEvent> events = Collections.singletonList(rotaryEvent);
434 
435         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
436         mRotaryService.onRotaryEvents(validDisplayId, events);
437 
438         AccessibilityNodeInfo defaultFocusNode = createNode("defaultFocus");
439         assertThat(mRotaryService.getFocusedNode()).isEqualTo(defaultFocusNode);
440     }
441 
442     /**
443      * Tests {@link RotaryService#onRotaryEvents} in the following view tree:
444      * <pre>
445      *                      root
446      *                     /    \
447      *                    /      \
448      *       focusParkingView   focusArea
449      *                        /     |     \
450      *                       /      |       \
451      *               button1  defaultFocus  button3
452      *                          (focused)
453      * </pre>
454      */
455     @Test
testOnRotaryEvents_withFocusedView()456     public void testOnRotaryEvents_withFocusedView() {
457         initActivity(R.layout.rotary_service_test_1_activity);
458 
459         AccessibilityWindowInfo window = new WindowBuilder()
460                 .setRoot(mWindowRoot)
461                 .setBoundsInScreen(mWindowRoot.getBoundsInScreen())
462                 .build();
463         List<AccessibilityWindowInfo> windows = Collections.singletonList(window);
464         when(mRotaryService.getWindows()).thenReturn(windows);
465         doAnswer(invocation -> 1)
466                 .when(mRotaryService).getRotateAcceleration(any(Integer.class), any(Long.class));
467 
468         AccessibilityNodeInfo defaultFocusNode = createNode("defaultFocus");
469         assertThat(defaultFocusNode.isFocused()).isTrue();
470         mRotaryService.setFocusedNode(defaultFocusNode);
471         assertThat(mRotaryService.getFocusedNode()).isEqualTo(defaultFocusNode);
472 
473         // Since RotaryService#mFocusedNode is already initialized, rotating the controller
474         // clockwise should move the focus from defaultFocus to button3.
475 
476         int inputType = CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION;
477         boolean clockwise = true;
478         long[] timestamps = new long[]{0};
479         RotaryEvent rotaryEvent = new RotaryEvent(inputType, clockwise, timestamps);
480         List<RotaryEvent> events = Collections.singletonList(rotaryEvent);
481 
482         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
483         mRotaryService.onRotaryEvents(validDisplayId, events);
484 
485         AccessibilityNodeInfo button3Node = createNode("button3");
486         assertThat(mRotaryService.getFocusedNode()).isEqualTo(button3Node);
487 
488         // Rotating the controller clockwise again should do nothing because button3 is the last
489         // child of its ancestor FocusArea and the ancestor FocusArea doesn't support wrap-around.
490         mRotaryService.onRotaryEvents(validDisplayId, events);
491         assertThat(mRotaryService.getFocusedNode()).isEqualTo(button3Node);
492 
493         // Rotating the controller counterclockwise should move focus to defaultFocus.
494         clockwise = false;
495         rotaryEvent = new RotaryEvent(inputType, clockwise, timestamps);
496         events = Collections.singletonList(rotaryEvent);
497         mRotaryService.onRotaryEvents(validDisplayId, events);
498         assertThat(mRotaryService.getFocusedNode()).isEqualTo(defaultFocusNode);
499     }
500 
501     /**
502      * Tests {@link RotaryService#onRotaryEvents} in the following view tree:
503      * <pre>
504      *                        root
505      *                      /      \
506      *                     /        \
507      *      focusParkingView        focusArea
508      *                           /   |   |    \
509      *                         /     |   |       \
510      *             defaultFocus button2 button3 ... button6
511      *              (focused)
512      * </pre>
513      */
514     @Test
testOnRotaryEvents_acceleration()515     public void testOnRotaryEvents_acceleration() {
516         initActivity(R.layout.rotary_service_test_3_activity);
517 
518         AccessibilityWindowInfo window = new WindowBuilder()
519                 .setRoot(mWindowRoot)
520                 .setBoundsInScreen(mWindowRoot.getBoundsInScreen())
521                 .build();
522         List<AccessibilityWindowInfo> windows = Collections.singletonList(window);
523         when(mRotaryService.getWindows()).thenReturn(windows);
524 
525         AccessibilityNodeInfo defaultFocusNode = createNode("defaultFocus");
526         assertThat(defaultFocusNode.isFocused()).isTrue();
527         mRotaryService.setFocusedNode(defaultFocusNode);
528         assertThat(mRotaryService.getFocusedNode()).isEqualTo(defaultFocusNode);
529 
530 
531         // Rotating the controller clockwise slowly should move the focus from defaultFocus to
532         // button2.
533         int inputType = CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION;
534         int eventTime = ROTATION_ACCELERATION_2X_MS + 1;
535         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
536         mRotaryService.onRotaryEvents(validDisplayId,
537                 Collections.singletonList(
538                         new RotaryEvent(inputType, true, new long[]{eventTime})));
539         AccessibilityNodeInfo button2Node = createNode("button2");
540         assertThat(mRotaryService.getFocusedNode()).isEqualTo(button2Node);
541 
542         // Move focus back to defaultFocus.
543         eventTime += ROTATION_ACCELERATION_2X_MS + 1;
544         mRotaryService.onRotaryEvents(validDisplayId,
545                 Collections.singletonList(
546                         new RotaryEvent(inputType, false, new long[]{eventTime})));
547         assertThat(mRotaryService.getFocusedNode()).isEqualTo(defaultFocusNode);
548 
549         // Rotating the controller clockwise somewhat fast should move the focus from defaultFocus
550         // to button3.
551         eventTime += ROTATION_ACCELERATION_2X_MS;
552         mRotaryService.onRotaryEvents(validDisplayId,
553                 Collections.singletonList(
554                         new RotaryEvent(inputType, true, new long[]{eventTime})));
555         AccessibilityNodeInfo button3Node = createNode("button3");
556         assertThat(mRotaryService.getFocusedNode()).isEqualTo(button3Node);
557 
558         // Move focus back to defaultFocus.
559         eventTime += ROTATION_ACCELERATION_2X_MS;
560         mRotaryService.onRotaryEvents(validDisplayId,
561                 Collections.singletonList(
562                         new RotaryEvent(inputType, false, new long[]{eventTime})));
563 
564         // Rotating the controller clockwise very faster should move the focus from defaultFocus to
565         // button4.
566         eventTime += ROTATION_ACCELERATION_3X_MS;
567         mRotaryService.onRotaryEvents(validDisplayId,
568                 Collections.singletonList(
569                         new RotaryEvent(inputType, true, new long[]{eventTime})));
570         AccessibilityNodeInfo button4Node = createNode("button4");
571         assertThat(mRotaryService.getFocusedNode()).isEqualTo(button4Node);
572 
573         // Move focus back to defaultFocus.
574         eventTime += ROTATION_ACCELERATION_3X_MS;
575         mRotaryService.onRotaryEvents(validDisplayId,
576                 Collections.singletonList(
577                         new RotaryEvent(inputType, false, new long[]{eventTime})));
578 
579         // Rotating the controller two detents clockwise somewhat fast should move the focus from
580         // defaultFocus to button5.
581         mRotaryService.onRotaryEvents(validDisplayId, Collections.singletonList(
582                 new RotaryEvent(inputType, true,
583                         new long[]{eventTime + ROTATION_ACCELERATION_2X_MS,
584                                 eventTime + ROTATION_ACCELERATION_2X_MS * 2})));
585         AccessibilityNodeInfo button5Node = createNode("button5");
586         assertThat(mRotaryService.getFocusedNode()).isEqualTo(button5Node);
587     }
588 
589     /**
590      * Tests {@link RotaryService#nudgeTo(List, int)} in the following view tree:
591      * <pre>
592      *      The HUN window:
593      *
594      *      HUN FocusParkingView
595      *      ==========HUN focus area==========
596      *      =                                =
597      *      =  .............  .............  =
598      *      =  .           .  .           .  =
599      *      =  .hun button1.  .hun button2.  =
600      *      =  .           .  .           .  =
601      *      =  .............  .............  =
602      *      =                                =
603      *      ==================================
604      *
605      *      The app window:
606      *
607      *      app FocusParkingView
608      *      ===========focus area 1===========    ===========focus area 2===========
609      *      =                                =    =                                =
610      *      =  .............  .............  =    =  .............  .............  =
611      *      =  .           .  .           .  =    =  .           .  .           .  =
612      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
613      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
614      *      =  .............  .............  =    =  .............  .............  =
615      *      =                                =    =                                =
616      *      ==================================    ==================================
617      *
618      *      ===========focus area 3===========
619      *      =                                =
620      *      =  .............  .............  =
621      *      =  .           .  .           .  =
622      *      =  .app button3.  .  default  .  =
623      *      =  .           .  .   focus   .  =
624      *      =  .............  .............  =
625      *      =                                =
626      *      ==================================
627      * </pre>
628      */
629     @Test
testNudgeTo_nudgeToHun()630     public void testNudgeTo_nudgeToHun() {
631         initActivity(R.layout.rotary_service_test_2_activity);
632 
633         AccessibilityNodeInfo hunRoot = createNode("hun_root");
634         AccessibilityWindowInfo hunWindow = new WindowBuilder()
635                 .setRoot(hunRoot)
636                 .build();
637         AccessibilityNodeInfo appRoot = createNode("app_root");
638         AccessibilityWindowInfo appWindow = new WindowBuilder()
639                 .setRoot(appRoot)
640                 .build();
641         List<AccessibilityWindowInfo> windows = new ArrayList<>();
642         windows.add(hunWindow);
643         windows.add(appWindow);
644         when(mRotaryService.getWindows()).thenReturn(windows);
645 
646         AccessibilityNodeInfo hunButton1 = createNode("hun_button1");
647         AccessibilityNodeInfo mockHunFpv = mock(AccessibilityNodeInfo.class);
648         doAnswer(invocation -> {
649             mRotaryService.setFocusedNode(hunButton1);
650             return true;
651         }).when(mockHunFpv).performAction(ACTION_RESTORE_DEFAULT_FOCUS);
652         when(mockHunFpv.refresh()).thenReturn(true);
653         when(mockHunFpv.getClassName()).thenReturn(Utils.FOCUS_PARKING_VIEW_CLASS_NAME);
654         when(mNavigator.findFocusParkingViewInRoot(hunRoot)).thenReturn(mockHunFpv);
655         when(mNavigator.findHunWindow(anyList())).thenReturn(hunWindow);
656 
657         assertThat(mRotaryService.getFocusedNode()).isNotEqualTo(hunButton1);
658 
659         int hunNudgeDirection = mRotaryService.mHunNudgeDirection;
660         mRotaryService.nudgeTo(windows, hunNudgeDirection);
661         assertThat(mRotaryService.getFocusedNode()).isEqualTo(hunButton1);
662     }
663 
664     /**
665      * Tests {@link RotaryService#nudgeTo(List, int)} in the following view tree:
666      * <pre>
667      *      The HUN window:
668      *
669      *      HUN FocusParkingView
670      *      ==========HUN focus area==========
671      *      =                                =
672      *      =  .............  .............  =
673      *      =  .           .  .           .  =
674      *      =  .hun button1.  .hun button2.  =
675      *      =  .           .  .           .  =
676      *      =  .............  .............  =
677      *      =                                =
678      *      ==================================
679      *
680      *      The app window:
681      *
682      *      app FocusParkingView
683      *      ===========focus area 1===========    ===========focus area 2===========
684      *      =                                =    =                                =
685      *      =  .............  .............  =    =  .............  .............  =
686      *      =  .           .  .           .  =    =  .           .  .           .  =
687      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
688      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
689      *      =  .............  .............  =    =  .............  .............  =
690      *      =                                =    =                                =
691      *      ==================================    ==================================
692      *
693      *      ===========focus area 3===========
694      *      =                                =
695      *      =  .............  .............  =
696      *      =  .           .  .           .  =
697      *      =  .app button3.  .  default  .  =
698      *      =  .           .  .   focus   .  =
699      *      =  .............  .............  =
700      *      =                                =
701      *      ==================================
702      * </pre>
703      */
704     @Test
testNudgeTo_nudgeToNudgeShortcut_legacy()705     public void testNudgeTo_nudgeToNudgeShortcut_legacy() {
706         initActivity(R.layout.rotary_service_test_2_activity);
707 
708         AccessibilityNodeInfo appRoot = createNode("app_root");
709         AccessibilityWindowInfo appWindow = new WindowBuilder()
710                 .setRoot(appRoot)
711                 .build();
712         List<AccessibilityWindowInfo> windows = new ArrayList<>();
713         windows.add(appWindow);
714 
715         Activity activity = mActivityRule.getActivity();
716         Button appButton1 = activity.findViewById(R.id.app_button1);
717         appButton1.post(() -> appButton1.requestFocus());
718         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
719         assertThat(appButton1.isFocused()).isTrue();
720         AccessibilityNodeInfo appButton1Node = createNode("app_button1");
721         mRotaryService.setFocusedNode(appButton1Node);
722 
723         mRotaryService.nudgeTo(windows, View.FOCUS_RIGHT);
724         AccessibilityNodeInfo nudgeShortcut1Node = createNode("nudge_shortcut1");
725         assertThat(mRotaryService.getFocusedNode()).isEqualTo(nudgeShortcut1Node);
726     }
727 
728     /**
729      * Tests {@link RotaryService#nudgeTo(List, int)} in the following view tree:
730      * <pre>
731      *      The HUN window:
732      *
733      *      HUN FocusParkingView
734      *      ==========HUN focus area==========
735      *      =                                =
736      *      =  .............  .............  =
737      *      =  .           .  .           .  =
738      *      =  .hun button1.  .hun button2.  =
739      *      =  .           .  .           .  =
740      *      =  .............  .............  =
741      *      =                                =
742      *      ==================================
743      *
744      *      The app window:
745      *
746      *      app FocusParkingView
747      *      ===========focus area 1===========    ===========focus area 2===========
748      *      =                                =    =                                =
749      *      =  .............  .............  =    =  .............  .............  =
750      *      =  .           .  .           .  =    =  .           .  .           .  =
751      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
752      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
753      *      =  .............  .............  =    =  .............  .............  =
754      *      =                                =    =                                =
755      *      ==================================    ==================================
756      *
757      *      ===========focus area 3===========
758      *      =                                =
759      *      =  .............  .............  =
760      *      =  .           .  .           .  =
761      *      =  .app button3.  .  default  .  =
762      *      =  .           .  .   focus   .  =
763      *      =  .............  .............  =
764      *      =                                =
765      *      ==================================
766      * </pre>
767      */
768     @Test
testNudgeTo_nudgeToNudgeShortcut_new()769     public void testNudgeTo_nudgeToNudgeShortcut_new() {
770         initActivity(R.layout.rotary_service_test_2_activity);
771 
772         AccessibilityNodeInfo appRoot = createNode("app_root");
773         AccessibilityWindowInfo appWindow = new WindowBuilder()
774                 .setRoot(appRoot)
775                 .build();
776         List<AccessibilityWindowInfo> windows = new ArrayList<>();
777         windows.add(appWindow);
778 
779         Activity activity = mActivityRule.getActivity();
780         Button appButton2 = activity.findViewById(R.id.app_button2);
781         appButton2.post(() -> appButton2.requestFocus());
782         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
783         assertThat(appButton2.isFocused()).isTrue();
784         AccessibilityNodeInfo appButton2Node = createNode("app_button2");
785         mRotaryService.setFocusedNode(appButton2Node);
786 
787         mRotaryService.nudgeTo(windows, View.FOCUS_RIGHT);
788         AccessibilityNodeInfo nudgeShortcut2Node = createNode("nudge_shortcut2");
789         assertThat(mRotaryService.getFocusedNode()).isEqualTo(nudgeShortcut2Node);
790     }
791 
792     /**
793      * Tests {@link RotaryService#nudgeTo(List, int)} in the following view tree:
794      * <pre>
795      *      The HUN window:
796      *
797      *      HUN FocusParkingView
798      *      ==========HUN focus area==========
799      *      =                                =
800      *      =  .............  .............  =
801      *      =  .           .  .           .  =
802      *      =  .hun button1.  .hun button2.  =
803      *      =  .           .  .           .  =
804      *      =  .............  .............  =
805      *      =                                =
806      *      ==================================
807      *
808      *      The app window:
809      *
810      *      app FocusParkingView
811      *      ===========focus area 1===========    ===========focus area 2===========
812      *      =                                =    =                                =
813      *      =  .............  .............  =    =  .............  .............  =
814      *      =  .           .  .           .  =    =  .           .  .           .  =
815      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
816      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
817      *      =  .............  .............  =    =  .............  .............  =
818      *      =                                =    =                                =
819      *      ==================================    ==================================
820      *
821      *      ===========focus area 3===========
822      *      =                                =
823      *      =  .............  .............  =
824      *      =  .           .  .           .  =
825      *      =  .app button3.  .  default  .  =
826      *      =  .           .  .   focus   .  =
827      *      =  .............  .............  =
828      *      =                                =
829      *      ==================================
830      * </pre>
831      */
832     @Test
testNudgeTo_nudgeToUserSpecifiedTarget()833     public void testNudgeTo_nudgeToUserSpecifiedTarget() {
834         initActivity(R.layout.rotary_service_test_2_activity);
835 
836         AccessibilityNodeInfo appRoot = createNode("app_root");
837         AccessibilityWindowInfo appWindow = new WindowBuilder()
838                 .setRoot(appRoot)
839                 .build();
840         List<AccessibilityWindowInfo> windows = new ArrayList<>();
841         windows.add(appWindow);
842 
843         Activity activity = mActivityRule.getActivity();
844         Button appButton2 = activity.findViewById(R.id.app_button2);
845         appButton2.post(() -> appButton2.requestFocus());
846         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
847         assertThat(appButton2.isFocused()).isTrue();
848         AccessibilityNodeInfo appButton2Node = createNode("app_button2");
849         mRotaryService.setFocusedNode(appButton2Node);
850 
851         mRotaryService.nudgeTo(windows, View.FOCUS_LEFT);
852         AccessibilityNodeInfo appDefaultFocusNode = createNode("app_default_focus");
853         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appDefaultFocusNode);
854     }
855 
856     /**
857      * Tests {@link RotaryService#nudgeTo(List, int)} in the following view tree:
858      * <pre>
859      *      The HUN window:
860      *
861      *      HUN FocusParkingView
862      *      ==========HUN focus area==========
863      *      =                                =
864      *      =  .............  .............  =
865      *      =  .           .  .           .  =
866      *      =  .hun button1.  .hun button2.  =
867      *      =  .           .  .           .  =
868      *      =  .............  .............  =
869      *      =                                =
870      *      ==================================
871      *
872      *      The app window:
873      *
874      *      app FocusParkingView
875      *      ===========focus area 1===========    ===========focus area 2===========
876      *      =                                =    =                                =
877      *      =  .............  .............  =    =  .............  .............  =
878      *      =  .           .  .           .  =    =  .           .  .           .  =
879      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
880      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
881      *      =  .............  .............  =    =  .............  .............  =
882      *      =                                =    =                                =
883      *      ==================================    ==================================
884      *
885      *      ===========focus area 3===========
886      *      =                                =
887      *      =  .............  .............  =
888      *      =  .           .  .           .  =
889      *      =  .app button3.  .  default  .  =
890      *      =  .           .  .   focus   .  =
891      *      =  .............  .............  =
892      *      =                                =
893      *      ==================================
894      * </pre>
895      */
896     @Test
testNudgeTo_nudgeToNearestTarget()897     public void testNudgeTo_nudgeToNearestTarget() {
898         initActivity(R.layout.rotary_service_test_2_activity);
899 
900         AccessibilityNodeInfo appRoot = createNode("app_root");
901         AccessibilityWindowInfo appWindow = new WindowBuilder()
902                 .setRoot(appRoot)
903                 .build();
904         List<AccessibilityWindowInfo> windows = new ArrayList<>();
905         windows.add(appWindow);
906 
907         Activity activity = mActivityRule.getActivity();
908         Button appButton3 = activity.findViewById(R.id.app_button3);
909         appButton3.post(() -> appButton3.requestFocus());
910         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
911         assertThat(appButton3.isFocused()).isTrue();
912         AccessibilityNodeInfo appButton3Node = createNode("app_button3");
913         AccessibilityNodeInfo appFocusArea3Node = createNode("app_focus_area3");
914         mRotaryService.setFocusedNode(appButton3Node);
915 
916         AccessibilityNodeInfo appFocusArea1Node = createNode("app_focus_area1");
917         when(mNavigator.findNudgeTargetFocusArea(
918                 windows, appButton3Node, appFocusArea3Node, View.FOCUS_UP))
919                 .thenReturn(AccessibilityNodeInfo.obtain(appFocusArea1Node));
920 
921         mRotaryService.nudgeTo(windows, View.FOCUS_UP);
922         AccessibilityNodeInfo appButton1Node = createNode("app_button1");
923         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appButton1Node);
924     }
925 
926     /**
927      * Tests {@link RotaryService#onKeyEvents} in the following view tree:
928      * <pre>
929      *      The HUN window:
930      *
931      *      hun FocusParkingView
932      *      ==========HUN focus area==========
933      *      =                                =
934      *      =  .............  .............  =
935      *      =  .           .  .           .  =
936      *      =  .hun button1.  .hun button2.  =
937      *      =  .           .  .           .  =
938      *      =  .............  .............  =
939      *      =                                =
940      *      ==================================
941      *
942      *      The app window:
943      *
944      *      app FocusParkingView
945      *      ===========focus area 1===========    ===========focus area 2===========
946      *      =                                =    =                                =
947      *      =  .............  .............  =    =  .............  .............  =
948      *      =  .           .  .           .  =    =  .           .  .           .  =
949      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
950      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
951      *      =  .............  .............  =    =  .............  .............  =
952      *      =                                =    =                                =
953      *      ==================================    ==================================
954      *
955      *      ===========focus area 3===========
956      *      =                                =
957      *      =  .............  .............  =
958      *      =  .           .  .           .  =
959      *      =  .app button3.  .  default  .  =
960      *      =  . (source)  .  .   focus   .  =
961      *      =  .............  .............  =
962      *      =                                =
963      *      ==================================
964      * </pre>
965      */
966     @Test
testOnKeyEvents_nudgeUp_moveFocus()967     public void testOnKeyEvents_nudgeUp_moveFocus() {
968         initActivity(R.layout.rotary_service_test_2_activity);
969 
970         AccessibilityNodeInfo appRoot = createNode("app_root");
971         AccessibilityWindowInfo appWindow = new WindowBuilder()
972                 .setRoot(appRoot)
973                 .build();
974         List<AccessibilityWindowInfo> windows = new ArrayList<>();
975         windows.add(appWindow);
976         when(mRotaryService.getWindows()).thenReturn(windows);
977 
978         Activity activity = mActivityRule.getActivity();
979         Button appButton3 = activity.findViewById(R.id.app_button3);
980         appButton3.post(() -> appButton3.requestFocus());
981         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
982         assertThat(appButton3.isFocused()).isTrue();
983         AccessibilityNodeInfo appButton3Node = createNode("app_button3");
984         AccessibilityNodeInfo appFocusArea3Node = createNode("app_focus_area3");
985         mRotaryService.setFocusedNode(appButton3Node);
986 
987         AccessibilityNodeInfo appFocusArea1Node = createNode("app_focus_area1");
988         when(mNavigator.findNudgeTargetFocusArea(
989                 windows, appButton3Node, appFocusArea3Node, View.FOCUS_UP))
990                 .thenReturn(AccessibilityNodeInfo.obtain(appFocusArea1Node));
991 
992         // Nudge up the controller.
993         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
994         KeyEvent nudgeUpEventActionDown =
995         new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP);
996         mRotaryService.onKeyEvents(validDisplayId,
997                 Collections.singletonList(nudgeUpEventActionDown));
998         KeyEvent nudgeUpEventActionUp =
999                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP);
1000         mRotaryService.onKeyEvents(validDisplayId, Collections.singletonList(nudgeUpEventActionUp));
1001 
1002         // It should move focus to the FocusArea above.
1003         AccessibilityNodeInfo appButton1Node = createNode("app_button1");
1004         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appButton1Node);
1005     }
1006 
1007     /**
1008      * Tests {@link RotaryService#onKeyEvents} in the following view tree:
1009      * <pre>
1010      *      The HUN window:
1011      *
1012      *      hun FocusParkingView
1013      *      ==========HUN focus area==========
1014      *      =                                =
1015      *      =  .............  .............  =
1016      *      =  .           .  .           .  =
1017      *      =  .hun button1.  .hun button2.  =
1018      *      =  .           .  .           .  =
1019      *      =  .............  .............  =
1020      *      =                                =
1021      *      ==================================
1022      *
1023      *      The app window:
1024      *
1025      *      app FocusParkingView
1026      *      ===========focus area 1===========    ===========focus area 2===========
1027      *      =                                =    =                                =
1028      *      =  .............  .............  =    =  .............  .............  =
1029      *      =  .           .  .           .  =    =  .           .  .           .  =
1030      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1031      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1032      *      =  .............  .............  =    =  .............  .............  =
1033      *      =                                =    =                                =
1034      *      ==================================    ==================================
1035      *
1036      *      ===========focus area 3===========
1037      *      =                                =
1038      *      =  .............  .............  =
1039      *      =  .           .  .           .  =
1040      *      =  .app button3.  .  default  .  =
1041      *      =  .           .  .   focus   .  =
1042      *      =  .............  .............  =
1043      *      =                                =
1044      *      ==================================
1045      * </pre>
1046      */
1047     @Test
testOnKeyEvents_nudgeUp_initFocus()1048     public void testOnKeyEvents_nudgeUp_initFocus() {
1049         initActivity(R.layout.rotary_service_test_2_activity);
1050 
1051         // RotaryService.mFocusedNode is not initialized.
1052         AccessibilityNodeInfo appRoot = createNode("app_root");
1053         AccessibilityWindowInfo appWindow = new WindowBuilder()
1054                 .setRoot(appRoot)
1055                 .build();
1056         List<AccessibilityWindowInfo> windows = new ArrayList<>();
1057         windows.add(appWindow);
1058         when(mRotaryService.getWindows()).thenReturn(windows);
1059         when(mRotaryService.getRootInActiveWindow())
1060                 .thenReturn(MockNodeCopierProvider.get().copy(mWindowRoot));
1061 
1062         // Move focus to the FocusParkingView.
1063         Activity activity = mActivityRule.getActivity();
1064         FocusParkingView fpv = activity.findViewById(R.id.app_fpv);
1065         fpv.setShouldRestoreFocus(false);
1066         fpv.post(() -> fpv.requestFocus());
1067         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
1068         assertThat(fpv.isFocused()).isTrue();
1069         assertNull(mRotaryService.getFocusedNode());
1070 
1071         // Nudge up the controller.
1072         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
1073         KeyEvent nudgeUpEventActionDown =
1074                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP);
1075         mRotaryService.onKeyEvents(validDisplayId,
1076                 Collections.singletonList(nudgeUpEventActionDown));
1077         KeyEvent nudgeUpEventActionUp =
1078                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP);
1079         mRotaryService.onKeyEvents(validDisplayId, Collections.singletonList(nudgeUpEventActionUp));
1080 
1081         // It should initialize the focus.
1082         AccessibilityNodeInfo appDefaultFocusNode = createNode("app_default_focus");
1083         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appDefaultFocusNode);
1084     }
1085 
1086     /**
1087      * Tests {@link RotaryService#onKeyEvents} in the following view tree:
1088      * <pre>
1089      *      The HUN window:
1090      *
1091      *      hun FocusParkingView
1092      *      ==========HUN focus area==========
1093      *      =                                =
1094      *      =  .............  .............  =
1095      *      =  .           .  .           .  =
1096      *      =  .hun button1.  .hun button2.  =
1097      *      =  . (focused) .  .           .  =
1098      *      =  .............  .............  =
1099      *      =                                =
1100      *      ==================================
1101      *
1102      *      The app window:
1103      *
1104      *      app FocusParkingView
1105      *      ===========focus area 1===========    ===========focus area 2===========
1106      *      =                                =    =                                =
1107      *      =  .............  .............  =    =  .............  .............  =
1108      *      =  .           .  .           .  =    =  .           .  .           .  =
1109      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1110      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1111      *      =  .............  .............  =    =  .............  .............  =
1112      *      =                                =    =                                =
1113      *      ==================================    ==================================
1114      *
1115      *      ===========focus area 3===========
1116      *      =                                =
1117      *      =  .............  .............  =
1118      *      =  .           .  .           .  =
1119      *      =  .app button3.  .  default  .  =
1120      *      =  .           .  .   focus   .  =
1121      *      =  .............  .............  =
1122      *      =                                =
1123      *      ==================================
1124      * </pre>
1125      */
1126     @Test
testOnKeyEvents_nudgeToHunEscapeNudgeDirection_leaveTheHun()1127     public void testOnKeyEvents_nudgeToHunEscapeNudgeDirection_leaveTheHun() {
1128         initActivity(R.layout.rotary_service_test_2_activity);
1129 
1130         AccessibilityNodeInfo appRoot = createNode("app_root");
1131         AccessibilityWindowInfo appWindow = new WindowBuilder()
1132                 .setRoot(appRoot)
1133                 .build();
1134         AccessibilityNodeInfo hunRoot = createNode("hun_root");
1135         AccessibilityWindowInfo hunWindow = new WindowBuilder()
1136                 .setRoot(hunRoot)
1137                 .build();
1138         List<AccessibilityWindowInfo> windows = new ArrayList<>();
1139         windows.add(appWindow);
1140         windows.add(hunWindow);
1141         when(mRotaryService.getWindows()).thenReturn(windows);
1142 
1143         // A Button in the HUN window is focused.
1144         Activity activity = mActivityRule.getActivity();
1145         Button hunButton1 = activity.findViewById(R.id.hun_button1);
1146         hunButton1.post(() -> hunButton1.requestFocus());
1147         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
1148         assertThat(hunButton1.isFocused()).isTrue();
1149         AccessibilityNodeInfo hunButton1Node = createNode("hun_button1");
1150         AccessibilityNodeInfo hunFocusAreaNode = createNode("hun_focus_area");
1151         mRotaryService.setFocusedNode(hunButton1Node);
1152 
1153         // Set HUN escape nudge direction to View.FOCUS_DOWN.
1154         mRotaryService.mHunEscapeNudgeDirection = View.FOCUS_DOWN;
1155 
1156         AccessibilityNodeInfo appFocusArea3Node = createNode("app_focus_area3");
1157         when(mNavigator.findNudgeTargetFocusArea(
1158                 windows, hunButton1Node, hunFocusAreaNode, View.FOCUS_DOWN))
1159                 .thenReturn(AccessibilityNodeInfo.obtain(appFocusArea3Node));
1160 
1161         // Nudge down the controller.
1162         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
1163         KeyEvent nudgeEventActionDown =
1164                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN);
1165         mRotaryService.onKeyEvents(validDisplayId, Collections.singletonList(nudgeEventActionDown));
1166         KeyEvent nudgeEventActionUp =
1167                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN);
1168         mRotaryService.onKeyEvents(validDisplayId, Collections.singletonList(nudgeEventActionUp));
1169 
1170         // Nudging down should exit the HUN and focus in app_focus_area3.
1171         AccessibilityNodeInfo appDefaultFocusNode = createNode("app_default_focus");
1172         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appDefaultFocusNode);
1173     }
1174 
1175     /**
1176      * Tests {@link RotaryService#onKeyEvents} in the following view tree:
1177      * <pre>
1178      *      The HUN window:
1179      *
1180      *      hun FocusParkingView
1181      *      ==========HUN focus area==========
1182      *      =                                =
1183      *      =  .............  .............  =
1184      *      =  .           .  .           .  =
1185      *      =  .hun button1.  .hun button2.  =
1186      *      =  . (focused) .  .           .  =
1187      *      =  .............  .............  =
1188      *      =                                =
1189      *      ==================================
1190      *
1191      *      The app window:
1192      *
1193      *      app FocusParkingView
1194      *      ===========focus area 1===========    ===========focus area 2===========
1195      *      =                                =    =                                =
1196      *      =  .............  .............  =    =  .............  .............  =
1197      *      =  .           .  .           .  =    =  .           .  .           .  =
1198      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1199      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1200      *      =  .............  .............  =    =  .............  .............  =
1201      *      =                                =    =                                =
1202      *      ==================================    ==================================
1203      *
1204      *      ===========focus area 3===========
1205      *      =                                =
1206      *      =  .............  .............  =
1207      *      =  .           .  .           .  =
1208      *      =  .app button3.  .  default  .  =
1209      *      =  .           .  .   focus   .  =
1210      *      =  .............  .............  =
1211      *      =                                =
1212      *      ==================================
1213      * </pre>
1214      */
1215     @Test
testOnKeyEvents_nudgeToNonHunEscapeNudgeDirection_stayInTheHun()1216     public void testOnKeyEvents_nudgeToNonHunEscapeNudgeDirection_stayInTheHun() {
1217         initActivity(R.layout.rotary_service_test_2_activity);
1218 
1219         AccessibilityNodeInfo appRoot = createNode("app_root");
1220         AccessibilityWindowInfo appWindow = new WindowBuilder()
1221                 .setRoot(appRoot)
1222                 .build();
1223         AccessibilityNodeInfo hunRoot = createNode("hun_root");
1224         AccessibilityWindowInfo hunWindow = new WindowBuilder()
1225                 .setRoot(hunRoot)
1226                 .build();
1227         List<AccessibilityWindowInfo> windows = new ArrayList<>();
1228         windows.add(appWindow);
1229         windows.add(hunWindow);
1230         when(mRotaryService.getWindows()).thenReturn(windows);
1231 
1232         // A Button in the HUN window is focused.
1233         Activity activity = mActivityRule.getActivity();
1234         Button hunButton1 = activity.findViewById(R.id.hun_button1);
1235         hunButton1.post(() -> hunButton1.requestFocus());
1236         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
1237         assertThat(hunButton1.isFocused()).isTrue();
1238         AccessibilityNodeInfo hunButton1Node = createNode("hun_button1");
1239         AccessibilityNodeInfo hunFocusAreaNode = createNode("hun_focus_area");
1240         mRotaryService.setFocusedNode(hunButton1Node);
1241 
1242         // Set HUN escape nudge direction to View.FOCUS_UP.
1243         mRotaryService.mHunEscapeNudgeDirection = View.FOCUS_UP;
1244 
1245         // RotaryService.mFocusedNode.getWindow() returns null in the test, so just pass null value
1246         // to simplify the test.
1247         when(mNavigator.isHunWindow(null)).thenReturn(true);
1248 
1249         AccessibilityNodeInfo appFocusArea3Node = createNode("app_focus_area3");
1250         when(mNavigator.findNudgeTargetFocusArea(
1251                 windows, hunButton1Node, hunFocusAreaNode, View.FOCUS_DOWN))
1252                 .thenReturn(AccessibilityNodeInfo.obtain(appFocusArea3Node));
1253 
1254         // Nudge down the controller.
1255         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
1256         KeyEvent nudgeEventActionDown =
1257                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN);
1258         mRotaryService.onKeyEvents(validDisplayId, Collections.singletonList(nudgeEventActionDown));
1259         KeyEvent nudgeEventActionUp =
1260                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN);
1261         mRotaryService.onKeyEvents(validDisplayId, Collections.singletonList(nudgeEventActionUp));
1262 
1263         // Nudging down should stay in the HUN because HUN escape nudge direction is View.FOCUS_UP.
1264         assertThat(mRotaryService.getFocusedNode()).isEqualTo(hunButton1Node);
1265     }
1266 
1267     /**
1268      * Tests {@link RotaryService#onKeyEvents} in the following view tree:
1269      * <pre>
1270      *      The HUN window:
1271      *
1272      *      hun FocusParkingView
1273      *      ==========HUN focus area==========
1274      *      =                                =
1275      *      =  .............  .............  =
1276      *      =  .           .  .           .  =
1277      *      =  .hun button1.  .hun button2.  =
1278      *      =  .           .  .           .  =
1279      *      =  .............  .............  =
1280      *      =                                =
1281      *      ==================================
1282      *
1283      *      The app window:
1284      *
1285      *      app FocusParkingView
1286      *      ===========focus area 1===========    ===========focus area 2===========
1287      *      =                                =    =                                =
1288      *      =  .............  .............  =    =  .............  .............  =
1289      *      =  .           .  .           .  =    =  .           .  .           .  =
1290      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1291      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1292      *      =  .............  .............  =    =  .............  .............  =
1293      *      =                                =    =                                =
1294      *      ==================================    ==================================
1295      *
1296      *      ===========focus area 3===========
1297      *      =                                =
1298      *      =  .............  .............  =
1299      *      =  .           .  .           .  =
1300      *      =  .app button3.  .  default  .  =
1301      *      =  .           .  .   focus   .  =
1302      *      =  .............  .............  =
1303      *      =                                =
1304      *      ==================================
1305      * </pre>
1306      */
1307     @Test
testOnKeyEvents_centerButtonClick_initFocus()1308     public void testOnKeyEvents_centerButtonClick_initFocus() {
1309         initActivity(R.layout.rotary_service_test_2_activity);
1310 
1311         // RotaryService.mFocusedNode is not initialized.
1312         AccessibilityNodeInfo appRoot = createNode("app_root");
1313         AccessibilityWindowInfo appWindow = new WindowBuilder()
1314                 .setRoot(appRoot)
1315                 .build();
1316         List<AccessibilityWindowInfo> windows = new ArrayList<>();
1317         windows.add(appWindow);
1318         when(mRotaryService.getWindows()).thenReturn(windows);
1319         when(mRotaryService.getRootInActiveWindow())
1320                 .thenReturn(MockNodeCopierProvider.get().copy(mWindowRoot));
1321         assertThat(mRotaryService.getFocusedNode()).isNull();
1322 
1323         // Click the center button of the controller.
1324         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
1325         KeyEvent centerButtonEventActionDown =
1326                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
1327         mRotaryService.onKeyEvents(validDisplayId,
1328                 Collections.singletonList(centerButtonEventActionDown));
1329         KeyEvent centerButtonEventActionUp =
1330                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
1331         mRotaryService.onKeyEvents(validDisplayId,
1332                 Collections.singletonList(centerButtonEventActionUp));
1333 
1334         // It should initialize the focus.
1335         AccessibilityNodeInfo appDefaultFocusNode = createNode("app_default_focus");
1336         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appDefaultFocusNode);
1337     }
1338 
1339     /**
1340      * Tests {@link RotaryService#onKeyEvents} in the following view tree:
1341      * <pre>
1342      *      The HUN window:
1343      *
1344      *      hun FocusParkingView
1345      *      ==========HUN focus area==========
1346      *      =                                =
1347      *      =  .............  .............  =
1348      *      =  .           .  .           .  =
1349      *      =  .hun button1.  .hun button2.  =
1350      *      =  .           .  .           .  =
1351      *      =  .............  .............  =
1352      *      =                                =
1353      *      ==================================
1354      *
1355      *      The app window:
1356      *
1357      *      app FocusParkingView
1358      *      ===========focus area 1===========    ===========focus area 2===========
1359      *      =                                =    =                                =
1360      *      =  .............  .............  =    =  .............  .............  =
1361      *      =  .           .  .           .  =    =  .           .  .           .  =
1362      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1363      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1364      *      =  .............  .............  =    =  .............  .............  =
1365      *      =                                =    =                                =
1366      *      ==================================    ==================================
1367      *
1368      *      ===========focus area 3===========
1369      *      =                                =
1370      *      =  .............  .............  =
1371      *      =  .           .  .           .  =
1372      *      =  .app button3.  .  default  .  =
1373      *      =  . (focused) .  .   focus   .  =
1374      *      =  .............  .............  =
1375      *      =                                =
1376      *      ==================================
1377      * </pre>
1378      */
1379     @Test
testOnKeyEvents_centerButtonClickInAppWindow_injectDpadCenterEvent()1380     public void testOnKeyEvents_centerButtonClickInAppWindow_injectDpadCenterEvent() {
1381         initActivity(R.layout.rotary_service_test_2_activity);
1382 
1383         AccessibilityNodeInfo appRoot = createNode("app_root");
1384         AccessibilityWindowInfo appWindow = new WindowBuilder()
1385                 .setRoot(appRoot)
1386                 .setType(TYPE_APPLICATION)
1387                 .build();
1388         List<AccessibilityWindowInfo> windows = new ArrayList<>();
1389         windows.add(appWindow);
1390         when(mRotaryService.getWindows()).thenReturn(windows);
1391         when(mRotaryService.getRootInActiveWindow())
1392                 .thenReturn(MockNodeCopierProvider.get().copy(mWindowRoot));
1393 
1394         AccessibilityNodeInfo mockAppButton3Node = mNodeBuilder
1395                 .setFocused(true)
1396                 .setWindow(appWindow)
1397                 .build();
1398         mRotaryService.setFocusedNode(mockAppButton3Node);
1399 
1400         assertThat(mRotaryService.mIgnoreViewClickedNode).isNull();
1401 
1402         // Click the center button of the controller.
1403         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
1404         KeyEvent centerButtonEventActionDown =
1405                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
1406         mRotaryService.onKeyEvents(validDisplayId,
1407                 Collections.singletonList(centerButtonEventActionDown));
1408         KeyEvent centerButtonEventActionUp =
1409                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
1410         mRotaryService.onKeyEvents(validDisplayId,
1411                 Collections.singletonList(centerButtonEventActionUp));
1412 
1413         // RotaryService should inject KEYCODE_DPAD_CENTER event because mockAppButton3Node is in
1414         // the application window.
1415         verify(mRotaryService, times(1))
1416                 .injectKeyEvent(KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.ACTION_DOWN);
1417         verify(mRotaryService, times(1))
1418                 .injectKeyEvent(KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.ACTION_UP);
1419         assertThat(mRotaryService.mIgnoreViewClickedNode).isEqualTo(mockAppButton3Node);
1420         assertThat(mRotaryService.getFocusedNode()).isEqualTo(mockAppButton3Node);
1421     }
1422 
1423     /**
1424      * Tests {@link RotaryService#onKeyEvents} in the following view tree:
1425      * <pre>
1426      *      The HUN window:
1427      *
1428      *      hun FocusParkingView
1429      *      ==========HUN focus area==========
1430      *      =                                =
1431      *      =  .............  .............  =
1432      *      =  .           .  .           .  =
1433      *      =  .hun button1.  .hun button2.  =
1434      *      =  .           .  .           .  =
1435      *      =  .............  .............  =
1436      *      =                                =
1437      *      ==================================
1438      *
1439      *      The app window:
1440      *
1441      *      app FocusParkingView
1442      *      ===========focus area 1===========    ===========focus area 2===========
1443      *      =                                =    =                                =
1444      *      =  .............  .............  =    =  .............  .............  =
1445      *      =  .           .  .           .  =    =  .           .  .           .  =
1446      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1447      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1448      *      =  .............  .............  =    =  .............  .............  =
1449      *      =                                =    =                                =
1450      *      ==================================    ==================================
1451      *
1452      *      ==============focus area 3==============
1453      *      =                                      =
1454      *      =  ...................                 =
1455      *      =  .     WebView     .  .............  =
1456      *      =  .  .............  .  .           .  =
1457      *      =  .  .app button3.  .  .  default  .  =
1458      *      =  .  . (focused) .  .  .   focus   .  =
1459      *      =  .  .............  .  .............  =
1460      *      =  ...................                 =
1461      *      =                                      =
1462      *      ========================================
1463      * </pre>
1464      */
1465     @Test
testOnKeyEvents_centerButtonClickInAppWindow_webViewFocused_injectEnterKeyEvent()1466     public void testOnKeyEvents_centerButtonClickInAppWindow_webViewFocused_injectEnterKeyEvent() {
1467         initActivity(R.layout.rotary_service_test_2_activity);
1468 
1469         AccessibilityNodeInfo appRoot = createNode("app_root");
1470         AccessibilityWindowInfo appWindow = new WindowBuilder()
1471                 .setRoot(appRoot)
1472                 .setType(TYPE_APPLICATION)
1473                 .build();
1474         List<AccessibilityWindowInfo> windows = new ArrayList<>();
1475         windows.add(appWindow);
1476         when(mRotaryService.getWindows()).thenReturn(windows);
1477         when(mRotaryService.getRootInActiveWindow())
1478                 .thenReturn(MockNodeCopierProvider.get().copy(mWindowRoot));
1479 
1480         AccessibilityNodeInfo mockWebViewParent = mNodeBuilder
1481                 .setClassName(Utils.WEB_VIEW_CLASS_NAME)
1482                 .setWindow(appWindow)
1483                 .build();
1484 
1485         AccessibilityNodeInfo mockAppButton3Node = mNodeBuilder
1486                 .setFocused(true)
1487                 .setParent(mockWebViewParent)
1488                 .setWindow(appWindow)
1489                 .build();
1490         mRotaryService.setFocusedNode(mockAppButton3Node);
1491 
1492         assertThat(mRotaryService.mIgnoreViewClickedNode).isNull();
1493 
1494         // Click the center button of the controller.
1495         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
1496         KeyEvent centerButtonEventActionDown =
1497                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
1498         mRotaryService.onKeyEvents(validDisplayId,
1499                 Collections.singletonList(centerButtonEventActionDown));
1500         KeyEvent centerButtonEventActionUp =
1501                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
1502         mRotaryService.onKeyEvents(validDisplayId,
1503                 Collections.singletonList(centerButtonEventActionUp));
1504 
1505         // RotaryService should inject KEYCODE_ENTER event because mockAppButton3Node is in
1506         // the application window, its parent is a WebView, and it is not checkable.
1507         verify(mRotaryService, times(1))
1508                 .injectKeyEvent(KeyEvent.KEYCODE_ENTER, KeyEvent.ACTION_DOWN);
1509         verify(mRotaryService, times(1))
1510                 .injectKeyEvent(KeyEvent.KEYCODE_ENTER, KeyEvent.ACTION_UP);
1511         assertThat(mRotaryService.mIgnoreViewClickedNode).isEqualTo(mockAppButton3Node);
1512         assertThat(mRotaryService.getFocusedNode()).isEqualTo(mockAppButton3Node);
1513     }
1514 
1515     /**
1516      * Tests {@link RotaryService#onKeyEvents} in the following view tree:
1517      * <pre>
1518      *      The HUN window:
1519      *
1520      *      hun FocusParkingView
1521      *      ==========HUN focus area==========
1522      *      =                                =
1523      *      =  .............  .............  =
1524      *      =  .           .  .           .  =
1525      *      =  .hun button1.  .hun button2.  =
1526      *      =  .           .  .           .  =
1527      *      =  .............  .............  =
1528      *      =                                =
1529      *      ==================================
1530      *
1531      *      The app window:
1532      *
1533      *      app FocusParkingView
1534      *      ===========focus area 1===========    ===========focus area 2===========
1535      *      =                                =    =                                =
1536      *      =  .............  .............  =    =  .............  .............  =
1537      *      =  .           .  .           .  =    =  .           .  .           .  =
1538      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1539      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1540      *      =  .............  .............  =    =  .............  .............  =
1541      *      =                                =    =                                =
1542      *      ==================================    ==================================
1543      *
1544      *      ==============focus area 3==============
1545      *      =                                      =
1546      *      =  ...................                 =
1547      *      =  .     WebView     .  .............  =
1548      *      =  .  .............  .  .           .  =
1549      *      =  .  .app button3.  .  .  default  .  =
1550      *      =  .  . (focused) .  .  .   focus   .  =
1551      *      =  .  .............  .  .............  =
1552      *      =  ...................                 =
1553      *      =                                      =
1554      *      ========================================
1555      * </pre>
1556      */
1557     @Test
1558     public void
testOnKeyEvents_centerButtonClickInAppWindow_webViewFocused_isCheckable_injectSpaceKeyEvent()1559     testOnKeyEvents_centerButtonClickInAppWindow_webViewFocused_isCheckable_injectSpaceKeyEvent() {
1560         initActivity(R.layout.rotary_service_test_2_activity);
1561 
1562         AccessibilityNodeInfo appRoot = createNode("app_root");
1563         AccessibilityWindowInfo appWindow = new WindowBuilder()
1564                 .setRoot(appRoot)
1565                 .setType(TYPE_APPLICATION)
1566                 .build();
1567         List<AccessibilityWindowInfo> windows = new ArrayList<>();
1568         windows.add(appWindow);
1569         when(mRotaryService.getWindows()).thenReturn(windows);
1570         when(mRotaryService.getRootInActiveWindow())
1571                 .thenReturn(MockNodeCopierProvider.get().copy(mWindowRoot));
1572 
1573         AccessibilityNodeInfo mockWebViewParent = mNodeBuilder
1574             .setClassName(Utils.WEB_VIEW_CLASS_NAME)
1575             .setWindow(appWindow)
1576             .build();
1577 
1578         AccessibilityNodeInfo mockAppButton3Node = mNodeBuilder
1579                 .setFocused(true)
1580                 .setCheckable(true)
1581                 .setParent(mockWebViewParent)
1582                 .setWindow(appWindow)
1583                 .build();
1584         mRotaryService.setFocusedNode(mockAppButton3Node);
1585 
1586         assertThat(mRotaryService.mIgnoreViewClickedNode).isNull();
1587 
1588         // Click the center button of the controller.
1589         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
1590         KeyEvent centerButtonEventActionDown =
1591                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
1592         mRotaryService.onKeyEvents(validDisplayId,
1593                 Collections.singletonList(centerButtonEventActionDown));
1594         KeyEvent centerButtonEventActionUp =
1595                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
1596         mRotaryService.onKeyEvents(validDisplayId,
1597                 Collections.singletonList(centerButtonEventActionUp));
1598 
1599         // RotaryService should inject KEYCODE_SPACE event because mockAppButton3Node is in
1600         // the application window, its parent is a WebView, and it is checkable.
1601         verify(mRotaryService, times(1))
1602                 .injectKeyEvent(KeyEvent.KEYCODE_SPACE, KeyEvent.ACTION_DOWN);
1603         verify(mRotaryService, times(1))
1604                 .injectKeyEvent(KeyEvent.KEYCODE_SPACE, KeyEvent.ACTION_UP);
1605         assertThat(mRotaryService.mIgnoreViewClickedNode).isEqualTo(mockAppButton3Node);
1606         assertThat(mRotaryService.getFocusedNode()).isEqualTo(mockAppButton3Node);
1607     }
1608 
1609     /**
1610      * Tests {@link RotaryService#onKeyEvents} in the following view tree:
1611      * <pre>
1612      *      The HUN window:
1613      *
1614      *      hun FocusParkingView
1615      *      ==========HUN focus area==========
1616      *      =                                =
1617      *      =  .............  .............  =
1618      *      =  .           .  .           .  =
1619      *      =  .hun button1.  .hun button2.  =
1620      *      =  .           .  .           .  =
1621      *      =  .............  .............  =
1622      *      =                                =
1623      *      ==================================
1624      *
1625      *      The app window:
1626      *
1627      *      app FocusParkingView
1628      *      ===========focus area 1===========    ===========focus area 2===========
1629      *      =                                =    =                                =
1630      *      =  .............  .............  =    =  .............  .............  =
1631      *      =  .           .  .           .  =    =  .           .  .           .  =
1632      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1633      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1634      *      =  .............  .............  =    =  .............  .............  =
1635      *      =                                =    =                                =
1636      *      ==================================    ==================================
1637      *
1638      *      ===========focus area 3===========
1639      *      =                                =
1640      *      =  .............  .............  =
1641      *      =  .           .  .           .  =
1642      *      =  .app button3.  .  default  .  =
1643      *      =  . (focused) .  .   focus   .  =
1644      *      =  .............  .............  =
1645      *      =                                =
1646      *      ==================================
1647      * </pre>
1648      */
1649     @Test
testOnKeyEvents_centerButtonClickInSystemWindow_performActionClick()1650     public void testOnKeyEvents_centerButtonClickInSystemWindow_performActionClick() {
1651         initActivity(R.layout.rotary_service_test_2_activity);
1652 
1653         AccessibilityNodeInfo appRoot = createNode("app_root");
1654         AccessibilityWindowInfo appWindow = new WindowBuilder()
1655                 .setRoot(appRoot)
1656                 .build();
1657         List<AccessibilityWindowInfo> windows = new ArrayList<>();
1658         windows.add(appWindow);
1659         when(mRotaryService.getWindows()).thenReturn(windows);
1660         when(mRotaryService.getRootInActiveWindow())
1661                 .thenReturn(MockNodeCopierProvider.get().copy(mWindowRoot));
1662 
1663         Activity activity = mActivityRule.getActivity();
1664         Button appButton3 = activity.findViewById(R.id.app_button3);
1665         appButton3.setOnClickListener(v -> v.setActivated(true));
1666         appButton3.post(() -> appButton3.requestFocus());
1667         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
1668         assertThat(appButton3.isFocused()).isTrue();
1669         AccessibilityNodeInfo appButton3Node = createNode("app_button3");
1670         mRotaryService.setFocusedNode(appButton3Node);
1671         mRotaryService.mLongPressMs = 400;
1672 
1673         assertThat(appButton3.isActivated()).isFalse();
1674         assertThat(mRotaryService.mIgnoreViewClickedNode).isNull();
1675 
1676         // Click the center button of the controller.
1677         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
1678         KeyEvent centerButtonEventActionDown =
1679                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
1680         mRotaryService.onKeyEvents(validDisplayId,
1681                 Collections.singletonList(centerButtonEventActionDown));
1682         KeyEvent centerButtonEventActionUp =
1683                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
1684         mRotaryService.onKeyEvents(validDisplayId,
1685                 Collections.singletonList(centerButtonEventActionUp));
1686 
1687         // appButton3Node.getWindow() will return null (because the test doesn't have the permission
1688         // to create an AccessibilityWindowInfo), so appButton3Node isn't considered in the
1689         // application window. Instead, it's considered in the system window. So RotaryService
1690         // should perform ACTION_CLICK on it.
1691         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
1692         assertThat(appButton3.isActivated()).isTrue();
1693         assertThat(mRotaryService.mIgnoreViewClickedNode).isEqualTo(appButton3Node);
1694         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appButton3Node);
1695     }
1696 
1697     /**
1698      * Tests {@link RotaryService#onKeyEvents} in the following view tree:
1699      * <pre>
1700      *      The HUN window:
1701      *
1702      *      hun FocusParkingView
1703      *      ==========HUN focus area==========
1704      *      =                                =
1705      *      =  .............  .............  =
1706      *      =  .           .  .           .  =
1707      *      =  .hun button1.  .hun button2.  =
1708      *      =  .           .  .           .  =
1709      *      =  .............  .............  =
1710      *      =                                =
1711      *      ==================================
1712      *
1713      *      The app window:
1714      *
1715      *      app FocusParkingView
1716      *      ===========focus area 1===========    ===========focus area 2===========
1717      *      =                                =    =                                =
1718      *      =  .............  .............  =    =  .............  .............  =
1719      *      =  .           .  .           .  =    =  .           .  .           .  =
1720      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1721      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1722      *      =  .............  .............  =    =  .............  .............  =
1723      *      =                                =    =                                =
1724      *      ==================================    ==================================
1725      *
1726      *      ===========focus area 3===========
1727      *      =                                =
1728      *      =  .............  .............  =
1729      *      =  .           .  .           .  =
1730      *      =  .app button3.  .  default  .  =
1731      *      =  . (focused) .  .   focus   .  =
1732      *      =  .............  .............  =
1733      *      =                                =
1734      *      ==================================
1735      * </pre>
1736      */
1737     @Test
testOnKeyEvents_centerButtonLongClickInSystemWindow_performActionLongClick()1738     public void testOnKeyEvents_centerButtonLongClickInSystemWindow_performActionLongClick() {
1739         initActivity(R.layout.rotary_service_test_2_activity);
1740 
1741         AccessibilityNodeInfo appRoot = createNode("app_root");
1742         AccessibilityWindowInfo appWindow = new WindowBuilder()
1743                 .setRoot(appRoot)
1744                 .build();
1745         List<AccessibilityWindowInfo> windows = new ArrayList<>();
1746         windows.add(appWindow);
1747         when(mRotaryService.getWindows()).thenReturn(windows);
1748         when(mRotaryService.getRootInActiveWindow())
1749                 .thenReturn(MockNodeCopierProvider.get().copy(mWindowRoot));
1750 
1751         Activity activity = mActivityRule.getActivity();
1752         Button appButton3 = activity.findViewById(R.id.app_button3);
1753         appButton3.setOnLongClickListener(v -> {
1754             v.setActivated(true);
1755             return true;
1756         });
1757         appButton3.post(() -> appButton3.requestFocus());
1758         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
1759         assertThat(appButton3.isFocused()).isTrue();
1760         AccessibilityNodeInfo appButton3Node = createNode("app_button3");
1761         mRotaryService.setFocusedNode(appButton3Node);
1762         mRotaryService.mLongPressMs = 0;
1763 
1764         assertThat(appButton3.isActivated()).isFalse();
1765         assertThat(mRotaryService.mIgnoreViewClickedNode).isNull();
1766 
1767         // Click the center button of the controller.
1768         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
1769         KeyEvent centerButtonEventActionDown =
1770                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
1771         mRotaryService.onKeyEvents(validDisplayId,
1772                 Collections.singletonList(centerButtonEventActionDown));
1773         KeyEvent centerButtonEventActionUp =
1774                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
1775         mRotaryService.onKeyEvents(validDisplayId,
1776                 Collections.singletonList(centerButtonEventActionUp));
1777         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
1778 
1779         // appButton3Node.getWindow() will return null (because the test doesn't have the permission
1780         // to create an AccessibilityWindowInfo), so appButton3Node isn't considered in the
1781         // application window. Instead, it's considered in the system window. So RotaryService
1782         // should perform ACTION_LONG_CLICK on it.
1783         assertThat(appButton3.isActivated()).isTrue();
1784         assertThat(mRotaryService.mIgnoreViewClickedNode).isNull();
1785         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appButton3Node);
1786     }
1787 
1788     /**
1789      * Tests {@link RotaryService#onAccessibilityEvent} in the following view tree:
1790      * <pre>
1791      *      The HUN window:
1792      *
1793      *      hun FocusParkingView
1794      *      ==========HUN focus area==========
1795      *      =                                =
1796      *      =  .............  .............  =
1797      *      =  .           .  .           .  =
1798      *      =  .hun button1.  .hun button2.  =
1799      *      =  . (focused) .  .           .  =
1800      *      =  .............  .............  =
1801      *      =                                =
1802      *      ==================================
1803      *
1804      *      The app window:
1805      *
1806      *      app FocusParkingView
1807      *      ===========focus area 1===========    ===========focus area 2===========
1808      *      =                                =    =                                =
1809      *      =  .............  .............  =    =  .............  .............  =
1810      *      =  .           .  .           .  =    =  .           .  .           .  =
1811      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1812      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1813      *      =  .............  .............  =    =  .............  .............  =
1814      *      =                                =    =                                =
1815      *      ==================================    ==================================
1816      *
1817      *      ===========focus area 3===========
1818      *      =                                =
1819      *      =  .............  .............  =
1820      *      =  .           .  .           .  =
1821      *      =  .app button3.  .  default  .  =
1822      *      =  .           .  .   focus   .  =
1823      *      =  .............  .............  =
1824      *      =                                =
1825      *      ==================================
1826      * </pre>
1827      */
1828     @Test
testOnAccessibilityEvent_typeViewFocused()1829     public void testOnAccessibilityEvent_typeViewFocused() {
1830         initActivity(R.layout.rotary_service_test_2_activity);
1831 
1832         // The app focuses appDefaultFocus, then the accessibility framework sends a
1833         // TYPE_VIEW_FOCUSED event.
1834         // RotaryService should set mFocusedNode to appDefaultFocusNode.
1835 
1836         Activity activity = mActivityRule.getActivity();
1837         Button appDefaultFocus = activity.findViewById(R.id.app_default_focus);
1838         AccessibilityNodeInfo appDefaultFocusNode = createNode("app_default_focus");
1839         assertThat(appDefaultFocus.isFocused()).isTrue();
1840         assertThat(mRotaryService.getFocusedNode()).isNull();
1841 
1842         mRotaryService.mInRotaryMode = true;
1843         AccessibilityEvent event = mock(AccessibilityEvent.class);
1844         when(event.getSource()).thenReturn(AccessibilityNodeInfo.obtain(appDefaultFocusNode));
1845         when(event.getEventType()).thenReturn(TYPE_VIEW_FOCUSED);
1846         mRotaryService.onAccessibilityEvent(event);
1847 
1848         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appDefaultFocusNode);
1849     }
1850 
1851     /**
1852      * Tests {@link RotaryService#onAccessibilityEvent} in the following view tree:
1853      * <pre>
1854      *      The HUN window:
1855      *
1856      *      hun FocusParkingView
1857      *      ==========HUN focus area==========
1858      *      =                                =
1859      *      =  .............  .............  =
1860      *      =  .           .  .           .  =
1861      *      =  .hun button1.  .hun button2.  =
1862      *      =  . (focused) .  .           .  =
1863      *      =  .............  .............  =
1864      *      =                                =
1865      *      ==================================
1866      *
1867      *      The app window:
1868      *
1869      *      app FocusParkingView
1870      *      ===========focus area 1===========    ===========focus area 2===========
1871      *      =                                =    =                                =
1872      *      =  .............  .............  =    =  .............  .............  =
1873      *      =  .           .  .           .  =    =  .           .  .           .  =
1874      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1875      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1876      *      =  .............  .............  =    =  .............  .............  =
1877      *      =                                =    =                                =
1878      *      ==================================    ==================================
1879      *
1880      *      ===========focus area 3===========
1881      *      =                                =
1882      *      =  .............  .............  =
1883      *      =  .           .  .           .  =
1884      *      =  .app button3.  .  default  .  =
1885      *      =  .           .  .   focus   .  =
1886      *      =  .............  .............  =
1887      *      =                                =
1888      *      ==================================
1889      * </pre>
1890      */
1891     @Test
testOnAccessibilityEvent_typeViewFocused2()1892     public void testOnAccessibilityEvent_typeViewFocused2() {
1893         initActivity(R.layout.rotary_service_test_2_activity);
1894 
1895         // RotaryService focuses appDefaultFocus, then the app focuses on the FocusParkingView
1896         // and the accessibility framework sends a TYPE_VIEW_FOCUSED event.
1897         // RotaryService should set mFocusedNode to null.
1898 
1899         Activity activity = mActivityRule.getActivity();
1900         Button appDefaultFocus = activity.findViewById(R.id.app_default_focus);
1901         AccessibilityNodeInfo appDefaultFocusNode = createNode("app_default_focus");
1902         assertThat(appDefaultFocus.isFocused()).isTrue();
1903         mRotaryService.setFocusedNode(appDefaultFocusNode);
1904         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appDefaultFocusNode);
1905 
1906         mRotaryService.mInRotaryMode = true;
1907 
1908         AccessibilityNodeInfo fpvNode = createNode("app_fpv");
1909         AccessibilityEvent event = mock(AccessibilityEvent.class);
1910         when(event.getSource()).thenReturn(AccessibilityNodeInfo.obtain(fpvNode));
1911         when(event.getEventType()).thenReturn(TYPE_VIEW_FOCUSED);
1912         mRotaryService.onAccessibilityEvent(event);
1913 
1914         assertThat(mRotaryService.getFocusedNode()).isNull();
1915     }
1916 
1917     /**
1918      * Tests {@link RotaryService#onAccessibilityEvent} in the following view tree:
1919      * <pre>
1920      *      The HUN window:
1921      *
1922      *      hun FocusParkingView
1923      *      ==========HUN focus area==========
1924      *      =                                =
1925      *      =  .............  .............  =
1926      *      =  .           .  .           .  =
1927      *      =  .hun button1.  .hun button2.  =
1928      *      =  . (focused) .  .           .  =
1929      *      =  .............  .............  =
1930      *      =                                =
1931      *      ==================================
1932      *
1933      *      The app window:
1934      *
1935      *      app FocusParkingView
1936      *      ===========focus area 1===========    ===========focus area 2===========
1937      *      =                                =    =                                =
1938      *      =  .............  .............  =    =  .............  .............  =
1939      *      =  .           .  .           .  =    =  .           .  .           .  =
1940      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
1941      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
1942      *      =  .............  .............  =    =  .............  .............  =
1943      *      =                                =    =                                =
1944      *      ==================================    ==================================
1945      *
1946      *      ===========focus area 3===========
1947      *      =                                =
1948      *      =  .............  .............  =
1949      *      =  .           .  .           .  =
1950      *      =  .app button3.  .  default  .  =
1951      *      =  .           .  .   focus   .  =
1952      *      =  .............  .............  =
1953      *      =                                =
1954      *      ==================================
1955      * </pre>
1956      */
1957     @Test
testOnAccessibilityEvent_typeViewClicked()1958     public void testOnAccessibilityEvent_typeViewClicked() {
1959         initActivity(R.layout.rotary_service_test_2_activity);
1960 
1961         // The focus is on appDefaultFocus, then the user clicks it via the rotary controller.
1962 
1963         Activity activity = mActivityRule.getActivity();
1964         Button appDefaultFocus = activity.findViewById(R.id.app_default_focus);
1965         AccessibilityNodeInfo appDefaultFocusNode = createNode("app_default_focus");
1966         assertThat(appDefaultFocus.isFocused()).isTrue();
1967         mRotaryService.setFocusedNode(appDefaultFocusNode);
1968         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appDefaultFocusNode);
1969 
1970         mRotaryService.mInRotaryMode = true;
1971         mRotaryService.mIgnoreViewClickedNode = AccessibilityNodeInfo.obtain(appDefaultFocusNode);
1972 
1973         AccessibilityEvent event = mock(AccessibilityEvent.class);
1974         when(event.getSource()).thenReturn(AccessibilityNodeInfo.obtain(appDefaultFocusNode));
1975         when(event.getEventType()).thenReturn(TYPE_VIEW_CLICKED);
1976         when(event.getEventTime()).thenReturn(-1l);
1977         mRotaryService.onAccessibilityEvent(event);
1978 
1979         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appDefaultFocusNode);
1980         assertThat(mRotaryService.mIgnoreViewClickedNode).isNull();
1981         assertThat(mRotaryService.mLastTouchedNode).isNull();
1982     }
1983 
1984     /**
1985      * Tests {@link RotaryService#onAccessibilityEvent} in the following view tree:
1986      * <pre>
1987      *      The HUN window:
1988      *
1989      *      hun FocusParkingView
1990      *      ==========HUN focus area==========
1991      *      =                                =
1992      *      =  .............  .............  =
1993      *      =  .           .  .           .  =
1994      *      =  .hun button1.  .hun button2.  =
1995      *      =  . (focused) .  .           .  =
1996      *      =  .............  .............  =
1997      *      =                                =
1998      *      ==================================
1999      *
2000      *      The app window:
2001      *
2002      *      app FocusParkingView
2003      *      ===========focus area 1===========    ===========focus area 2===========
2004      *      =                                =    =                                =
2005      *      =  .............  .............  =    =  .............  .............  =
2006      *      =  .           .  .           .  =    =  .           .  .           .  =
2007      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
2008      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
2009      *      =  .............  .............  =    =  .............  .............  =
2010      *      =                                =    =                                =
2011      *      ==================================    ==================================
2012      *
2013      *      ===========focus area 3===========
2014      *      =                                =
2015      *      =  .............  .............  =
2016      *      =  .           .  .           .  =
2017      *      =  .app button3.  .  default  .  =
2018      *      =  .           .  .   focus   .  =
2019      *      =  .............  .............  =
2020      *      =                                =
2021      *      ==================================
2022      * </pre>
2023      */
2024     @Test
testOnAccessibilityEvent_typeViewClicked2()2025     public void testOnAccessibilityEvent_typeViewClicked2() {
2026         initActivity(R.layout.rotary_service_test_2_activity);
2027 
2028         // The focus is on appDefaultFocus, then the user clicks appButton3 via the touch screen.
2029 
2030         Activity activity = mActivityRule.getActivity();
2031         Button appDefaultFocus = activity.findViewById(R.id.app_default_focus);
2032         AccessibilityNodeInfo appDefaultFocusNode = createNode("app_default_focus");
2033         assertThat(appDefaultFocus.isFocused()).isTrue();
2034         mRotaryService.setFocusedNode(appDefaultFocusNode);
2035         assertThat(mRotaryService.getFocusedNode()).isEqualTo(appDefaultFocusNode);
2036 
2037         mRotaryService.mInRotaryMode = true;
2038 
2039         AccessibilityNodeInfo appButton3Node = createNode("app_button3");
2040         AccessibilityEvent event = mock(AccessibilityEvent.class);
2041         when(event.getSource()).thenReturn(AccessibilityNodeInfo.obtain(appButton3Node));
2042         when(event.getEventType()).thenReturn(TYPE_VIEW_CLICKED);
2043         when(event.getEventTime()).thenReturn(-1l);
2044         mRotaryService.onAccessibilityEvent(event);
2045 
2046         assertThat(mRotaryService.getFocusedNode()).isNull();
2047         assertThat(mRotaryService.mIgnoreViewClickedNode).isNull();
2048         assertThat(mRotaryService.mLastTouchedNode).isEqualTo(appButton3Node);
2049     }
2050 
2051     @Test
testOnAccessibilityEvent_typeWindowStateChanged()2052     public void testOnAccessibilityEvent_typeWindowStateChanged() {
2053         AccessibilityWindowInfo window = mock(AccessibilityWindowInfo.class);
2054         when(window.getType()).thenReturn(TYPE_APPLICATION);
2055         when(window.getDisplayId()).thenReturn(DEFAULT_DISPLAY);
2056 
2057         AccessibilityNodeInfo node = mock(AccessibilityNodeInfo.class);
2058         when(node.getWindow()).thenReturn(window);
2059 
2060         AccessibilityEvent event = mock(AccessibilityEvent.class);
2061         when(event.getSource()).thenReturn(node);
2062         when(event.getEventType()).thenReturn(TYPE_WINDOW_STATE_CHANGED);
2063         final String packageName = "package.name";
2064         final String className = "class.name";
2065         when(event.getPackageName()).thenReturn(packageName);
2066         when(event.getClassName()).thenReturn(className);
2067         mRotaryService.onAccessibilityEvent(event);
2068 
2069         ComponentName foregroundActivity = new ComponentName(packageName, className);
2070         assertThat(mRotaryService.mForegroundActivity).isEqualTo(foregroundActivity);
2071     }
2072 
2073     /**
2074      * Tests Direct Manipulation mode in the following view tree:
2075      * <pre>
2076      *      The HUN window:
2077      *
2078      *      hun FocusParkingView
2079      *      ==========HUN focus area==========
2080      *      =                                =
2081      *      =  .............  .............  =
2082      *      =  .           .  .           .  =
2083      *      =  .hun button1.  .hun button2.  =
2084      *      =  .           .  .           .  =
2085      *      =  .............  .............  =
2086      *      =                                =
2087      *      ==================================
2088      *
2089      *      The app window:
2090      *
2091      *      app FocusParkingView
2092      *      ===========focus area 1===========    ===========focus area 2===========
2093      *      =                                =    =                                =
2094      *      =  .............  .............  =    =  .............  .............  =
2095      *      =  .           .  .           .  =    =  .           .  .           .  =
2096      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
2097      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
2098      *      =  .............  .............  =    =  .............  .............  =
2099      *      =                                =    =                                =
2100      *      ==================================    ==================================
2101      *
2102      *      ===========focus area 3===========
2103      *      =                                =
2104      *      =  .............  .............  =
2105      *      =  .           .  .           .  =
2106      *      =  .app button3.  .  default  .  =
2107      *      =  . (focused) .  .   focus   .  =
2108      *      =  .............  .............  =
2109      *      =                                =
2110      *      ==================================
2111      * </pre>
2112      */
2113     @Test
testDirectManipulationMode1()2114     public void testDirectManipulationMode1() {
2115         initActivity(R.layout.rotary_service_test_2_activity);
2116 
2117         Activity activity = mActivityRule.getActivity();
2118         Button appButton3 = activity.findViewById(R.id.app_button3);
2119         DirectManipulationHelper.setSupportsRotateDirectly(appButton3, true);
2120         appButton3.post(() -> appButton3.requestFocus());
2121         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
2122         assertThat(appButton3.isFocused()).isTrue();
2123         AccessibilityNodeInfo appButton3Node = createNode("app_button3");
2124         mRotaryService.setFocusedNode(appButton3Node);
2125         mRotaryService.mInRotaryMode = true;
2126         assertThat(mRotaryService.mInDirectManipulationMode).isFalse();
2127         assertThat(appButton3.isSelected()).isFalse();
2128 
2129         // Click the center button of the controller.
2130         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
2131         KeyEvent centerButtonEventActionDown =
2132                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
2133         mRotaryService.onKeyEvents(validDisplayId,
2134                 Collections.singletonList(centerButtonEventActionDown));
2135         KeyEvent centerButtonEventActionUp =
2136                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
2137         mRotaryService.onKeyEvents(validDisplayId,
2138                 Collections.singletonList(centerButtonEventActionUp));
2139 
2140         // RotaryService should enter Direct Manipulation mode because appButton3Node
2141         // supports rotate directly.
2142         assertThat(mRotaryService.mInDirectManipulationMode).isTrue();
2143         assertThat(appButton3.isSelected()).isTrue();
2144 
2145         // Click the back button of the controller.
2146         KeyEvent backButtonEventActionDown =
2147                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
2148         mRotaryService.onKeyEvents(validDisplayId,
2149                 Collections.singletonList(backButtonEventActionDown));
2150         KeyEvent backButtonEventActionUp =
2151                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
2152         mRotaryService.onKeyEvents(validDisplayId,
2153                 Collections.singletonList(backButtonEventActionUp));
2154 
2155         // RotaryService should exit Direct Manipulation mode because appButton3Node
2156         // supports rotate directly.
2157         assertThat(mRotaryService.mInDirectManipulationMode).isFalse();
2158         assertThat(appButton3.isSelected()).isFalse();
2159     }
2160 
2161     /**
2162      * Tests Direct Manipulation mode in the following view tree:
2163      * <pre>
2164      *      The HUN window:
2165      *
2166      *      hun FocusParkingView
2167      *      ==========HUN focus area==========
2168      *      =                                =
2169      *      =  .............  .............  =
2170      *      =  .           .  .           .  =
2171      *      =  .hun button1.  .hun button2.  =
2172      *      =  .           .  .           .  =
2173      *      =  .............  .............  =
2174      *      =                                =
2175      *      ==================================
2176      *
2177      *      The app window:
2178      *
2179      *      app FocusParkingView
2180      *      ===========focus area 1===========    ===========focus area 2===========
2181      *      =                                =    =                                =
2182      *      =  .............  .............  =    =  .............  .............  =
2183      *      =  .           .  .           .  =    =  .           .  .           .  =
2184      *      =  .app button1.  .   nudge   .  =    =  .app button2.  .   nudge   .  =
2185      *      =  .           .  . shortcut1 .  =    =  .           .  . shortcut2 .  =
2186      *      =  .............  .............  =    =  .............  .............  =
2187      *      =                                =    =                                =
2188      *      ==================================    ==================================
2189      *
2190      *      ===========focus area 3===========
2191      *      =                                =
2192      *      =  .............  .............  =
2193      *      =  .           .  .           .  =
2194      *      =  .app button3.  .  default  .  =
2195      *      =  . (focused) .  .   focus   .  =
2196      *      =  .............  .............  =
2197      *      =                                =
2198      *      ==================================
2199      * </pre>
2200      */
2201     @Test
testDirectManipulationMode2()2202     public void testDirectManipulationMode2() {
2203         initActivity(R.layout.rotary_service_test_2_activity);
2204 
2205         Activity activity = mActivityRule.getActivity();
2206         Button appButton3 = activity.findViewById(R.id.app_button3);
2207         appButton3.post(() -> appButton3.requestFocus());
2208         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
2209         assertThat(appButton3.isFocused()).isTrue();
2210         AccessibilityNodeInfo appButton3Node = createNode("app_button3");
2211         mRotaryService.setFocusedNode(appButton3Node);
2212         mRotaryService.mInRotaryMode = true;
2213         when(mRotaryService.isInApplicationWindow(appButton3Node)).thenReturn(true);
2214         assertThat(mRotaryService.mInDirectManipulationMode).isFalse();
2215         assertThat(mRotaryService.mIgnoreViewClickedNode).isNull();
2216 
2217         // Click the center button of the controller.
2218         int validDisplayId = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
2219         KeyEvent centerButtonEventActionDown =
2220                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
2221         mRotaryService.onKeyEvents(validDisplayId,
2222                 Collections.singletonList(centerButtonEventActionDown));
2223         KeyEvent centerButtonEventActionUp =
2224                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
2225         mRotaryService.onKeyEvents(validDisplayId,
2226                 Collections.singletonList(centerButtonEventActionUp));
2227 
2228         // RotaryService should inject KEYCODE_DPAD_CENTER event because appButton3Node doesn't
2229         // support rotate directly and is in the application window.
2230         verify(mRotaryService, times(1))
2231                 .injectKeyEvent(KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.ACTION_DOWN);
2232         verify(mRotaryService, times(1))
2233                 .injectKeyEvent(KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.ACTION_UP);
2234         assertThat(mRotaryService.mIgnoreViewClickedNode).isEqualTo(appButton3Node);
2235 
2236         // The app sends a TYPE_VIEW_ACCESSIBILITY_FOCUSED event to RotaryService.
2237         // RotaryService should enter Direct Manipulation mode when receiving the event.
2238         AccessibilityEvent event = mock(AccessibilityEvent.class);
2239         when(event.getSource()).thenReturn(AccessibilityNodeInfo.obtain(appButton3Node));
2240         when(event.getEventType()).thenReturn(TYPE_VIEW_ACCESSIBILITY_FOCUSED);
2241         when(event.getClassName()).thenReturn(DIRECT_MANIPULATION);
2242         mRotaryService.onAccessibilityEvent(event);
2243         assertThat(mRotaryService.mInDirectManipulationMode).isTrue();
2244 
2245         // Click the back button of the controller.
2246         KeyEvent backButtonEventActionDown =
2247                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
2248         mRotaryService.onKeyEvents(validDisplayId,
2249                 Collections.singletonList(backButtonEventActionDown));
2250         KeyEvent backButtonEventActionUp =
2251                 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
2252         mRotaryService.onKeyEvents(validDisplayId,
2253                 Collections.singletonList(backButtonEventActionUp));
2254 
2255         // RotaryService should inject KEYCODE_BACK event because appButton3Node doesn't
2256         // support rotate directly and is in the application window.
2257         verify(mRotaryService, times(1))
2258                 .injectKeyEvent(KeyEvent.KEYCODE_BACK, KeyEvent.ACTION_DOWN);
2259         verify(mRotaryService, times(1))
2260                 .injectKeyEvent(KeyEvent.KEYCODE_BACK, KeyEvent.ACTION_UP);
2261 
2262         // The app sends a TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED event to RotaryService.
2263         // RotaryService should exit Direct Manipulation mode when receiving the event.
2264         event = mock(AccessibilityEvent.class);
2265         when(event.getSource()).thenReturn(AccessibilityNodeInfo.obtain(appButton3Node));
2266         when(event.getEventType()).thenReturn(TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2267         when(event.getClassName()).thenReturn(DIRECT_MANIPULATION);
2268         mRotaryService.onAccessibilityEvent(event);
2269         assertThat(mRotaryService.mInDirectManipulationMode).isFalse();
2270     }
2271 
2272     /**
2273      * Starts the test activity with the given layout and initializes the root
2274      * {@link AccessibilityNodeInfo}.
2275      */
initActivity(@ayoutRes int layoutResId)2276     private void initActivity(@LayoutRes int layoutResId) {
2277         mIntent.putExtra(NavigatorTestActivity.KEY_LAYOUT_ID, layoutResId);
2278         mActivityRule.launchActivity(mIntent);
2279         mWindowRoot = sUiAutomation.getRootInActiveWindow();
2280     }
2281 
2282     /**
2283      * Returns the {@link AccessibilityNodeInfo} related to the provided {@code viewId}. Returns
2284      * null if no such node exists. Callers should ensure {@link #initActivity} has already been
2285      * called. Caller shouldn't recycle the result because it will be recycled in {@link #tearDown}.
2286      */
createNode(String viewId)2287     private AccessibilityNodeInfo createNode(String viewId) {
2288         String fullViewId = "com.android.car.rotary.tests.unit:id/" + viewId;
2289         List<AccessibilityNodeInfo> nodes =
2290                 mWindowRoot.findAccessibilityNodeInfosByViewId(fullViewId);
2291         if (nodes.isEmpty()) {
2292             L.e("Failed to create node by View ID " + viewId);
2293             return null;
2294         }
2295         mNodes.addAll(nodes);
2296         return nodes.get(0);
2297     }
2298 }
2299