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