1 /*
2  * Copyright 2020 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 package com.android.car.rotary;
17 
18 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
19 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED;
20 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED;
21 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
22 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED;
23 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOWS_CHANGED;
24 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
25 import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_REMOVED;
26 import static android.view.Display.DEFAULT_DISPLAY;
27 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
28 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD;
29 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD;
30 
31 import android.accessibilityservice.AccessibilityService;
32 import android.accessibilityservice.AccessibilityServiceInfo;
33 import android.car.Car;
34 import android.car.input.CarInputManager;
35 import android.car.input.RotaryEvent;
36 import android.content.Context;
37 import android.content.res.Resources;
38 import android.hardware.display.DisplayManager;
39 import android.hardware.input.InputManager;
40 import android.os.Build;
41 import android.os.SystemClock;
42 import android.text.TextUtils;
43 import android.view.Display;
44 import android.view.InputDevice;
45 import android.view.KeyEvent;
46 import android.view.MotionEvent;
47 import android.view.View;
48 import android.view.WindowManager;
49 import android.view.accessibility.AccessibilityEvent;
50 import android.view.accessibility.AccessibilityNodeInfo;
51 import android.view.accessibility.AccessibilityWindowInfo;
52 
53 import androidx.annotation.NonNull;
54 import androidx.annotation.Nullable;
55 
56 import com.android.car.ui.utils.DirectManipulationHelper;
57 
58 import java.util.Collections;
59 import java.util.HashMap;
60 import java.util.List;
61 import java.util.Map;
62 
63 /**
64  * A service that can change focus based on rotary controller rotation and nudges, and perform
65  * clicks based on rotary controller center button clicks.
66  * <p>
67  * As an {@link AccessibilityService}, this service responds to {@link KeyEvent}s (on debug builds
68  * only) and {@link AccessibilityEvent}s.
69  * <p>
70  * On debug builds, {@link KeyEvent}s coming from the keyboard are handled by clicking the view, or
71  * moving the focus, sometimes within a window and sometimes between windows.
72  * <p>
73  * This service listens to two types of {@link AccessibilityEvent}s: {@link
74  * AccessibilityEvent#TYPE_VIEW_FOCUSED} and {@link AccessibilityEvent#TYPE_VIEW_CLICKED}. The
75  * former is used to keep {@link #mFocusedNode} up to date as the focus changes. The latter is used
76  * to detect when the user switches from rotary mode to touch mode and to keep {@link
77  * #mLastTouchedNode} up to date.
78  * <p>
79  * As a {@link CarInputManager.CarInputCaptureCallback}, this service responds to {@link KeyEvent}s
80  * and {@link RotaryEvent}s, both of which are coming from the controller.
81  * <p>
82  * {@link KeyEvent}s are handled by clicking the view, or moving the focus, sometimes within a
83  * window and sometimes between windows.
84  * <p>
85  * {@link RotaryEvent}s are handled by moving the focus within the same {@link
86  * com.android.car.ui.FocusArea}.
87  * <p>
88  * Note: onFoo methods are all called on the main thread so no locks are needed.
89  */
90 public class RotaryService extends AccessibilityService implements
91         CarInputManager.CarInputCaptureCallback {
92 
93     /*
94      * Whether to treat the application window as system window for direct manipulation mode. Set it
95      * to {@code true} for testing only.
96      */
97     private static final boolean TREAT_APP_WINDOW_AS_SYSTEM_WINDOW = false;
98 
99     /**
100      * How many detents to rotate when the user holds in shift while pressing C, V, Q, or E on a
101      * debug build.
102      */
103     private static final int SHIFT_DETENTS = 10;
104 
105     @NonNull
106     private NodeCopier mNodeCopier = new NodeCopier();
107 
108     private Navigator mNavigator;
109 
110     /** Input types to capture. */
111     private final int[] mInputTypes = new int[]{
112             // Capture controller rotation.
113             CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION,
114             // Capture controller center button clicks.
115             CarInputManager.INPUT_TYPE_DPAD_KEYS,
116             // Capture controller nudges.
117             CarInputManager.INPUT_TYPE_SYSTEM_NAVIGATE_KEYS};
118 
119     /**
120      * Time interval in milliseconds to decide whether we should accelerate the rotation by 3 times
121      * for a rotate event.
122      */
123     private int mRotationAcceleration3xMs;
124 
125     /**
126      * Time interval in milliseconds to decide whether we should accelerate the rotation by 2 times
127      * for a rotate event.
128      */
129     private int mRotationAcceleration2xMs;
130 
131     /** Whether to clear focus area history when the user rotates the controller. */
132     private boolean mClearFocusAreaHistoryWhenRotating;
133 
134     /**
135      * The currently focused node, if any. It's null if no nodes are focused or a {@link
136      * com.android.car.ui.FocusParkingView} is focused.
137      */
138     private AccessibilityNodeInfo mFocusedNode = null;
139 
140     /**
141      * The previously focused node, if any. It's null if no nodes were focused or a {@link
142      * com.android.car.ui.FocusParkingView} was focused.
143      */
144     private AccessibilityNodeInfo mPreviousFocusedNode = null;
145 
146     /**
147      * The currently focused {@link com.android.car.ui.FocusParkingView} that was focused by us to
148      * clear the focus, if any.
149      */
150     private AccessibilityNodeInfo mFocusParkingView = null;
151 
152     /**
153      * The current scrollable container, if any. Either {@link #mFocusedNode} or an ancestor of it.
154      */
155     private AccessibilityNodeInfo mScrollableContainer = null;
156 
157     /**
158      * The last clicked node by touching the screen, if any were clicked since we last navigated.
159      */
160     private AccessibilityNodeInfo mLastTouchedNode = null;
161 
162     /**
163      * How many milliseconds to ignore {@link AccessibilityEvent#TYPE_VIEW_CLICKED} events after
164      * performing {@link AccessibilityNodeInfo#ACTION_CLICK} or injecting a {@link
165      * KeyEvent#KEYCODE_DPAD_CENTER} event.
166      */
167     private int mIgnoreViewClickedMs;
168 
169     /**
170      * When not {@code null}, {@link AccessibilityEvent#TYPE_VIEW_CLICKED} events with this node
171      * are ignored if they occur before {@link #mIgnoreViewClickedUntil}.
172      */
173     private AccessibilityNodeInfo mIgnoreViewClickedNode;
174 
175     /**
176      * When to stop ignoring {@link AccessibilityEvent#TYPE_VIEW_CLICKED} events for {@link
177      * #mIgnoreViewClickedNode} in {@link SystemClock#uptimeMillis}.
178      */
179     private long mIgnoreViewClickedUntil;
180 
181     /**
182      * Possible actions to do after receiving {@link AccessibilityEvent#TYPE_VIEW_SCROLLED}.
183      *
184      * @see #injectScrollEvent
185      */
186     private enum AfterScrollAction {
187         /** Do nothing. */
188         NONE,
189         /**
190          * Focus the view before the focused view in Tab order in the scrollable container, if any.
191          */
192         FOCUS_PREVIOUS,
193         /**
194          * Focus the view after the focused view in Tab order in the scrollable container, if any.
195          */
196         FOCUS_NEXT,
197         /** Focus the first view in the scrollable container, if any. */
198         FOCUS_FIRST,
199         /** Focus the last view in the scrollable container, if any. */
200         FOCUS_LAST,
201     }
202 
203     private AfterScrollAction mAfterScrollAction = AfterScrollAction.NONE;
204 
205     /**
206      * How many milliseconds to wait for a {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event after
207      * scrolling.
208      */
209     private int mAfterScrollTimeoutMs;
210 
211     /**
212      * When to give up on receiving {@link AccessibilityEvent#TYPE_VIEW_SCROLLED}, in
213      * {@link SystemClock#uptimeMillis}.
214      */
215     private long mAfterScrollActionUntil;
216 
217     /** Whether we're in rotary mode (vs touch mode). */
218     private boolean mInRotaryMode;
219 
220     /** Whether we're in direct manipulation mode. */
221     private boolean mInDirectManipulationMode;
222 
223     /** The {@link SystemClock#uptimeMillis} when the last rotary rotation event occurred. */
224     private long mLastRotateEventTime;
225 
226     /**
227      * The repeat count of {@link KeyEvent#KEYCODE_DPAD_CENTER}. Use to prevent processing a center
228      * button click when the center button is released after a long press.
229      */
230     private int mCenterButtonRepeatCount;
231 
232     private static final Map<Integer, Integer> TEST_TO_REAL_KEYCODE_MAP;
233 
234     private static final Map<Integer, Integer> DIRECTION_TO_KEYCODE_MAP;
235 
236     static {
237         Map<Integer, Integer> map = new HashMap<>();
map.put(KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT)238         map.put(KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT);
map.put(KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT)239         map.put(KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT);
map.put(KeyEvent.KEYCODE_W, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP)240         map.put(KeyEvent.KEYCODE_W, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP);
map.put(KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN)241         map.put(KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN);
map.put(KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_DPAD_CENTER)242         map.put(KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_DPAD_CENTER);
map.put(KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_BACK)243         map.put(KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_BACK);
244         // Legacy map
map.put(KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT)245         map.put(KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT);
map.put(KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT)246         map.put(KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT);
map.put(KeyEvent.KEYCODE_I, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP)247         map.put(KeyEvent.KEYCODE_I, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP);
map.put(KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN)248         map.put(KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN);
map.put(KeyEvent.KEYCODE_COMMA, KeyEvent.KEYCODE_DPAD_CENTER)249         map.put(KeyEvent.KEYCODE_COMMA, KeyEvent.KEYCODE_DPAD_CENTER);
map.put(KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_BACK)250         map.put(KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_BACK);
251 
252         TEST_TO_REAL_KEYCODE_MAP = Collections.unmodifiableMap(map);
253     }
254 
255     static {
256         Map<Integer, Integer> map = new HashMap<>();
map.put(View.FOCUS_UP, KeyEvent.KEYCODE_DPAD_UP)257         map.put(View.FOCUS_UP, KeyEvent.KEYCODE_DPAD_UP);
map.put(View.FOCUS_DOWN, KeyEvent.KEYCODE_DPAD_DOWN)258         map.put(View.FOCUS_DOWN, KeyEvent.KEYCODE_DPAD_DOWN);
map.put(View.FOCUS_LEFT, KeyEvent.KEYCODE_DPAD_LEFT)259         map.put(View.FOCUS_LEFT, KeyEvent.KEYCODE_DPAD_LEFT);
map.put(View.FOCUS_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT)260         map.put(View.FOCUS_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT);
261 
262         DIRECTION_TO_KEYCODE_MAP = Collections.unmodifiableMap(map);
263     }
264 
265     private Car mCar;
266     private CarInputManager mCarInputManager;
267     private InputManager mInputManager;
268 
269     /** Package name of foreground app. */
270     private CharSequence mForegroundApp;
271 
272     private WindowManager mWindowManager;
273 
274     @Override
onCreate()275     public void onCreate() {
276         super.onCreate();
277         Resources res = getResources();
278         mRotationAcceleration3xMs = res.getInteger(R.integer.rotation_acceleration_3x_ms);
279         mRotationAcceleration2xMs = res.getInteger(R.integer.rotation_acceleration_2x_ms);
280 
281         mClearFocusAreaHistoryWhenRotating =
282                 res.getBoolean(R.bool.clear_focus_area_history_when_rotating);
283 
284         @RotaryCache.CacheType int focusHistoryCacheType =
285                 res.getInteger(R.integer.focus_history_cache_type);
286         int focusHistoryCacheSize =
287                 res.getInteger(R.integer.focus_history_cache_size);
288         int focusHistoryExpirationTimeMs =
289                 res.getInteger(R.integer.focus_history_expiration_time_ms);
290 
291         @RotaryCache.CacheType int focusAreaHistoryCacheType =
292                 res.getInteger(R.integer.focus_area_history_cache_type);
293         int focusAreaHistoryCacheSize =
294                 res.getInteger(R.integer.focus_area_history_cache_size);
295         int focusAreaHistoryExpirationTimeMs =
296                 res.getInteger(R.integer.focus_area_history_expiration_time_ms);
297 
298         @RotaryCache.CacheType int focusWindowCacheType =
299                 res.getInteger(R.integer.focus_window_cache_type);
300         int focusWindowCacheSize =
301                 res.getInteger(R.integer.focus_window_cache_size);
302         int focusWindowExpirationTimeMs =
303                 res.getInteger(R.integer.focus_window_expiration_time_ms);
304 
305         int hunMarginHorizontal =
306                 res.getDimensionPixelSize(R.dimen.notification_headsup_card_margin_horizontal);
307         int hunLeft = hunMarginHorizontal;
308         WindowManager windowManager = getSystemService(WindowManager.class);
309         int displayWidth = windowManager.getCurrentWindowMetrics().getBounds().width();
310         int hunRight = displayWidth - hunMarginHorizontal;
311         boolean showHunOnBottom = res.getBoolean(R.bool.config_showHeadsUpNotificationOnBottom);
312 
313         mIgnoreViewClickedMs = res.getInteger(R.integer.ignore_view_clicked_ms);
314         mAfterScrollTimeoutMs = res.getInteger(R.integer.after_scroll_timeout_ms);
315 
316         mNavigator = new Navigator(
317                 focusHistoryCacheType,
318                 focusHistoryCacheSize,
319                 focusHistoryExpirationTimeMs,
320                 focusAreaHistoryCacheType,
321                 focusAreaHistoryCacheSize,
322                 focusAreaHistoryExpirationTimeMs,
323                 focusWindowCacheType,
324                 focusWindowCacheSize,
325                 focusWindowExpirationTimeMs,
326                 hunLeft,
327                 hunRight,
328                 showHunOnBottom);
329     }
330 
331     /**
332      * {@inheritDoc}
333      * <p>
334      * We need to access WindowManager in onCreate() and
335      * IAccessibilityServiceClientWrapper.Callbacks#init(). Since WindowManager is a visual
336      * service, only Activity or other visual Context can access it. So we create a window context
337      * (a visual context) and delegate getSystemService() to it.
338      */
339     @Override
getSystemService(@erviceName @onNull String name)340     public Object getSystemService(@ServiceName @NonNull String name) {
341         // Guarantee that we always return the same WindowManager instance.
342         if (WINDOW_SERVICE.equals(name)) {
343             if (mWindowManager == null) {
344                 // We need to set the display before creating the WindowContext.
345                 DisplayManager displayManager = getSystemService(DisplayManager.class);
346                 Display primaryDisplay = displayManager.getDisplay(DEFAULT_DISPLAY);
347                 updateDisplay(primaryDisplay.getDisplayId());
348 
349                 Context windowContext = createWindowContext(TYPE_APPLICATION_OVERLAY, null);
350                 mWindowManager = (WindowManager) windowContext.getSystemService(WINDOW_SERVICE);
351             }
352             return mWindowManager;
353         }
354         return super.getSystemService(name);
355     }
356 
357     @Override
onServiceConnected()358     public void onServiceConnected() {
359         super.onServiceConnected();
360 
361         mCar = Car.createCar(this, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
362                 (car, ready) -> {
363                     mCar = car;
364                     if (ready) {
365                         mCarInputManager =
366                                 (CarInputManager) mCar.getCarManager(Car.CAR_INPUT_SERVICE);
367                         mCarInputManager.requestInputEventCapture(this,
368                                 CarInputManager.TARGET_DISPLAY_TYPE_MAIN,
369                                 mInputTypes,
370                                 CarInputManager.CAPTURE_REQ_FLAGS_ALLOW_DELAYED_GRANT);
371                     }
372                 });
373 
374         if (Build.IS_DEBUGGABLE) {
375             AccessibilityServiceInfo serviceInfo = getServiceInfo();
376             // Filter testing KeyEvents from a keyboard.
377             serviceInfo.flags |= AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS;
378             setServiceInfo(serviceInfo);
379         }
380 
381         mInputManager = getSystemService(InputManager.class);
382     }
383 
384     @Override
onInterrupt()385     public void onInterrupt() {
386         L.v("onInterrupt()");
387     }
388 
389     @Override
onDestroy()390     public void onDestroy() {
391         if (mCarInputManager != null) {
392             mCarInputManager.releaseInputEventCapture(CarInputManager.TARGET_DISPLAY_TYPE_MAIN);
393         }
394         if (mCar != null) {
395             mCar.disconnect();
396         }
397         super.onDestroy();
398     }
399 
400     @Override
onAccessibilityEvent(AccessibilityEvent event)401     public void onAccessibilityEvent(AccessibilityEvent event) {
402         switch (event.getEventType()) {
403             case TYPE_VIEW_FOCUSED: {
404                 handleViewFocusedEvent(event);
405                 break;
406             }
407             case TYPE_VIEW_CLICKED: {
408                 handleViewClickedEvent(event);
409                 break;
410             }
411             case TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
412                 updateDirectManipulationMode(event, true);
413                 break;
414             }
415             case TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
416                 updateDirectManipulationMode(event, false);
417                 break;
418             }
419             case TYPE_VIEW_SCROLLED: {
420                 handleViewScrolledEvent(event);
421                 break;
422             }
423             case TYPE_WINDOW_STATE_CHANGED: {
424                 CharSequence packageName = event.getPackageName();
425                 onForegroundAppChanged(packageName);
426                 break;
427             }
428             case TYPE_WINDOWS_CHANGED: {
429                 handleWindowsChangedEvent(event);
430                 break;
431             }
432             default:
433                 // Do nothing.
434         }
435     }
436 
437     /**
438      * Callback of {@link AccessibilityService}. It allows us to observe testing {@link KeyEvent}s
439      * from keyboard, including keys "C" and "V" to emulate controller rotation, keys "J" "L" "I"
440      * "K" to emulate controller nudges, and key "Comma" to emulate center button clicks.
441      */
442     @Override
onKeyEvent(KeyEvent event)443     protected boolean onKeyEvent(KeyEvent event) {
444         if (Build.IS_DEBUGGABLE) {
445             return handleKeyEvent(event);
446         }
447         return false;
448     }
449 
450     /**
451      * Callback of {@link CarInputManager.CarInputCaptureCallback}. It allows us to capture {@link
452      * KeyEvent}s generated by a navigation controller, such as controller nudge and controller
453      * click events.
454      */
455     @Override
onKeyEvents(int targetDisplayId, List<KeyEvent> events)456     public void onKeyEvents(int targetDisplayId, List<KeyEvent> events) {
457         if (!isValidDisplayId(targetDisplayId)) {
458             return;
459         }
460         for (KeyEvent event : events) {
461             handleKeyEvent(event);
462         }
463     }
464 
465     /**
466      * Callback of {@link CarInputManager.CarInputCaptureCallback}. It allows us to capture {@link
467      * RotaryEvent}s generated by a navigation controller.
468      */
469     @Override
onRotaryEvents(int targetDisplayId, List<RotaryEvent> events)470     public void onRotaryEvents(int targetDisplayId, List<RotaryEvent> events) {
471         if (!isValidDisplayId(targetDisplayId)) {
472             return;
473         }
474         for (RotaryEvent rotaryEvent : events) {
475             handleRotaryEvent(rotaryEvent);
476         }
477     }
478 
479     @Override
onCaptureStateChanged(int targetDisplayId, @android.annotation.NonNull @CarInputManager.InputTypeEnum int[] activeInputTypes)480     public void onCaptureStateChanged(int targetDisplayId,
481             @android.annotation.NonNull @CarInputManager.InputTypeEnum int[] activeInputTypes) {
482         // Do nothing.
483     }
484 
isValidDisplayId(int displayId)485     private static boolean isValidDisplayId(int displayId) {
486         if (displayId == CarInputManager.TARGET_DISPLAY_TYPE_MAIN) {
487             return true;
488         }
489         L.e("RotaryService shouldn't capture events from display ID " + displayId);
490         return false;
491     }
492 
493     /**
494      * Handles key events. Returns whether the key event was consumed. To avoid invalid event stream
495      * getting through to the application, if a key down event is consumed, the corresponding key up
496      * event must be consumed too, and vice versa.
497      */
handleKeyEvent(KeyEvent event)498     private boolean handleKeyEvent(KeyEvent event) {
499         int action = event.getAction();
500         boolean isActionDown = action == KeyEvent.ACTION_DOWN;
501         int keyCode = getKeyCode(event);
502         int detents = event.isShiftPressed() ? SHIFT_DETENTS : 1;
503         switch (keyCode) {
504             case KeyEvent.KEYCODE_Q:
505             case KeyEvent.KEYCODE_C:
506                 if (isActionDown) {
507                     handleRotateEvent(/* clockwise= */ false, detents,
508                             event.getEventTime());
509                 }
510                 return true;
511             case KeyEvent.KEYCODE_E:
512             case KeyEvent.KEYCODE_V:
513                 if (isActionDown) {
514                     handleRotateEvent(/* clockwise= */ true, detents,
515                             event.getEventTime());
516                 }
517                 return true;
518             case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT:
519                 handleNudgeEvent(View.FOCUS_LEFT, action);
520                 return true;
521             case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT:
522                 handleNudgeEvent(View.FOCUS_RIGHT, action);
523                 return true;
524             case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP:
525                 handleNudgeEvent(View.FOCUS_UP, action);
526                 return true;
527             case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN:
528                 handleNudgeEvent(View.FOCUS_DOWN, action);
529                 return true;
530             case KeyEvent.KEYCODE_DPAD_CENTER:
531                 if (isActionDown) {
532                     mCenterButtonRepeatCount = event.getRepeatCount();
533                 }
534                 if (mCenterButtonRepeatCount == 0) {
535                     handleCenterButtonEvent(action, /* longClick= */ false);
536                 } else if (mCenterButtonRepeatCount == 1) {
537                     handleCenterButtonEvent(action, /* longClick= */ true);
538                 }
539                 return true;
540             case KeyEvent.KEYCODE_BACK:
541                 if (mInDirectManipulationMode) {
542                     handleBackButtonEvent(action);
543                     return true;
544                 }
545                 return false;
546             default:
547                 // Do nothing
548         }
549         return false;
550     }
551 
552     /** Handles {@link AccessibilityEvent#TYPE_VIEW_FOCUSED} event. */
handleViewFocusedEvent(@onNull AccessibilityEvent event)553     private void handleViewFocusedEvent(@NonNull AccessibilityEvent event) {
554         // A view was focused. We ignore focus changes in touch mode. We don't use
555         // TYPE_VIEW_FOCUSED to keep mLastTouchedNode up to date because most views can't be
556         // focused in touch mode. In rotary mode, we use TYPE_VIEW_FOCUSED events to keep
557         // mFocusedNode up to date, to clear the focus when moving between windows, to detect when
558         // a scrollable container scrolls and pushes the focused descendant out of the viewport,
559         // and to detect when the focused view is removed.
560         if (!mInRotaryMode) {
561             return;
562         }
563         AccessibilityNodeInfo sourceNode = event.getSource();
564         // No need to handle TYPE_VIEW_FOCUSED event if sourceNode is null or the focused node stays
565         // the same.
566         if (sourceNode == null || sourceNode.equals(mFocusedNode)) {
567             Utils.recycleNode(sourceNode);
568             return;
569         }
570         // Case 1: the focused view is not a FocusParkingView. In this case we just update
571         // mFocusedNode.
572         if (!Utils.isFocusParkingView(sourceNode)) {
573             // Android doesn't clear focus automatically when focus is set in another window.
574             maybeClearFocusInCurrentWindow(sourceNode);
575             setFocusedNode(sourceNode);
576         }
577         // Case 2: the focused view is a FocusParkingView and it was focused by us to clear the
578         // focus in another window. In this case we should do nothing but reset mFocusParkingView.
579         else if (sourceNode.equals(mFocusParkingView)) {
580             Utils.recycleNode(mFocusParkingView);
581             mFocusParkingView = null;
582         }
583         // Case 3: the focused view is a FocusParkingView and it was focused when scrolling pushed
584         // the focused view out of the viewport. When this happens, focus the scrollable container.
585         else if (mFocusedNode != null && mScrollableContainer != null) {
586             mScrollableContainer = Utils.refreshNode(mScrollableContainer);
587             if (mScrollableContainer != null) {
588                 L.d("Moving focus from FocusParkingView to scrollable container");
589                 performFocusAction(mScrollableContainer);
590             } else {
591                 L.d("mScrollableContainer is not in the view tree");
592             }
593         }
594         // Case 4 (all other cases): the focused view is a FocusParkingView and it's none
595         // of the cases above. For example:
596         // 1. When the previously focused view is removed by the app, Android will focus on
597         //    the first focusable view in the window, which is the FocusParkingView.
598         // 2. When a dialog window shows up, Android will focus on the first focusable view
599         //    in the dialog window, which is the FocusParkingView.
600         // In both cases we should try to move focus to another view nearby.
601         else {
602             moveFocusToNearbyView(sourceNode);
603         }
604 
605         // Recycle sourceNode no matter in which case above.
606         Utils.recycleNode(sourceNode);
607     }
608 
609     /** Handles {@link AccessibilityEvent#TYPE_VIEW_CLICKED} event. */
handleViewClickedEvent(@onNull AccessibilityEvent event)610     private void handleViewClickedEvent(@NonNull AccessibilityEvent event) {
611         // A view was clicked. If we triggered the click via performAction(ACTION_CLICK) or
612         // by injecting KEYCODE_DPAD_CENTER, we ignore it. Otherwise, we assume the user
613         // touched the screen. In this case, we exit rotary mode if necessary, update
614         // mLastTouchedNode, and clear the focus if the user touched a view in a different
615         // window.
616         // To decide whether the click was triggered by us, we can compare the source node
617         // in the event with mIgnoreViewClickedNode. If they're equal, the click was
618         // triggered by us. But there is a corner case. If a dialog shows up after we
619         // clicked the view, the window containing the view will be removed. We still
620         // receive click event (TYPE_VIEW_CLICKED) but the source node in the event will be
621         // null.
622         // Note: there is no way to tell whether the window is removed in click event
623         // because window remove event (TYPE_WINDOWS_CHANGED with type
624         // WINDOWS_CHANGE_REMOVED) comes AFTER click event.
625         AccessibilityNodeInfo sourceNode = event.getSource();
626         if (mIgnoreViewClickedNode != null
627                 && event.getEventTime() < mIgnoreViewClickedUntil
628                 && ((sourceNode == null) || mIgnoreViewClickedNode.equals(sourceNode))) {
629             setIgnoreViewClickedNode(null);
630         } else {
631             // Enter touch mode once the user touches the screen.
632             mInRotaryMode = false;
633             if (sourceNode != null) {
634                 // Explicitly clear focus when user uses touch in another window.
635                 maybeClearFocusInCurrentWindow(sourceNode);
636 
637                 if (!sourceNode.equals(mLastTouchedNode)) {
638                     setLastTouchedNode(sourceNode);
639                 }
640             }
641         }
642         Utils.recycleNode(sourceNode);
643     }
644 
645     /** Handles {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event. */
handleViewScrolledEvent(@onNull AccessibilityEvent event)646     private void handleViewScrolledEvent(@NonNull AccessibilityEvent event) {
647         if (mAfterScrollAction == AfterScrollAction.NONE
648                 || SystemClock.uptimeMillis() >= mAfterScrollActionUntil) {
649             return;
650         }
651         AccessibilityNodeInfo sourceNode = event.getSource();
652         if (sourceNode == null || !Utils.isScrollableContainer(sourceNode)) {
653             Utils.recycleNode(sourceNode);
654             return;
655         }
656         switch (mAfterScrollAction) {
657             case FOCUS_PREVIOUS:
658             case FOCUS_NEXT: {
659                 if (mFocusedNode.equals(sourceNode)) {
660                     break;
661                 }
662                 AccessibilityNodeInfo target = Navigator.findFocusableDescendantInDirection(
663                         sourceNode, mFocusedNode,
664                         mAfterScrollAction == AfterScrollAction.FOCUS_PREVIOUS
665                                 ? View.FOCUS_BACKWARD
666                                 : View.FOCUS_FORWARD);
667                 if (target == null) {
668                     break;
669                 }
670                 L.d("Focusing %s after scroll",
671                         mAfterScrollAction == AfterScrollAction.FOCUS_PREVIOUS
672                                 ? "previous"
673                                 : "next");
674                 if (performFocusAction(target)) {
675                     mAfterScrollAction = AfterScrollAction.NONE;
676                 }
677                 Utils.recycleNode(target);
678                 break;
679             }
680             case FOCUS_FIRST:
681             case FOCUS_LAST: {
682                 AccessibilityNodeInfo target =
683                         mAfterScrollAction == AfterScrollAction.FOCUS_FIRST
684                                 ? mNavigator.findFirstFocusableDescendant(sourceNode)
685                                 : mNavigator.findLastFocusableDescendant(sourceNode);
686                 if (target == null) {
687                     break;
688                 }
689                 L.d("Focusing %s after scroll",
690                         mAfterScrollAction == AfterScrollAction.FOCUS_FIRST ? "first" : "last");
691                 if (performFocusAction(target)) {
692                     mAfterScrollAction = AfterScrollAction.NONE;
693                 }
694                 Utils.recycleNode(target);
695                 break;
696             }
697             default:
698                 throw new IllegalStateException(
699                         "Unknown after scroll action: " + mAfterScrollAction);
700         }
701         Utils.recycleNode(sourceNode);
702     }
703 
704     /** Handles {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} event. */
handleWindowsChangedEvent(@onNull AccessibilityEvent event)705     private void handleWindowsChangedEvent(@NonNull AccessibilityEvent event) {
706         if ((event.getWindowChanges() & WINDOWS_CHANGE_REMOVED) != 0
707                 && mInRotaryMode
708                 && mFocusedNode != null
709                 && mFocusedNode.getWindowId() == event.getWindowId()) {
710             // The window containing the focused node is gone. Restore focus to the last
711             // focused node in the last focused window.
712             setFocusedNode(null);
713             AccessibilityNodeInfo newFocus = mNavigator.getMostRecentFocus();
714             if (newFocus != null) {
715                 performFocusAction(newFocus);
716                 newFocus.recycle();
717             }
718         }
719     }
720 
getKeyCode(KeyEvent event)721     private static int getKeyCode(KeyEvent event) {
722         int keyCode = event.getKeyCode();
723         if (Build.IS_DEBUGGABLE) {
724             Integer mappingKeyCode = TEST_TO_REAL_KEYCODE_MAP.get(keyCode);
725             if (mappingKeyCode != null) {
726                 keyCode = mappingKeyCode;
727             }
728         }
729         return keyCode;
730     }
731 
732     /** Handles controller center button event. */
handleCenterButtonEvent(int action, boolean longClick)733     private void handleCenterButtonEvent(int action, boolean longClick) {
734         if (!isValidAction(action)) {
735             return;
736         }
737         if (initFocus()) {
738             return;
739         }
740         // Case 1: the focus is in application window, inject KeyEvent.KEYCODE_DPAD_CENTER event and
741         // the application will handle it.
742         if (isInApplicationWindow(mFocusedNode)) {
743             injectKeyEvent(KeyEvent.KEYCODE_DPAD_CENTER, action);
744             setIgnoreViewClickedNode(mFocusedNode);
745             return;
746         }
747         // We're done with ACTION_DOWN event.
748         if (action == KeyEvent.ACTION_DOWN) {
749             return;
750         }
751 
752         // Case 2: the focus is not in application window (e.g., in system window) and the focused
753         // node supports direct manipulation, enter direct manipulation mode.
754         if (DirectManipulationHelper.supportDirectManipulation(mFocusedNode)) {
755             if (!mInDirectManipulationMode) {
756                 mInDirectManipulationMode = true;
757                 L.d("Enter direct manipulation mode because focused node is clicked.");
758             }
759             return;
760         }
761 
762         // Case 3: the focus is not in application window and the focused node doesn't support
763         // direct manipulation, perform click or long click on the focused node.
764         boolean result = mFocusedNode.performAction(
765                 longClick
766                 ? AccessibilityNodeInfo.ACTION_LONG_CLICK
767                 : AccessibilityNodeInfo.ACTION_CLICK);
768         if (!result) {
769             L.w("Failed to perform " + (longClick ? "ACTION_LONG_CLICK" : "ACTION_CLICK")
770                     + " on " + mFocusedNode);
771         }
772         if (!longClick) {
773             setIgnoreViewClickedNode(mFocusedNode);
774         }
775     }
776 
handleNudgeEvent(int direction, int action)777     private void handleNudgeEvent(int direction, int action) {
778         if (!isValidAction(action)) {
779             return;
780         }
781         if (initFocus()) {
782             return;
783         }
784 
785         // If the focused node is in direct manipulation mode, manipulate it directly.
786         if (mInDirectManipulationMode) {
787             if (isInApplicationWindow(mFocusedNode)) {
788                 injectKeyEventForDirection(direction, action);
789             } else {
790                 L.d("Ignore nudge events because we're in DM mode and the focus is not in"
791                         + " application window");
792             }
793             return;
794         }
795 
796         // We're done with ACTION_UP event.
797         if (action == KeyEvent.ACTION_UP) {
798             return;
799         }
800 
801         // If the focused node is not in direct manipulation mode, move the focus.
802         // TODO(b/152438801): sometimes getWindows() takes 10s after boot.
803         List<AccessibilityWindowInfo> windows = getWindows();
804         AccessibilityNodeInfo targetNode =
805                 mNavigator.findNudgeTarget(windows, mFocusedNode, direction);
806         Utils.recycleWindows(windows);
807         if (targetNode == null) {
808             L.w("Failed to find nudge target");
809             return;
810         }
811 
812         // Android doesn't clear focus automatically when focus is set in another window.
813         maybeClearFocusInCurrentWindow(targetNode);
814 
815         performFocusAction(targetNode);
816         Utils.recycleNode(targetNode);
817     }
818 
handleRotaryEvent(RotaryEvent rotaryEvent)819     private void handleRotaryEvent(RotaryEvent rotaryEvent) {
820         if (rotaryEvent.getInputType() != CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION) {
821             return;
822         }
823         boolean clockwise = rotaryEvent.isClockwise();
824         int count = rotaryEvent.getNumberOfClicks();
825         // TODO(b/153195148): Use the first eventTime for now. We'll need to improve it later.
826         long eventTime = rotaryEvent.getUptimeMillisForClick(0);
827         handleRotateEvent(clockwise, count, eventTime);
828     }
829 
handleRotateEvent(boolean clockwise, int count, long eventTime)830     private void handleRotateEvent(boolean clockwise, int count, long eventTime) {
831         // Clear focus area history if configured to do so, but not when rotating in the HUN. The
832         // HUN overlaps the application window so it's common for focus areas to overlap, causing
833         // geometric searches to fail. History is essential here.
834         if (mClearFocusAreaHistoryWhenRotating && !isFocusInHunWindow()) {
835             mNavigator.clearFocusAreaHistory();
836         }
837         if (initFocus()) {
838             return;
839         }
840 
841         int rotationCount = getRotateAcceleration(count, eventTime);
842 
843         // If a scrollable container is focused, no focusable descendants are visible, so scroll the
844         // container.
845         AccessibilityNodeInfo.AccessibilityAction scrollAction =
846                 clockwise ? ACTION_SCROLL_FORWARD : ACTION_SCROLL_BACKWARD;
847         if (mFocusedNode != null && Utils.isScrollableContainer(mFocusedNode)
848                 && mFocusedNode.getActionList().contains(scrollAction)) {
849             injectScrollEvent(mFocusedNode, clockwise, rotationCount);
850             return;
851         }
852 
853         // If the focused node is in direct manipulation mode, manipulate it directly.
854         if (mInDirectManipulationMode) {
855             if (isInApplicationWindow(mFocusedNode)) {
856                 AccessibilityWindowInfo window = mFocusedNode.getWindow();
857                 if (window == null) {
858                     L.w("Failed to get window of " + mFocusedNode);
859                     return;
860                 }
861                 int displayId = window.getDisplayId();
862                 window.recycle();
863                 // TODO(b/155823126): Add config to let OEMs determine the mapping.
864                 injectMotionEvent(displayId, MotionEvent.AXIS_SCROLL,
865                         clockwise ? rotationCount : -rotationCount);
866             } else {
867                 performScrollAction(mFocusedNode, clockwise);
868             }
869             return;
870         }
871 
872         // If the focused node is not in direct manipulation mode, move the focus. Skip over
873         // mScrollableContainer; we don't want to navigate from a focusable descendant to the
874         // scrollable container except as a side-effect of scrolling.
875         int remainingRotationCount = rotationCount;
876         int direction = clockwise ? View.FOCUS_FORWARD : View.FOCUS_BACKWARD;
877         Navigator.FindRotateTargetResult result = mNavigator.findRotateTarget(mFocusedNode,
878                 /* skipNode= */ mScrollableContainer, direction, rotationCount);
879         if (result != null) {
880             if (performFocusAction(result.node)) {
881                 remainingRotationCount -= result.advancedCount;
882             }
883             Utils.recycleNode(result.node);
884         } else {
885             L.w("Failed to find rotate target");
886         }
887 
888         // If navigation didn't consume all of rotationCount and the focused node either is a
889         // scrollable container or is a descendant of one, scroll it. The former happens when no
890         // focusable views are visible in the scrollable container. The latter happens when there
891         // are focusable views but they're in the wrong direction. Inject a MotionEvent rather than
892         // performing an action so that the application can control the amount it scrolls. Scrolling
893         // is only supported in the application window because injected events always go to the
894         // application window. We don't bother checking whether the scrollable container can
895         // currently scroll because there's nothing else to do if it can't.
896         if (remainingRotationCount > 0 && isInApplicationWindow(mFocusedNode)
897                 && mScrollableContainer != null) {
898             injectScrollEvent(mScrollableContainer, clockwise, remainingRotationCount);
899         }
900     }
901 
902     /** Handles Back button event. */
handleBackButtonEvent(int action)903     private void handleBackButtonEvent(int action) {
904         if (!isValidAction(action)) {
905             return;
906         }
907 
908         // If the focus is in application window, inject Back button event and the application will
909         // handle it. If the focus is not in application window, exit direct manipulation mode on
910         // key up.
911         if (isInApplicationWindow(mFocusedNode)) {
912             injectKeyEvent(KeyEvent.KEYCODE_BACK, action);
913         } else if (action == KeyEvent.ACTION_UP) {
914             L.d("Exit direct manipulation mode on back button event");
915             mInDirectManipulationMode = false;
916         }
917     }
918 
onForegroundAppChanged(CharSequence packageName)919     private void onForegroundAppChanged(CharSequence packageName) {
920         if (TextUtils.equals(mForegroundApp, packageName)) {
921             return;
922         }
923         mForegroundApp = packageName;
924         if (mInDirectManipulationMode) {
925             L.d("Exit direct manipulation mode because the foreground app has changed");
926             mInDirectManipulationMode = false;
927         }
928     }
929 
isValidAction(int action)930     private static boolean isValidAction(int action) {
931         if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) {
932             L.w("Invalid action " + action);
933             return false;
934         }
935         return true;
936     }
937 
938     /** Performs scroll action on the given {@code targetNode} if it supports scroll action. */
performScrollAction(@onNull AccessibilityNodeInfo targetNode, boolean clockwise)939     private static void performScrollAction(@NonNull AccessibilityNodeInfo targetNode,
940             boolean clockwise) {
941         // TODO(b/155823126): Add config to let OEMs determine the mapping.
942         int actionToPerform = clockwise
943                 ? AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
944                 : AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
945         int supportedActions = targetNode.getActions();
946         if ((actionToPerform & supportedActions) == 0) {
947             L.w("Node " + targetNode + " doesn't support action " + actionToPerform);
948             return;
949         }
950         boolean result = targetNode.performAction(actionToPerform);
951         if (!result) {
952             L.w("Failed to perform action " + actionToPerform + " on " + targetNode);
953         }
954     }
955 
956     /** Returns whether the given {@code node} is in the application window. */
isInApplicationWindow(@onNull AccessibilityNodeInfo node)957     private static boolean isInApplicationWindow(@NonNull AccessibilityNodeInfo node) {
958         if (TREAT_APP_WINDOW_AS_SYSTEM_WINDOW) {
959             return false;
960         }
961         AccessibilityWindowInfo window = node.getWindow();
962         if (window == null) {
963             L.w("Failed to get window of " + node);
964             return false;
965         }
966         boolean result = window.getType() == AccessibilityWindowInfo.TYPE_APPLICATION;
967         Utils.recycleWindow(window);
968         return result;
969     }
970 
971     /** Returns whether {@link #mFocusedNode} is in the HUN window. */
isFocusInHunWindow()972     private boolean isFocusInHunWindow() {
973         if (mFocusedNode == null) {
974             return false;
975         }
976         AccessibilityWindowInfo window = mFocusedNode.getWindow();
977         if (window == null) {
978             L.w("Failed to get window of " + mFocusedNode);
979             return false;
980         }
981         boolean result = mNavigator.isHunWindow(window);
982         Utils.recycleWindow(window);
983         return result;
984     }
985 
updateDirectManipulationMode(AccessibilityEvent event, boolean enable)986     private void updateDirectManipulationMode(AccessibilityEvent event, boolean enable) {
987         if (!mInRotaryMode || !DirectManipulationHelper.isDirectManipulation(event)) {
988             return;
989         }
990         if (enable) {
991             mFocusedNode = Utils.refreshNode(mFocusedNode);
992             if (mFocusedNode == null) {
993                 L.w("Failed to enter direct manipulation mode because mFocusedNode is no longer "
994                         + "in view tree.");
995                 return;
996             }
997             if (!mFocusedNode.isFocused()) {
998                 L.w("Failed to enter direct manipulation mode because mFocusedNode is no longer "
999                         + "focused.");
1000                 return;
1001             }
1002         }
1003         if (mInDirectManipulationMode != enable) {
1004             // Toggle direct manipulation mode upon app's request.
1005             mInDirectManipulationMode = enable;
1006             L.d((enable ? "Enter" : "Exit") + " direct manipulation mode upon app's request");
1007         }
1008     }
1009 
1010     /**
1011      * Injects a {@link MotionEvent} to scroll {@code scrollableContainer} by {@code rotationCount}
1012      * steps. The direction depends on the value of {@code clockwise}. Sets
1013      * {@link #mAfterScrollAction} to move the focus once the scroll occurs, as follows:<ul>
1014      *     <li>If the user is spinning the rotary controller quickly, focuses the first or last
1015      *         focusable descendant so that the next rotation event will scroll immediately.
1016      *     <li>If the user is spinning slowly and there are no focusable descendants visible,
1017      *         focuses the first focusable descendant to scroll into view. This will be the last
1018      *         focusable descendant when scrolling up.
1019      *     <li>If the user is spinning slowly and there are focusable descendants visible, focuses
1020      *         the next or previous focusable descendant.
1021      * </ul>
1022      */
injectScrollEvent(@onNull AccessibilityNodeInfo scrollableContainer, boolean clockwise, int rotationCount)1023     private void injectScrollEvent(@NonNull AccessibilityNodeInfo scrollableContainer,
1024             boolean clockwise, int rotationCount) {
1025         // TODO(b/155823126): Add config to let OEMs determine the mappings.
1026         if (rotationCount > 1) {
1027             // Focus last when quickly scrolling down so the next event scrolls.
1028             mAfterScrollAction = clockwise
1029                     ? AfterScrollAction.FOCUS_LAST
1030                     : AfterScrollAction.FOCUS_FIRST;
1031         } else {
1032             if (Utils.isScrollableContainer(mFocusedNode)) {
1033                 // Focus first when scrolling down while no focusable descendants are visible.
1034                 mAfterScrollAction = clockwise
1035                         ? AfterScrollAction.FOCUS_FIRST
1036                         : AfterScrollAction.FOCUS_LAST;
1037             } else {
1038                 // Focus next when scrolling down with a focused descendant.
1039                 mAfterScrollAction = clockwise
1040                         ? AfterScrollAction.FOCUS_NEXT
1041                         : AfterScrollAction.FOCUS_PREVIOUS;
1042             }
1043         }
1044         mAfterScrollActionUntil = SystemClock.uptimeMillis() + mAfterScrollTimeoutMs;
1045         int axis = Utils.isHorizontallyScrollableContainer(scrollableContainer)
1046                 ? MotionEvent.AXIS_HSCROLL
1047                 : MotionEvent.AXIS_VSCROLL;
1048         AccessibilityWindowInfo window = scrollableContainer.getWindow();
1049         if (window == null) {
1050             L.w("Failed to get window of " + scrollableContainer);
1051             return;
1052         }
1053         int displayId = window.getDisplayId();
1054         window.recycle();
1055         injectMotionEvent(displayId, axis, clockwise ? -rotationCount : rotationCount);
1056     }
1057 
injectMotionEvent(int displayId, int axis, int axisValue)1058     private void injectMotionEvent(int displayId, int axis, int axisValue) {
1059         long upTime = SystemClock.uptimeMillis();
1060         MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[1];
1061         properties[0] = new MotionEvent.PointerProperties();
1062         properties[0].id = 0; // Any integer value but -1 (INVALID_POINTER_ID) is fine.
1063         MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[1];
1064         coords[0] = new MotionEvent.PointerCoords();
1065         // No need to set X,Y coordinates. We use a non-pointer source so the event will be routed
1066         // to the focused view.
1067         coords[0].setAxisValue(axis, axisValue);
1068         MotionEvent motionEvent = MotionEvent.obtain(/* downTime= */ upTime,
1069                 /* eventTime= */ upTime,
1070                 MotionEvent.ACTION_SCROLL,
1071                 /* pointerCount= */ 1,
1072                 properties,
1073                 coords,
1074                 /* metaState= */ 0,
1075                 /* buttonState= */ 0,
1076                 /* xPrecision= */ 1.0f,
1077                 /* yPrecision= */ 1.0f,
1078                 /* deviceId= */ 0,
1079                 /* edgeFlags= */ 0,
1080                 InputDevice.SOURCE_ROTARY_ENCODER,
1081                 displayId,
1082                 /* flags= */ 0);
1083 
1084         if (motionEvent != null) {
1085             mInputManager.injectInputEvent(motionEvent,
1086                     InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
1087         } else {
1088             L.w("Unable to obtain MotionEvent");
1089         }
1090     }
1091 
injectKeyEventForDirection(int direction, int action)1092     private boolean injectKeyEventForDirection(int direction, int action) {
1093         Integer keyCode = DIRECTION_TO_KEYCODE_MAP.get(direction);
1094         if (keyCode == null) {
1095             throw new IllegalArgumentException("direction must be one of "
1096                     + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
1097         }
1098         return injectKeyEvent(keyCode, action);
1099     }
1100 
injectKeyEvent(int keyCode, int action)1101     private boolean injectKeyEvent(int keyCode, int action) {
1102         long upTime = SystemClock.uptimeMillis();
1103         KeyEvent keyEvent = new KeyEvent(
1104                 /* downTime= */ upTime, /* eventTime= */ upTime, action, keyCode, /* repeat= */ 0);
1105         return mInputManager.injectInputEvent(keyEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
1106     }
1107 
1108     /**
1109      * Updates {@link #mFocusedNode} and {@link #mLastTouchedNode} in case the {@link View}s
1110      * represented by them are no longer in the view tree.
1111      */
refreshSavedNodes()1112     private void refreshSavedNodes() {
1113         mFocusedNode = Utils.refreshNode(mFocusedNode);
1114         mLastTouchedNode = Utils.refreshNode(mLastTouchedNode);
1115         mScrollableContainer = Utils.refreshNode(mScrollableContainer);
1116         mPreviousFocusedNode = Utils.refreshNode(mPreviousFocusedNode);
1117     }
1118 
1119     /**
1120      * This method should be called when receiving an event from a rotary controller. It does the
1121      * following:<ol>
1122      *     <li>If {@link #mFocusedNode} isn't null and represents a view that still exists, does
1123      *         nothing. The event isn't consumed in this case. This is the normal case.
1124      *     <li>If {@link #mScrollableContainer} isn't null and represents a view that still exists,
1125      *         focuses it. The event isn't consumed in this case. This can happen when the user
1126      *         rotates quickly as they scroll into a section without any focusable views.
1127      *     <li>If {@link #mLastTouchedNode} isn't null and represents a view that still exists,
1128      *         focuses it. The event is consumed in this case. This happens when the user switches
1129      *         from touch to rotary.
1130      * </ol>
1131      *
1132      * @return whether the event was consumed by this method. When {@code false},
1133      *         {@link #mFocusedNode} is guaranteed to not be {@code null}.
1134      */
initFocus()1135     private boolean initFocus() {
1136         refreshSavedNodes();
1137         mInRotaryMode = true;
1138         if (mFocusedNode != null) {
1139             return false;
1140         }
1141         if (mScrollableContainer != null) {
1142             if (performFocusAction(mScrollableContainer)) {
1143                 return false;
1144             }
1145         }
1146         if (mLastTouchedNode != null) {
1147             if (focusLastTouchedNode()) {
1148                 return true;
1149             }
1150         }
1151         focusFirstFocusDescendant();
1152         return true;
1153     }
1154 
1155     /** Clears the current rotary focus if {@code targetFocus} is in a different window. */
maybeClearFocusInCurrentWindow(@onNull AccessibilityNodeInfo targetFocus)1156     private void maybeClearFocusInCurrentWindow(@NonNull AccessibilityNodeInfo targetFocus) {
1157         if (mFocusedNode == null || !mFocusedNode.isFocused()
1158                 || mFocusedNode.getWindowId() == targetFocus.getWindowId()) {
1159             return;
1160         }
1161         if (clearFocusInCurrentWindow()) {
1162             setFocusedNode(null);
1163         }
1164     }
1165 
1166     /**
1167      * Clears the current rotary focus.
1168      * <p>
1169      * If we really clear focus in the current window, Android will re-focus a view in the current
1170      * window automatically, resulting in the current window and the target window being focused
1171      * simultaneously. To avoid that we don't really clear the focus. Instead, we "park" the focus
1172      * on a FocusParkingView in the current window. FocusParkingView is transparent no matter
1173      * whether it's focused or not, so it's invisible to the user.
1174      *
1175      * @return whether the FocusParkingView was focused successfully
1176      */
clearFocusInCurrentWindow()1177     private boolean clearFocusInCurrentWindow() {
1178         if (mFocusedNode == null) {
1179             L.e("Don't call clearFocusInCurrentWindow() when mFocusedNode is null");
1180             return false;
1181         }
1182         AccessibilityWindowInfo window = mFocusedNode.getWindow();
1183         if (window == null) {
1184             L.w("Failed to get window of " + mFocusedNode);
1185             return false;
1186         }
1187         AccessibilityNodeInfo focusParkingView = mNavigator.findFocusParkingView(window);
1188         if (focusParkingView == null) {
1189             L.e("No FocusParkingView in " + window);
1190             window.recycle();
1191             return false;
1192         }
1193         window.recycle();
1194         boolean result = focusParkingView.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
1195         if (result) {
1196             if (mFocusParkingView != null) {
1197                 L.e("mFocusParkingView should be null but is " + mFocusParkingView);
1198                 Utils.recycleNode(mFocusParkingView);
1199             }
1200             mFocusParkingView = copyNode(focusParkingView);
1201         } else {
1202             L.w("Failed to perform ACTION_FOCUS on " + focusParkingView);
1203         }
1204         focusParkingView.recycle();
1205         return result;
1206     }
1207 
1208     /**
1209      * Focuses the last touched node, if any.
1210      *
1211      * @return {@code true} if {@link #mLastTouchedNode} isn't {@code null} and it was
1212      *         successfully focused
1213      */
focusLastTouchedNode()1214     private boolean focusLastTouchedNode() {
1215         boolean lastTouchedNodeFocused = false;
1216         if (mLastTouchedNode != null) {
1217             lastTouchedNodeFocused = performFocusAction(mLastTouchedNode);
1218             if (mLastTouchedNode != null) {
1219                 setLastTouchedNode(null);
1220             }
1221         }
1222         return lastTouchedNodeFocused;
1223     }
1224 
1225     /**
1226      * Focuses the first focus descendant (a node inside a focus area that can take focus) in the
1227      * currently active window, if any.
1228      */
focusFirstFocusDescendant()1229     private void focusFirstFocusDescendant() {
1230         AccessibilityNodeInfo rootNode = getRootInActiveWindow();
1231         if (rootNode == null) {
1232             L.e("rootNode of active window is null");
1233             return;
1234         }
1235         AccessibilityNodeInfo targetNode = mNavigator.findFirstFocusDescendant(rootNode);
1236         rootNode.recycle();
1237         if (targetNode == null) {
1238             L.w("Failed to find the first focus descendant");
1239             return;
1240         }
1241         performFocusAction(targetNode);
1242         targetNode.recycle();
1243     }
1244 
1245     /**
1246      * Sets {@link #mFocusedNode} to a copy of the given node, and clears {@link #mLastTouchedNode}.
1247      */
setFocusedNode(@ullable AccessibilityNodeInfo focusedNode)1248     private void setFocusedNode(@Nullable AccessibilityNodeInfo focusedNode) {
1249         setFocusedNodeInternal(focusedNode);
1250         if (mFocusedNode != null && mLastTouchedNode != null) {
1251             setLastTouchedNodeInternal(null);
1252         }
1253     }
1254 
setFocusedNodeInternal(@ullable AccessibilityNodeInfo focusedNode)1255     private void setFocusedNodeInternal(@Nullable AccessibilityNodeInfo focusedNode) {
1256         if ((mFocusedNode == null && focusedNode == null) ||
1257                 (mFocusedNode != null && mFocusedNode.equals(focusedNode))) {
1258             L.d("Don't reset mFocusedNode since it stays the same: " + mFocusedNode);
1259             return;
1260         }
1261         if (mInDirectManipulationMode && focusedNode == null) {
1262             // Toggle off direct manipulation mode since there is no focused node.
1263             mInDirectManipulationMode = false;
1264             L.d("Exit direct manipulation mode since there is no focused node");
1265         }
1266 
1267         // Recycle mPreviousFocusedNode only when it's not the same with focusedNode.
1268         if (mPreviousFocusedNode != focusedNode) {
1269             Utils.recycleNode(mPreviousFocusedNode);
1270         } else {
1271             // TODO(b/159949186)
1272             L.e("mPreviousFocusedNode shouldn't be the same with focusedNode " + focusedNode);
1273         }
1274 
1275         mPreviousFocusedNode = mFocusedNode;
1276         mFocusedNode = copyNode(focusedNode);
1277 
1278         // Set mScrollableContainer to the scrollable container which contains mFocusedNode, if any.
1279         // Skip if mFocusedNode is a FocusParkingView. The FocusParkingView is focused when the
1280         // focus view is scrolled off the screen. We'll focus the scrollable container when we
1281         // receive the TYPE_VIEW_FOCUSED event in this case.
1282         if (mFocusedNode == null) {
1283             setScrollableContainer(null);
1284         } else if (!Utils.isFocusParkingView(mFocusedNode)) {
1285             setScrollableContainer(mNavigator.findScrollableContainer(mFocusedNode));
1286         }
1287 
1288         // Cache the focused node by focus area.
1289         if (mFocusedNode != null) {
1290             mNavigator.saveFocusedNode(mFocusedNode);
1291         }
1292     }
1293 
setScrollableContainer(@ullable AccessibilityNodeInfo scrollableContainer)1294     private void setScrollableContainer(@Nullable AccessibilityNodeInfo scrollableContainer) {
1295         if ((mScrollableContainer == null && scrollableContainer == null)
1296                 || (mScrollableContainer != null
1297                         && mScrollableContainer.equals(scrollableContainer))) {
1298             return;
1299         }
1300 
1301         Utils.recycleNode(mScrollableContainer);
1302         mScrollableContainer = copyNode(scrollableContainer);
1303     }
1304 
1305     /**
1306      * Sets {@link #mLastTouchedNode} to a copy of the given node, and clears {@link #mFocusedNode}.
1307      */
setLastTouchedNode(@ullable AccessibilityNodeInfo lastTouchedNode)1308     private void setLastTouchedNode(@Nullable AccessibilityNodeInfo lastTouchedNode) {
1309         setLastTouchedNodeInternal(lastTouchedNode);
1310         if (mLastTouchedNode != null && mFocusedNode != null) {
1311             setFocusedNodeInternal(null);
1312         }
1313     }
1314 
setLastTouchedNodeInternal(@ullable AccessibilityNodeInfo lastTouchedNode)1315     private void setLastTouchedNodeInternal(@Nullable AccessibilityNodeInfo lastTouchedNode) {
1316         if ((mLastTouchedNode == null && lastTouchedNode == null)
1317                 || (mLastTouchedNode != null && mLastTouchedNode.equals(lastTouchedNode))) {
1318             L.d("Don't reset mLastTouchedNode since it stays the same: " + mLastTouchedNode);
1319             return;
1320         }
1321 
1322         Utils.recycleNode(mLastTouchedNode);
1323         mLastTouchedNode = copyNode(lastTouchedNode);
1324     }
1325 
setIgnoreViewClickedNode(@ullable AccessibilityNodeInfo node)1326     private void setIgnoreViewClickedNode(@Nullable AccessibilityNodeInfo node) {
1327         if (mIgnoreViewClickedNode != null) {
1328             mIgnoreViewClickedNode.recycle();
1329         }
1330         mIgnoreViewClickedNode = copyNode(node);
1331         if (node != null) {
1332             mIgnoreViewClickedUntil = SystemClock.uptimeMillis() + mIgnoreViewClickedMs;
1333         }
1334     }
1335 
1336     /**
1337      * Performs {@link AccessibilityNodeInfo#ACTION_FOCUS} on the given {@code targetNode}.
1338      *
1339      * @return true if {@code targetNode} was focused already or became focused after performing
1340      *         {@link AccessibilityNodeInfo#ACTION_FOCUS}
1341      */
performFocusAction(@onNull AccessibilityNodeInfo targetNode)1342     private boolean performFocusAction(@NonNull AccessibilityNodeInfo targetNode) {
1343         if (targetNode.equals(mFocusedNode)) {
1344             return true;
1345         }
1346         if (targetNode.isFocused()) {
1347             L.w("targetNode is already focused: " + targetNode);
1348             setFocusedNode(targetNode);
1349             return true;
1350         }
1351         boolean focusCleared = false;
1352         if (Utils.hasFocus(targetNode)){
1353             // One of targetNode's descendants is already focused, so we can't perform ACTION_FOCUS
1354             // on targetNode directly. The workaround is to clear the focus first (by focusing on
1355             // the FocusParkingView), then focus on targetNode.
1356             L.d("One of targetNode's descendants is already focused: " + targetNode);
1357             if (!clearFocusInCurrentWindow()) {
1358                 return false;
1359             }
1360             focusCleared = true;
1361         }
1362         // Now we can perform ACTION_FOCUS on targetNode since it doesn't have focus, or its
1363         // descendant's focus has been cleared.
1364         boolean result = targetNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
1365         if (!result) {
1366             L.w("Failed to perform ACTION_FOCUS on node " + targetNode);
1367             // Previously we cleared the focus of targetNode's descendant, which won't reset the
1368             // focused node to null. So we need to reset it manually.
1369             if (focusCleared) {
1370                 setFocusedNode(null);
1371             }
1372             return false;
1373         }
1374 
1375         setFocusedNode(targetNode);
1376         return true;
1377     }
1378 
1379     /**
1380      * Returns the number of "ticks" to rotate for a single rotate event with the given detent
1381      * {@code count} at the given time. Uses and updates {@link #mLastRotateEventTime}. The result
1382      * will be one, two, or three times the given detent {@code count} depending on the interval
1383      * between the current event and the previous event and the detent {@code count}.
1384      *
1385      * @param count     the number of detents the user rotated
1386      * @param eventTime the {@link SystemClock#uptimeMillis} when the event occurred
1387      * @return the number of "ticks" to rotate
1388      */
getRotateAcceleration(int count, long eventTime)1389     private int getRotateAcceleration(int count, long eventTime) {
1390         // count is 0 when testing key "C" or "V" is pressed.
1391         if (count <= 0) {
1392             count = 1;
1393         }
1394         int result = count;
1395         // TODO(b/153195148): This method can be improved once we've plumbed through the VHAL
1396         //  changes. We'll get timestamps for each detent.
1397         long delta = (eventTime - mLastRotateEventTime) / count;  // Assume constant speed.
1398         if (delta <= mRotationAcceleration3xMs) {
1399             result = count * 3;
1400         } else if (delta <= mRotationAcceleration2xMs) {
1401             result = count * 2;
1402         }
1403         mLastRotateEventTime = eventTime;
1404         return result;
1405     }
1406 
copyNode(@ullable AccessibilityNodeInfo node)1407     private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) {
1408         return mNodeCopier.copy(node);
1409     }
1410 
1411     /**
1412      * Moves focus from the given {@code focusParkingView} to a view near the previously focused
1413      * view, which is chosen in the following order:
1414      * <ol>
1415      *   <li> the previously focused view ({@link #mPreviousFocusedNode}), if any
1416      *   <li> the default focus (app:defaultFocus) in the FocusArea that contains {@link
1417      *        #mFocusedNode}, if any
1418      *   <li> the first focusable view in the FocusArea that contains {@link #mFocusedNode}, if any,
1419      *        excluding any FocusParkingViews
1420      *   <li> the default focus in the window, if any, excluding any FocusParkingViews
1421      *   <li> the first focusable view in the window, if any, excluding any FocusParkingViews
1422      * </ol>
1423      */
moveFocusToNearbyView(@onNull AccessibilityNodeInfo focusParkingView)1424     private void moveFocusToNearbyView(@NonNull AccessibilityNodeInfo focusParkingView) {
1425         mPreviousFocusedNode = Utils.refreshNode(mPreviousFocusedNode);
1426         if (mPreviousFocusedNode != null && performFocusAction(mPreviousFocusedNode)) {
1427             L.d("Move focus to the previously focused node");
1428             return;
1429         }
1430         // TODO(b/158797952): do 2,3.
1431         if (focusParkingView.performAction(AccessibilityNodeInfo.ACTION_DISMISS)) {
1432             L.d("Move focus to the default focus in the window");
1433             return;
1434         }
1435         L.d("Try to focus on the first focusable view in the window");
1436         focusFirstFocusDescendant();
1437     }
1438 }
1439