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