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