• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS;
19 import static android.car.CarOccupantZoneManager.DisplayTypeEnum;
20 import static android.car.settings.CarSettings.Secure.KEY_ROTARY_KEY_EVENT_FILTER;
21 import static android.provider.Settings.Secure.DEFAULT_INPUT_METHOD;
22 import static android.provider.Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS;
23 import static android.provider.Settings.Secure.ENABLED_INPUT_METHODS;
24 import static android.view.Display.DEFAULT_DISPLAY;
25 import static android.view.KeyEvent.ACTION_DOWN;
26 import static android.view.KeyEvent.ACTION_UP;
27 import static android.view.KeyEvent.KEYCODE_UNKNOWN;
28 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
29 import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
30 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
31 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
32 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
33 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED;
34 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED;
35 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
36 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED;
37 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOWS_CHANGED;
38 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
39 import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ADDED;
40 import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_REMOVED;
41 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_SELECTION;
42 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
43 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
44 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
45 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_SELECT;
46 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD;
47 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD;
48 import static android.view.accessibility.AccessibilityWindowInfo.TYPE_APPLICATION;
49 import static android.view.accessibility.AccessibilityWindowInfo.TYPE_INPUT_METHOD;
50 
51 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;
52 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
53 import static com.android.car.ui.utils.RotaryConstants.ACTION_HIDE_IME;
54 import static com.android.car.ui.utils.RotaryConstants.ACTION_NUDGE_SHORTCUT;
55 import static com.android.car.ui.utils.RotaryConstants.ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA;
56 import static com.android.car.ui.utils.RotaryConstants.ACTION_RESTORE_DEFAULT_FOCUS;
57 import static com.android.car.ui.utils.RotaryConstants.NUDGE_DIRECTION;
58 
59 import android.accessibilityservice.AccessibilityService;
60 import android.accessibilityservice.AccessibilityServiceInfo;
61 import android.car.Car;
62 import android.car.CarOccupantZoneManager;
63 import android.car.input.CarInputManager;
64 import android.car.input.RotaryEvent;
65 import android.content.BroadcastReceiver;
66 import android.content.ComponentName;
67 import android.content.Context;
68 import android.content.Intent;
69 import android.content.IntentFilter;
70 import android.content.SharedPreferences;
71 import android.content.pm.ActivityInfo;
72 import android.content.pm.PackageManager;
73 import android.content.pm.ResolveInfo;
74 import android.content.res.Resources;
75 import android.database.ContentObserver;
76 import android.graphics.PixelFormat;
77 import android.graphics.Rect;
78 import android.hardware.display.DisplayManager;
79 import android.hardware.input.InputManager;
80 import android.os.Build;
81 import android.os.Bundle;
82 import android.os.Handler;
83 import android.os.Looper;
84 import android.os.Message;
85 import android.os.SystemClock;
86 import android.os.UserManager;
87 import android.provider.Settings;
88 import android.text.TextUtils;
89 import android.util.IndentingPrintWriter;
90 import android.util.proto.ProtoOutputStream;
91 import android.view.Display;
92 import android.view.Gravity;
93 import android.view.InputDevice;
94 import android.view.KeyEvent;
95 import android.view.MotionEvent;
96 import android.view.View;
97 import android.view.ViewConfiguration;
98 import android.view.WindowManager;
99 import android.view.accessibility.AccessibilityEvent;
100 import android.view.accessibility.AccessibilityNodeInfo;
101 import android.view.accessibility.AccessibilityWindowInfo;
102 import android.widget.FrameLayout;
103 
104 import androidx.annotation.NonNull;
105 import androidx.annotation.Nullable;
106 import androidx.annotation.VisibleForTesting;
107 
108 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
109 import com.android.car.ui.utils.DirectManipulationHelper;
110 import com.android.internal.util.ArrayUtils;
111 import com.android.internal.util.dump.DualDumpOutputStream;
112 
113 import java.io.FileDescriptor;
114 import java.io.FileOutputStream;
115 import java.io.PrintWriter;
116 import java.lang.ref.WeakReference;
117 import java.net.URISyntaxException;
118 import java.util.Arrays;
119 import java.util.Collections;
120 import java.util.HashMap;
121 import java.util.List;
122 import java.util.Map;
123 
124 /**
125  * A service that can change focus based on rotary controller rotation and nudges, and perform
126  * clicks based on rotary controller center button clicks.
127  * <p>
128  * As an {@link AccessibilityService}, this service responds to {@link KeyEvent}s (on debug builds
129  * only) and {@link AccessibilityEvent}s.
130  * <p>
131  * On debug builds, {@link KeyEvent}s coming from the keyboard are handled by clicking the view, or
132  * moving the focus, sometimes within a window and sometimes between windows.
133  * <p>
134  * This service listens to two types of {@link AccessibilityEvent}s: {@link
135  * AccessibilityEvent#TYPE_VIEW_FOCUSED} and {@link AccessibilityEvent#TYPE_VIEW_CLICKED}. The
136  * former is used to keep {@link #mFocusedNode} up to date as the focus changes. The latter is used
137  * to detect when the user switches from rotary mode to touch mode and to keep {@link
138  * #mLastTouchedNode} up to date.
139  * <p>
140  * As a {@link CarInputManager.CarInputCaptureCallback}, this service responds to {@link KeyEvent}s
141  * and {@link RotaryEvent}s, both of which are coming from the controller.
142  * <p>
143  * {@link KeyEvent}s are handled by clicking the view, or moving the focus, sometimes within a
144  * window and sometimes between windows.
145  * <p>
146  * {@link RotaryEvent}s are handled by moving the focus within the same {@link
147  * com.android.car.ui.FocusArea}.
148  * <p>
149  * Note: onFoo methods are all called on the main thread so no locks are needed.
150  */
151 public class RotaryService extends AccessibilityService implements
152         CarInputManager.CarInputCaptureCallback {
153 
154     /**
155      * How many detents to rotate when the user holds in shift while pressing C, V, Q, or E on a
156      * debug build.
157      */
158     private static final int SHIFT_DETENTS = 10;
159 
160     /**
161      * A value to indicate that it isn't one of the nudge directions. (i.e.
162      * {@link View#FOCUS_LEFT}, {@link View#FOCUS_UP}, {@link View#FOCUS_RIGHT}, or
163      * {@link View#FOCUS_DOWN}).
164      */
165     private static final int INVALID_NUDGE_DIRECTION = -1;
166 
167     /**
168      * Message for timer indicating that the center button has been held down long enough to trigger
169      * a long-press.
170      */
171     private static final int MSG_LONG_PRESS = 1;
172 
173     private static final String SHARED_PREFS = "com.android.car.rotary.RotaryService";
174     private static final String TOUCH_INPUT_METHOD_PREFIX = "TOUCH_INPUT_METHOD_";
175 
176     /**
177      * Key for activity metadata indicating that a nudge in the given direction ("up", "down",
178      * "left", or "right") that would otherwise do nothing should trigger a global action, e.g.
179      * {@link #GLOBAL_ACTION_BACK}.
180      */
181     private static final String OFF_SCREEN_NUDGE_GLOBAL_ACTION_FORMAT = "nudge.%s.globalAction";
182     /**
183      * Key for activity metadata indicating that a nudge in the given direction ("up", "down",
184      * "left", or "right") that would otherwise do nothing should trigger a key click, e.g. {@link
185      * KeyEvent#KEYCODE_BACK}.
186      */
187     private static final String OFF_SCREEN_NUDGE_KEY_CODE_FORMAT = "nudge.%s.keyCode";
188     /**
189      * Key for activity metadata indicating that a nudge in the given direction ("up", "down",
190      * "left", or "right") that would otherwise do nothing should launch an activity via an intent.
191      */
192     private static final String OFF_SCREEN_NUDGE_INTENT_FORMAT = "nudge.%s.intent";
193 
194     private static final int INVALID_GLOBAL_ACTION = -1;
195 
196     private static final int NUM_DIRECTIONS = 4;
197 
198     /**
199      * Maps a direction to a string used to look up an off-screen nudge action in an activity's
200      * metadata.
201      *
202      * @see #OFF_SCREEN_NUDGE_GLOBAL_ACTION_FORMAT
203      * @see #OFF_SCREEN_NUDGE_KEY_CODE_FORMAT
204      * @see #OFF_SCREEN_NUDGE_INTENT_FORMAT
205      */
206     private static final Map<Integer, String> DIRECTION_TO_STRING;
207     static {
208         Map<Integer, String> map = new HashMap<>();
map.put(View.FOCUS_UP, "up")209         map.put(View.FOCUS_UP, "up");
map.put(View.FOCUS_DOWN, "down")210         map.put(View.FOCUS_DOWN, "down");
map.put(View.FOCUS_LEFT, "left")211         map.put(View.FOCUS_LEFT, "left");
map.put(View.FOCUS_RIGHT, "right")212         map.put(View.FOCUS_RIGHT, "right");
213         DIRECTION_TO_STRING = Collections.unmodifiableMap(map);
214     }
215 
216     /**
217      * Maps a direction to an index used to look up an off-screen nudge action in .
218      *
219      * @see #mOffScreenNudgeGlobalActions
220      * @see #mOffScreenNudgeKeyCodes
221      * @see #mOffScreenNudgeIntents
222      */
223     private static final Map<Integer, Integer> DIRECTION_TO_INDEX;
224     static {
225         Map<Integer, Integer> map = new HashMap<>();
map.put(View.FOCUS_UP, 0)226         map.put(View.FOCUS_UP, 0);
map.put(View.FOCUS_DOWN, 1)227         map.put(View.FOCUS_DOWN, 1);
map.put(View.FOCUS_LEFT, 2)228         map.put(View.FOCUS_LEFT, 2);
map.put(View.FOCUS_RIGHT, 3)229         map.put(View.FOCUS_RIGHT, 3);
230         DIRECTION_TO_INDEX = Collections.unmodifiableMap(map);
231     }
232 
233     /**
234      * A reference to {@link #mWindowContext} or null if one hasn't been created. This is static in
235      * order to prevent the creation of multiple window contexts when this service is enabled and
236      * disabled repeatedly. Android imposes a limit on the number of window contexts without a
237      * corresponding surface.
238      */
239     @Nullable private static WeakReference<Context> sWindowContext;
240 
241     @NonNull
242     private NodeCopier mNodeCopier = new NodeCopier();
243 
244     private Navigator mNavigator;
245 
246     /** Input types to capture. */
247     private final int[] mInputTypes = new int[]{
248             // Capture controller rotation.
249             CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION,
250             // Capture controller center button clicks.
251             CarInputManager.INPUT_TYPE_DPAD_KEYS,
252             // Capture controller nudges.
253             CarInputManager.INPUT_TYPE_SYSTEM_NAVIGATE_KEYS};
254 
255     /**
256      * Time interval in milliseconds to decide whether we should accelerate the rotation by 3 times
257      * for a rotate event.
258      */
259     private int mRotationAcceleration3xMs;
260 
261     /**
262      * Time interval in milliseconds to decide whether we should accelerate the rotation by 2 times
263      * for a rotate event.
264      */
265     private int mRotationAcceleration2xMs;
266 
267     /**
268      * The currently focused node, if any. This is typically set when performing {@code
269      * ACTION_FOCUS} on a node. However, when performing {@code ACTION_FOCUS} on a {@code
270      * FocusArea}, this is set to the {@code FocusArea} until we receive a {@code TYPE_VIEW_FOCUSED}
271      * event with the descendant of the {@code FocusArea} that was actually focused. It's null if no
272      * nodes are focused or a {@link com.android.car.ui.FocusParkingView} is focused.
273      */
274     private AccessibilityNodeInfo mFocusedNode = null;
275 
276     /**
277      * The node being edited by the IME, if any. When focus moves to the IME, if it's moving from an
278      * editable node, we leave it focused. This variable is used to keep track of it so that we can
279      * return to it when the user nudges out of the IME.
280      */
281     private AccessibilityNodeInfo mEditNode = null;
282 
283     /**
284      * The focus area that contains the {@link #mFocusedNode}. It's null if {@link #mFocusedNode} is
285      * null.
286      */
287     private AccessibilityNodeInfo mFocusArea = null;
288 
289     /**
290      * The last clicked node by touching the screen, if any were clicked since we last navigated.
291      */
292     @VisibleForTesting
293     AccessibilityNodeInfo mLastTouchedNode = null;
294 
295     /**
296      * How many milliseconds to ignore {@link AccessibilityEvent#TYPE_VIEW_CLICKED} events after
297      * performing {@link AccessibilityNodeInfo#ACTION_CLICK} or injecting a {@link
298      * KeyEvent#KEYCODE_DPAD_CENTER} event.
299      */
300     private int mIgnoreViewClickedMs;
301 
302     /**
303      * When not {@code null}, {@link AccessibilityEvent#TYPE_VIEW_CLICKED} events with this node
304      * are ignored if they occur within {@link #mIgnoreViewClickedMs} of {@link
305      * #mLastViewClickedTime}.
306      */
307     @VisibleForTesting
308     AccessibilityNodeInfo mIgnoreViewClickedNode;
309 
310     /**
311      * The time of the last {@link AccessibilityEvent#TYPE_VIEW_CLICKED} event in {@link
312      * SystemClock#uptimeMillis}.
313      */
314     private long mLastViewClickedTime;
315 
316     /** Component name of rotary IME. Empty if none. */
317     @Nullable private String mRotaryInputMethod;
318 
319     /** Component name of default IME used in touch mode. */
320     @Nullable private String mDefaultTouchInputMethod;
321 
322     /** Component name of current IME used in touch mode. */
323     @Nullable private String mTouchInputMethod;
324 
325     /** Observer to update {@link #mTouchInputMethod} when the user switches IMEs. */
326     private ContentObserver mInputMethodObserver;
327 
328     /** Observer to update service info when the developer toggles key event filtering. */
329     private ContentObserver mKeyEventFilterObserver;
330 
331     private SharedPreferences mPrefs;
332     private UserManager mUserManager;
333 
334     /**
335      * The direction of the HUN. If there is no focused node, or the focused node is outside the
336      * HUN, nudging to this direction will focus on a node inside the HUN.
337      */
338     @VisibleForTesting
339     @View.FocusRealDirection
340     int mHunNudgeDirection;
341 
342     /**
343      * The direction to escape the HUN. If the focused node is inside the HUN, nudging to this
344      * direction will move focus to a node outside the HUN, while nudging to other directions
345      * will do nothing.
346      */
347     @VisibleForTesting
348     @View.FocusRealDirection
349     int mHunEscapeNudgeDirection;
350 
351     /**
352      * Global actions to perform when the user nudges up, down, left, or right off the edge of the
353      * screen. No global action is performed if the relevant element of this array is
354      * {@link #INVALID_GLOBAL_ACTION}.
355      */
356     private int[] mOffScreenNudgeGlobalActions;
357     /**
358      * Key codes of click events to inject when the user nudges up, down, left, or right off the
359      * edge of the screen. No event is injected if the relevant element of this array is
360      * {@link KeyEvent#KEYCODE_UNKNOWN}.
361      */
362     private int[] mOffScreenNudgeKeyCodes;
363     /**
364      * Intents to launch an activity when the user nudges up, down, left, or right off the edge of
365      * the screen. No activity is launched if the relevant element of this array is null.
366      */
367     private final Intent[] mOffScreenNudgeIntents = new Intent[NUM_DIRECTIONS];
368 
369     /**
370      * Possible actions to do after receiving {@link AccessibilityEvent#TYPE_VIEW_SCROLLED}.
371      *
372      * @see #injectScrollEvent
373      * TODO(b/185154771): Replace with #IntDef
374      */
375     enum AfterScrollAction {
376         /** Do nothing. */
377         NONE,
378         /**
379          * Focus the view before the focused view in Tab order in the scrollable container, if any.
380          */
381         FOCUS_PREVIOUS,
382         /**
383          * Focus the view after the focused view in Tab order in the scrollable container, if any.
384          */
385         FOCUS_NEXT,
386         /** Focus the first view in the scrollable container, if any. */
387         FOCUS_FIRST,
388         /** Focus the last view in the scrollable container, if any. */
389         FOCUS_LAST,
390     }
391 
392     private AfterScrollAction mAfterScrollAction = AfterScrollAction.NONE;
393 
394     /**
395      * How many milliseconds to wait for a {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event after
396      * scrolling.
397      */
398     private int mAfterScrollTimeoutMs;
399 
400     /**
401      * When to give up on receiving {@link AccessibilityEvent#TYPE_VIEW_SCROLLED}, in
402      * {@link SystemClock#uptimeMillis}.
403      */
404     private long mAfterScrollActionUntil;
405 
406     /** Whether we're in rotary mode (vs touch mode). */
407     @VisibleForTesting
408     boolean mInRotaryMode;
409 
410     /**
411      * Whether we're in direct manipulation mode.
412      * <p>
413      * If the focused node supports rotate directly, this mode is controlled by us. Otherwise
414      * this mode is controlled by the client app, which is responsible for updating the mode by
415      * calling {@link DirectManipulationHelper#enableDirectManipulationMode} when needed.
416      */
417     @VisibleForTesting
418     boolean mInDirectManipulationMode;
419 
420     /** The {@link SystemClock#uptimeMillis} when the last rotary rotation event occurred. */
421     private long mLastRotateEventTime;
422 
423     /**
424      * How many milliseconds the center buttons must be held down before a long-press is triggered.
425      * This doesn't apply to the application window.
426      */
427     @VisibleForTesting
428     long mLongPressMs;
429 
430     /**
431      * Whether the center button was held down long enough to trigger a long-press. In this case, a
432      * click won't be triggered when the center button is released.
433      */
434     private boolean mLongPressTriggered;
435 
436     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
437         @Override
438         public void handleMessage(@NonNull Message msg) {
439             if (msg.what == MSG_LONG_PRESS) {
440                 handleCenterButtonLongPressEvent();
441             }
442         }
443     };
444 
445     /**
446      * A context to use for fetching the {@link WindowManager} and creating the touch overlay or
447      * null if one hasn't been created yet.
448      */
449     @Nullable private Context mWindowContext;
450 
451     /**
452      * Mapping from test keycodes to production keycodes. During development, you can use a USB
453      * keyboard as a stand-in for rotary hardware. To enable this: {@code adb shell settings put
454      * secure android.car.ROTARY_KEY_EVENT_FILTER 1}.
455      */
456     private static final Map<Integer, Integer> TEST_TO_REAL_KEYCODE_MAP;
457 
458     private static final Map<Integer, Integer> DIRECTION_TO_KEYCODE_MAP;
459 
460     static {
461         Map<Integer, Integer> map = new HashMap<>();
map.put(KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT)462         map.put(KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT);
map.put(KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT)463         map.put(KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT);
map.put(KeyEvent.KEYCODE_W, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP)464         map.put(KeyEvent.KEYCODE_W, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP);
map.put(KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN)465         map.put(KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN);
map.put(KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_DPAD_CENTER)466         map.put(KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_DPAD_CENTER);
map.put(KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_BACK)467         map.put(KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_BACK);
468         // Legacy map
map.put(KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT)469         map.put(KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT);
map.put(KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT)470         map.put(KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT);
map.put(KeyEvent.KEYCODE_I, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP)471         map.put(KeyEvent.KEYCODE_I, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP);
map.put(KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN)472         map.put(KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN);
map.put(KeyEvent.KEYCODE_COMMA, KeyEvent.KEYCODE_DPAD_CENTER)473         map.put(KeyEvent.KEYCODE_COMMA, KeyEvent.KEYCODE_DPAD_CENTER);
map.put(KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_BACK)474         map.put(KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_BACK);
475 
476         TEST_TO_REAL_KEYCODE_MAP = Collections.unmodifiableMap(map);
477     }
478 
479     static {
480         Map<Integer, Integer> map = new HashMap<>();
map.put(View.FOCUS_UP, KeyEvent.KEYCODE_DPAD_UP)481         map.put(View.FOCUS_UP, KeyEvent.KEYCODE_DPAD_UP);
map.put(View.FOCUS_DOWN, KeyEvent.KEYCODE_DPAD_DOWN)482         map.put(View.FOCUS_DOWN, KeyEvent.KEYCODE_DPAD_DOWN);
map.put(View.FOCUS_LEFT, KeyEvent.KEYCODE_DPAD_LEFT)483         map.put(View.FOCUS_LEFT, KeyEvent.KEYCODE_DPAD_LEFT);
map.put(View.FOCUS_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT)484         map.put(View.FOCUS_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT);
485 
486         DIRECTION_TO_KEYCODE_MAP = Collections.unmodifiableMap(map);
487     }
488 
489     private final BroadcastReceiver mHomeButtonReceiver = new BroadcastReceiver() {
490         // Should match the values in PhoneWindowManager.java
491         private static final String SYSTEM_DIALOG_REASON_KEY = "reason";
492         private static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
493 
494         @Override
495         public void onReceive(Context context, Intent intent) {
496             String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
497             if (!SYSTEM_DIALOG_REASON_HOME_KEY.equals(reason)) {
498                 L.d("Skipping the processing of ACTION_CLOSE_SYSTEM_DIALOGS broadcast event due "
499                         + "to reason: " + reason);
500                 return;
501             }
502 
503             // Trigger a back action in order to exit direct manipulation mode.
504             if (mInDirectManipulationMode) {
505                 handleBackButtonEvent(ACTION_DOWN);
506                 handleBackButtonEvent(ACTION_UP);
507             }
508 
509             List<AccessibilityWindowInfo> windows = getWindows();
510             for (AccessibilityWindowInfo window : windows) {
511                 if (window == null) {
512                     continue;
513                 }
514 
515                 if (mInRotaryMode && mNavigator.isMainApplicationWindow(window)) {
516                     // Post this in a handler so that there is no race condition between app
517                     // transitions and restoration of focus.
518                     getMainThreadHandler().post(() -> {
519                         AccessibilityNodeInfo rootView = window.getRoot();
520                         if (rootView == null) {
521                             L.e("Root view in application window no longer exists");
522                             return;
523                         }
524                         boolean result = restoreDefaultFocusInRoot(rootView);
525                         if (!result) {
526                             L.e("Failed to focus the default element in the application window");
527                         }
528                         Utils.recycleNode(rootView);
529                     });
530                 } else {
531                     // Post this in a handler so that there is no race condition between app
532                     // transitions and restoration of focus.
533                     getMainThreadHandler().post(() -> {
534                         boolean result = clearFocusInWindow(window);
535                         if (!result) {
536                             L.e("Failed to clear the focus in window: " + window);
537                         }
538                     });
539                 }
540             }
541             Utils.recycleWindows(windows);
542         }
543     };
544 
545     private Car mCar;
546     private CarInputManager mCarInputManager;
547     private InputManager mInputManager;
548 
549     /** Component name of foreground activity. */
550     @VisibleForTesting
551     @Nullable
552     ComponentName mForegroundActivity;
553 
554     private WindowManager mWindowManager;
555 
556     private final WindowCache mWindowCache = new WindowCache();
557 
558     /**
559      * The last node which has performed {@link AccessibilityNodeInfo#ACTION_FOCUS} if it hasn't
560      * reported a {@link AccessibilityEvent#TYPE_VIEW_FOCUSED} event yet. Null otherwise.
561      */
562     @Nullable private AccessibilityNodeInfo mPendingFocusedNode;
563 
564     private long mAfterFocusTimeoutMs;
565 
566     /** Expiration time for {@link #mPendingFocusedNode} in {@link SystemClock#uptimeMillis}. */
567     private long mPendingFocusedExpirationTime;
568 
569     private final BroadcastReceiver mAppInstallUninstallReceiver = new BroadcastReceiver() {
570         @Override
571         public void onReceive(Context context, Intent intent) {
572             String packageName = intent.getData().getSchemeSpecificPart();
573             if (TextUtils.isEmpty(packageName)) {
574                 L.e("System sent an empty app install/uninstall broadcast");
575                 return;
576             }
577             if (mNavigator == null) {
578                 L.v("mNavigator is not initialized");
579                 return;
580             }
581             if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
582                 mNavigator.clearHostApp(packageName);
583             } else {
584                 mNavigator.initHostApp(getPackageManager());
585             }
586         }
587     };
588 
589     @Override
onCreate()590     public void onCreate() {
591         super.onCreate();
592         Resources res = getResources();
593         mRotationAcceleration3xMs = res.getInteger(R.integer.rotation_acceleration_3x_ms);
594         mRotationAcceleration2xMs = res.getInteger(R.integer.rotation_acceleration_2x_ms);
595 
596         int hunMarginHorizontal =
597                 res.getDimensionPixelSize(R.dimen.notification_headsup_card_margin_horizontal);
598         int hunLeft = hunMarginHorizontal;
599         WindowManager windowManager = getSystemService(WindowManager.class);
600         Rect displayBounds = windowManager.getCurrentWindowMetrics().getBounds();
601         int displayWidth = displayBounds.width();
602         int displayHeight = displayBounds.height();
603         int hunRight = displayWidth - hunMarginHorizontal;
604         boolean showHunOnBottom = res.getBoolean(R.bool.config_showHeadsUpNotificationOnBottom);
605         mHunNudgeDirection = showHunOnBottom ? View.FOCUS_DOWN : View.FOCUS_UP;
606         mHunEscapeNudgeDirection = showHunOnBottom ? View.FOCUS_UP : View.FOCUS_DOWN;
607 
608         mIgnoreViewClickedMs = res.getInteger(R.integer.ignore_view_clicked_ms);
609         mAfterScrollTimeoutMs = res.getInteger(R.integer.after_scroll_timeout_ms);
610 
611         String[] excludedOverlayWindowTitles =
612                 res.getStringArray(R.array.excluded_application_overlay_window_titles);
613 
614         mNavigator = new Navigator(displayWidth, displayHeight, hunLeft, hunRight, showHunOnBottom,
615                 excludedOverlayWindowTitles);
616         mNavigator.initHostApp(getPackageManager());
617 
618         mPrefs = createDeviceProtectedStorageContext().getSharedPreferences(SHARED_PREFS,
619                 Context.MODE_PRIVATE);
620         mUserManager = getSystemService(UserManager.class);
621 
622         mRotaryInputMethod = res.getString(R.string.rotary_input_method);
623         mDefaultTouchInputMethod = res.getString(R.string.default_touch_input_method);
624         mTouchInputMethod = mPrefs.getString(TOUCH_INPUT_METHOD_PREFIX + mUserManager.getUserName(),
625                 mDefaultTouchInputMethod);
626         if (mRotaryInputMethod != null
627                 && mRotaryInputMethod.equals(getCurrentIme())
628                 && isValidIme(mTouchInputMethod)) {
629             // Switch from the rotary IME to the touch IME in case Android defaults to the rotary
630             // IME.
631             // TODO(b/169423887): Figure out how to configure the default IME through Android
632             // without needing to do this.
633             setCurrentIme(mTouchInputMethod);
634         }
635 
636         mAfterFocusTimeoutMs = res.getInteger(R.integer.after_focus_timeout_ms);
637 
638         mLongPressMs = res.getInteger(R.integer.long_press_ms);
639         if (mLongPressMs == 0) {
640             mLongPressMs = ViewConfiguration.getLongPressTimeout();
641         }
642 
643         mOffScreenNudgeGlobalActions = res.getIntArray(R.array.off_screen_nudge_global_actions);
644         mOffScreenNudgeKeyCodes = res.getIntArray(R.array.off_screen_nudge_key_codes);
645         String[] intentUrls = res.getStringArray(R.array.off_screen_nudge_intents);
646         for (int i = 0; i < NUM_DIRECTIONS; i++) {
647             String intentUrl = intentUrls[i];
648             if (intentUrl == null || intentUrl.isEmpty()) {
649                 continue;
650             }
651             try {
652                 mOffScreenNudgeIntents[i] = Intent.parseUri(intentUrl, Intent.URI_INTENT_SCHEME);
653             } catch (URISyntaxException e) {
654                 L.w("Invalid off-screen nudge intent: " + intentUrl);
655             }
656         }
657 
658         getWindowContext().registerReceiver(mHomeButtonReceiver,
659                 new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
660 
661         IntentFilter filter = new IntentFilter();
662         filter.addAction(Intent.ACTION_PACKAGE_ADDED);
663         filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
664         filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
665         filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
666         filter.addDataScheme("package");
667         registerReceiver(mAppInstallUninstallReceiver, filter);
668     }
669 
670     /**
671      * {@inheritDoc}
672      * <p>
673      * We need to access WindowManager in onCreate() and
674      * IAccessibilityServiceClientWrapper.Callbacks#init(). Since WindowManager is a visual
675      * service, only Activity or other visual Context can access it. So we create a window context
676      * (a visual context) and delegate getSystemService() to it.
677      */
678     @Override
getSystemService(@erviceName @onNull String name)679     public Object getSystemService(@ServiceName @NonNull String name) {
680         // Guarantee that we always return the same WindowManager instance.
681         if (WINDOW_SERVICE.equals(name)) {
682             if (mWindowManager == null) {
683                 Context windowContext = getWindowContext();
684                 mWindowManager = (WindowManager) windowContext.getSystemService(WINDOW_SERVICE);
685             }
686             return mWindowManager;
687         }
688         return super.getSystemService(name);
689     }
690 
691     @Override
onServiceConnected()692     public void onServiceConnected() {
693         super.onServiceConnected();
694 
695         mCar = Car.createCar(this, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
696                 (car, ready) -> {
697                     mCar = car;
698                     if (ready) {
699                         mCarInputManager =
700                                 (CarInputManager) mCar.getCarManager(Car.CAR_INPUT_SERVICE);
701                         mCarInputManager.requestInputEventCapture(
702                                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
703                                 mInputTypes,
704                                 CarInputManager.CAPTURE_REQ_FLAGS_ALLOW_DELAYED_GRANT,
705                                 /* callback= */ this);
706                     }
707                 });
708 
709         updateServiceInfo();
710 
711         mInputManager = getSystemService(InputManager.class);
712 
713         // Add an overlay to capture touch events.
714         addTouchOverlay();
715 
716         // Register an observer to update mTouchInputMethod whenever the user switches IMEs.
717         registerInputMethodObserver();
718 
719         // Register an observer to update the service info when the developer changes the filter
720         // setting.
721         registerFilterObserver();
722     }
723 
724     @Override
onInterrupt()725     public void onInterrupt() {
726         L.v("onInterrupt()");
727     }
728 
729     @Override
onDestroy()730     public void onDestroy() {
731         unregisterReceiver(mAppInstallUninstallReceiver);
732         getWindowContext().unregisterReceiver(mHomeButtonReceiver);
733 
734         unregisterInputMethodObserver();
735         unregisterFilterObserver();
736         if (mCarInputManager != null) {
737             mCarInputManager.releaseInputEventCapture(CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
738         }
739         if (mCar != null) {
740             mCar.disconnect();
741         }
742 
743         // Reset to touch IME if the current IME is rotary IME.
744         mInRotaryMode = false;
745         updateIme();
746 
747         super.onDestroy();
748     }
749 
750     @Override
onAccessibilityEvent(AccessibilityEvent event)751     public void onAccessibilityEvent(AccessibilityEvent event) {
752         L.v("onAccessibilityEvent: " + event);
753         AccessibilityNodeInfo source = event.getSource();
754         if (source != null) {
755             L.v("event source: " + source);
756         }
757         L.v("event window ID: " + Integer.toHexString(event.getWindowId()));
758 
759         switch (event.getEventType()) {
760             case TYPE_VIEW_FOCUSED: {
761                 handleViewFocusedEvent(event, source);
762                 break;
763             }
764             case TYPE_VIEW_CLICKED: {
765                 handleViewClickedEvent(event, source);
766                 break;
767             }
768             case TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
769                 updateDirectManipulationMode(event, true);
770                 break;
771             }
772             case TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
773                 updateDirectManipulationMode(event, false);
774                 break;
775             }
776             case TYPE_VIEW_SCROLLED: {
777                 handleViewScrolledEvent(source);
778                 break;
779             }
780             case TYPE_WINDOW_STATE_CHANGED: {
781                 if (source != null) {
782                     AccessibilityWindowInfo window = source.getWindow();
783                     if (window != null) {
784                         if (window.getType() == TYPE_APPLICATION
785                                 && window.getDisplayId() == DEFAULT_DISPLAY) {
786                             onForegroundActivityChanged(source, event.getPackageName(),
787                                     event.getClassName());
788                         }
789                         window.recycle();
790                     }
791                 }
792                 break;
793             }
794             case TYPE_WINDOWS_CHANGED: {
795                 if ((event.getWindowChanges() & WINDOWS_CHANGE_REMOVED) != 0) {
796                     handleWindowRemovedEvent(event);
797                 }
798                 if ((event.getWindowChanges() & WINDOWS_CHANGE_ADDED) != 0) {
799                     handleWindowAddedEvent(event);
800                 }
801                 break;
802             }
803             default:
804                 // Do nothing.
805         }
806         Utils.recycleNode(source);
807     }
808 
809     /**
810      * Callback of {@link AccessibilityService}. It allows us to observe testing {@link KeyEvent}s
811      * from keyboard, including keys "C" and "V" to emulate controller rotation, keys "J" "L" "I"
812      * "K" to emulate controller nudges, and key "Comma" to emulate center button clicks.
813      */
814     @Override
onKeyEvent(KeyEvent event)815     protected boolean onKeyEvent(KeyEvent event) {
816         if (Build.IS_DEBUGGABLE) {
817             return handleKeyEvent(event);
818         }
819         return false;
820     }
821 
822     /**
823      * Callback of {@link CarInputManager.CarInputCaptureCallback}. It allows us to capture {@link
824      * KeyEvent}s generated by a navigation controller, such as controller nudge and controller
825      * click events.
826      */
827     @Override
onKeyEvents(@isplayTypeEnum int targetDisplayType, @NonNull List<KeyEvent> events)828     public void onKeyEvents(@DisplayTypeEnum int targetDisplayType,
829             @NonNull List<KeyEvent> events) {
830         if (!isValidDisplayType(targetDisplayType)) {
831             return;
832         }
833         for (KeyEvent event : events) {
834             handleKeyEvent(event);
835         }
836     }
837 
838     /**
839      * Callback of {@link CarInputManager.CarInputCaptureCallback}. It allows us to capture {@link
840      * RotaryEvent}s generated by a navigation controller.
841      */
842     @Override
onRotaryEvents(@isplayTypeEnum int targetDisplayType, @NonNull List<RotaryEvent> events)843     public void onRotaryEvents(@DisplayTypeEnum int targetDisplayType,
844             @NonNull List<RotaryEvent> events) {
845         if (!isValidDisplayType(targetDisplayType)) {
846             return;
847         }
848         for (RotaryEvent rotaryEvent : events) {
849             handleRotaryEvent(rotaryEvent);
850         }
851     }
852 
getWindowContext()853     private Context getWindowContext() {
854         if (mWindowContext == null && sWindowContext != null) {
855             mWindowContext = sWindowContext.get();
856             if (mWindowContext != null) {
857                 L.d("Reusing window context");
858             }
859         }
860         if (mWindowContext == null) {
861             // We need to set the display before creating the WindowContext.
862             DisplayManager displayManager = getSystemService(DisplayManager.class);
863             Display primaryDisplay = displayManager.getDisplay(DEFAULT_DISPLAY);
864             updateDisplay(primaryDisplay.getDisplayId());
865             L.d("Creating window context");
866             mWindowContext = createWindowContext(TYPE_APPLICATION_OVERLAY, null);
867             sWindowContext = new WeakReference<>(mWindowContext);
868         }
869         return mWindowContext;
870     }
871 
872     /**
873      * Adds an overlay to capture touch events. The overlay has zero width and height so
874      * it doesn't prevent other windows from receiving touch events. It sets
875      * {@link WindowManager.LayoutParams#FLAG_WATCH_OUTSIDE_TOUCH} so it receives
876      * {@link MotionEvent#ACTION_OUTSIDE} events for touches anywhere on the screen. This
877      * is used to exit rotary mode when the user touches the screen, even if the touch
878      * isn't considered a click.
879      */
addTouchOverlay()880     private void addTouchOverlay() {
881         // Only views with a visual context, such as a window context, can be added by
882         // WindowManager.
883         FrameLayout frameLayout = new FrameLayout(getWindowContext());
884 
885         FrameLayout.LayoutParams frameLayoutParams =
886                 new FrameLayout.LayoutParams(/* width= */ 0, /* height= */ 0);
887         frameLayout.setLayoutParams(frameLayoutParams);
888         frameLayout.setOnTouchListener((view, event) -> {
889             // We're trying to identify real touches from the user's fingers, but using the rotary
890             // controller to press keys in the rotary IME also triggers this touch listener, so we
891             // ignore these touches.
892             if (mIgnoreViewClickedNode == null
893                     || event.getEventTime() >= mLastViewClickedTime + mIgnoreViewClickedMs) {
894                 onTouchEvent();
895             }
896             return false;
897         });
898         WindowManager.LayoutParams windowLayoutParams = new WindowManager.LayoutParams(
899                 /* w= */ 0,
900                 /* h= */ 0,
901                 TYPE_APPLICATION_OVERLAY,
902                 FLAG_NOT_FOCUSABLE | FLAG_WATCH_OUTSIDE_TOUCH,
903                 PixelFormat.TRANSPARENT);
904         windowLayoutParams.gravity = Gravity.RIGHT | Gravity.TOP;
905         windowLayoutParams.privateFlags |= PRIVATE_FLAG_TRUSTED_OVERLAY;
906         WindowManager windowManager = getSystemService(WindowManager.class);
907         windowManager.addView(frameLayout, windowLayoutParams);
908     }
909 
onTouchEvent()910     private void onTouchEvent() {
911         // The user touched the screen, so exit rotary mode. Do this even if mInRotaryMode is
912         // already false because this service might have crashed causing mInRotaryMode to be reset
913         // without a corresponding change to the IME.
914         setInRotaryMode(false);
915 
916         // Set mFocusedNode to null when user uses touch.
917         if (mFocusedNode != null) {
918             setFocusedNode(null);
919         }
920     }
921 
922     /**
923      * Updates this accessibility service's info, enabling or disabling key event filtering
924      * depending on a setting.
925      */
updateServiceInfo()926     private void updateServiceInfo() {
927         AccessibilityServiceInfo serviceInfo = getServiceInfo();
928         if (serviceInfo == null) {
929             L.w("Service info not available");
930             return;
931         }
932         int flags = serviceInfo.flags;
933         boolean filterKeyEvents = Settings.Secure.getInt(getContentResolver(),
934                 KEY_ROTARY_KEY_EVENT_FILTER, /* def= */ 0) != 0;
935         if (filterKeyEvents) {
936             flags |= FLAG_REQUEST_FILTER_KEY_EVENTS;
937         } else {
938             flags &= ~FLAG_REQUEST_FILTER_KEY_EVENTS;
939         }
940         if (flags == serviceInfo.flags) return;
941         L.d((filterKeyEvents ? "Enabling" : "Disabling") + " key event filtering");
942         serviceInfo.flags = flags;
943         setServiceInfo(serviceInfo);
944     }
945 
946     /**
947      * Registers an observer to updates {@link #mTouchInputMethod} whenever the user switches IMEs.
948      */
registerInputMethodObserver()949     private void registerInputMethodObserver() {
950         if (mInputMethodObserver != null) {
951             throw new IllegalStateException("Input method observer already registered");
952         }
953         mInputMethodObserver = new ContentObserver(new Handler(Looper.myLooper())) {
954             @Override
955             public void onChange(boolean selfChange) {
956                 // Either the user switched input methods or we did. In the former case, update
957                 // mTouchInputMethod and save it so we can switch back after switching to the rotary
958                 // input method.
959                 String inputMethod = getCurrentIme();
960                 if (inputMethod != null && !inputMethod.equals(mRotaryInputMethod)) {
961                     mTouchInputMethod = inputMethod;
962                     String userName = mUserManager.getUserName();
963                     mPrefs.edit()
964                             .putString(TOUCH_INPUT_METHOD_PREFIX + userName, mTouchInputMethod)
965                             .apply();
966                 }
967             }
968         };
969         getContentResolver().registerContentObserver(
970                 Settings.Secure.getUriFor(DEFAULT_INPUT_METHOD),
971                 /* notifyForDescendants= */ false,
972                 mInputMethodObserver);
973     }
974 
975     /** Unregisters the observer registered by {@link #registerInputMethodObserver}. */
unregisterInputMethodObserver()976     private void unregisterInputMethodObserver() {
977         if (mInputMethodObserver != null) {
978             getContentResolver().unregisterContentObserver(mInputMethodObserver);
979             mInputMethodObserver = null;
980         }
981     }
982 
983     /**
984      * Registers an observer to update our accessibility service info whenever the developer changes
985      * the key event filter setting.
986      */
registerFilterObserver()987     private void registerFilterObserver() {
988         if (mKeyEventFilterObserver != null) {
989             throw new IllegalStateException("Filter observer already registered");
990         }
991         mKeyEventFilterObserver = new ContentObserver(new Handler(Looper.myLooper())) {
992             @Override
993             public void onChange(boolean selfChange) {
994                 updateServiceInfo();
995             }
996         };
997         getContentResolver().registerContentObserver(
998                 Settings.Secure.getUriFor(KEY_ROTARY_KEY_EVENT_FILTER),
999                 /* notifyForDescendants= */ false,
1000                 mKeyEventFilterObserver);
1001     }
1002 
1003     /** Unregisters the observer registered by {@link #registerFilterObserver}. */
unregisterFilterObserver()1004     private void unregisterFilterObserver() {
1005         if (mKeyEventFilterObserver != null) {
1006             getContentResolver().unregisterContentObserver(mKeyEventFilterObserver);
1007             mKeyEventFilterObserver = null;
1008         }
1009     }
1010 
isValidDisplayType(int displayType)1011     private static boolean isValidDisplayType(int displayType) {
1012         if (displayType == CarOccupantZoneManager.DISPLAY_TYPE_MAIN) {
1013             return true;
1014         }
1015         L.e("RotaryService shouldn't capture events from display type " + displayType);
1016         return false;
1017     }
1018 
1019     /**
1020      * Handles key events. Returns whether the key event was consumed. To avoid invalid event stream
1021      * getting through to the application, if a key down event is consumed, the corresponding key up
1022      * event must be consumed too, and vice versa.
1023      */
handleKeyEvent(KeyEvent event)1024     private boolean handleKeyEvent(KeyEvent event) {
1025         int action = event.getAction();
1026         boolean isActionDown = action == ACTION_DOWN;
1027         int keyCode = getKeyCode(event);
1028         int detents = event.isShiftPressed() ? SHIFT_DETENTS : 1;
1029         switch (keyCode) {
1030             case KeyEvent.KEYCODE_Q:
1031             case KeyEvent.KEYCODE_C:
1032                 if (isActionDown) {
1033                     handleRotateEvent(/* clockwise= */ false, detents,
1034                             event.getEventTime());
1035                 }
1036                 return true;
1037             case KeyEvent.KEYCODE_E:
1038             case KeyEvent.KEYCODE_V:
1039                 if (isActionDown) {
1040                     handleRotateEvent(/* clockwise= */ true, detents,
1041                             event.getEventTime());
1042                 }
1043                 return true;
1044             case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT:
1045                 handleNudgeEvent(View.FOCUS_LEFT, action);
1046                 return true;
1047             case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT:
1048                 handleNudgeEvent(View.FOCUS_RIGHT, action);
1049                 return true;
1050             case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP:
1051                 handleNudgeEvent(View.FOCUS_UP, action);
1052                 return true;
1053             case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN:
1054                 handleNudgeEvent(View.FOCUS_DOWN, action);
1055                 return true;
1056             case KeyEvent.KEYCODE_DPAD_CENTER:
1057                 // Ignore repeat events. We only care about the initial ACTION_DOWN and the final
1058                 // ACTION_UP events.
1059                 if (event.getRepeatCount() == 0) {
1060                     handleCenterButtonEvent(action);
1061                 }
1062                 return true;
1063             case KeyEvent.KEYCODE_BACK:
1064                 if (mInDirectManipulationMode) {
1065                     handleBackButtonEvent(action);
1066                     return true;
1067                 }
1068                 return false;
1069             default:
1070                 // Do nothing
1071         }
1072         return false;
1073     }
1074 
1075     /** Handles {@link AccessibilityEvent#TYPE_VIEW_FOCUSED} event. */
handleViewFocusedEvent(@onNull AccessibilityEvent event, @Nullable AccessibilityNodeInfo sourceNode)1076     private void handleViewFocusedEvent(@NonNull AccessibilityEvent event,
1077             @Nullable AccessibilityNodeInfo sourceNode) {
1078         // A view was focused. We ignore focus changes in touch mode. We don't use
1079         // TYPE_VIEW_FOCUSED to keep mLastTouchedNode up to date because most views can't be
1080         // focused in touch mode.
1081         if (!mInRotaryMode) {
1082             return;
1083         }
1084         if (sourceNode == null) {
1085             L.w("Null source node in " + event);
1086             return;
1087         }
1088         if (mNavigator.isClientNode(sourceNode)) {
1089             L.d("Ignore focused event from the client app " + sourceNode);
1090             return;
1091         }
1092 
1093         // Update mFocusedNode if we're not waiting for focused event caused by performing an
1094         // action.
1095         refreshPendingFocusedNode();
1096         if (mPendingFocusedNode == null) {
1097             L.d("Focus event wasn't caused by performing an action");
1098             // If it's a FocusParkingView, only update mFocusedNode when it's in the same window
1099             // with mFocusedNode.
1100             if (Utils.isFocusParkingView(sourceNode)) {
1101                 if (mFocusedNode != null
1102                         && sourceNode.getWindowId() == mFocusedNode.getWindowId()) {
1103                     setFocusedNode(null);
1104                 }
1105                 return;
1106             }
1107             // If it's not a FocusParkingView, update mFocusedNode.
1108             setFocusedNode(sourceNode);
1109             return;
1110         }
1111 
1112         // If we're waiting for focused event but this isn't the one we're waiting for, ignore this
1113         // event. This event doesn't matter because focus has moved from sourceNode to
1114         // mPendingFocusedNode.
1115         if (!sourceNode.equals(mPendingFocusedNode)) {
1116             L.d("Ignoring focus event because focus has since moved");
1117             return;
1118         }
1119 
1120         // The event we're waiting for has arrived, so reset mPendingFocusedNode.
1121         L.d("Ignoring focus event caused by performing an action");
1122         setPendingFocusedNode(null);
1123     }
1124 
1125     /** Handles {@link AccessibilityEvent#TYPE_VIEW_CLICKED} event. */
handleViewClickedEvent(@onNull AccessibilityEvent event, @Nullable AccessibilityNodeInfo sourceNode)1126     private void handleViewClickedEvent(@NonNull AccessibilityEvent event,
1127             @Nullable AccessibilityNodeInfo sourceNode) {
1128         // A view was clicked. If we triggered the click via performAction(ACTION_CLICK) or
1129         // by injecting KEYCODE_DPAD_CENTER, we ignore it. Otherwise, we assume the user
1130         // touched the screen. In this case, we update mLastTouchedNode, and clear the focus
1131         // if the user touched a view in a different window.
1132         // To decide whether the click was triggered by us, we can compare the source node
1133         // in the event with mIgnoreViewClickedNode. If they're equal, the click was
1134         // triggered by us. But there is a corner case. If a dialog shows up after we
1135         // clicked the view, the window containing the view will be removed. We still
1136         // receive click event (TYPE_VIEW_CLICKED) but the source node in the event will be
1137         // null.
1138         // Note: there is no way to tell whether the window is removed in click event
1139         // because window remove event (TYPE_WINDOWS_CHANGED with type
1140         // WINDOWS_CHANGE_REMOVED) comes AFTER click event.
1141         if (mIgnoreViewClickedNode != null
1142                 && event.getEventTime() < mLastViewClickedTime + mIgnoreViewClickedMs
1143                 && ((sourceNode == null) || mIgnoreViewClickedNode.equals(sourceNode))) {
1144             setIgnoreViewClickedNode(null);
1145             return;
1146         }
1147 
1148         // When a view is clicked causing a new window to show up, the window containing the clicked
1149         // view will be removed. We still receive TYPE_VIEW_CLICKED event, but the source node can
1150         // be null. In that case we need to set mFocusedNode to null.
1151         if (sourceNode == null) {
1152             if (mFocusedNode != null) {
1153                 setFocusedNode(null);
1154             }
1155             return;
1156         }
1157 
1158         // Update mLastTouchedNode if the clicked view can take focus. If a view can't take focus,
1159         // performing focus action on it or calling focusSearch() on it will fail.
1160         if (!sourceNode.equals(mLastTouchedNode) && Utils.canTakeFocus(sourceNode)) {
1161             setLastTouchedNode(sourceNode);
1162         }
1163     }
1164 
1165     /** Handles {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event. */
handleViewScrolledEvent(@ullable AccessibilityNodeInfo sourceNode)1166     private void handleViewScrolledEvent(@Nullable AccessibilityNodeInfo sourceNode) {
1167         if (mAfterScrollAction == AfterScrollAction.NONE
1168                 || SystemClock.uptimeMillis() >= mAfterScrollActionUntil) {
1169             return;
1170         }
1171         if (sourceNode == null || !Utils.isScrollableContainer(sourceNode)) {
1172             return;
1173         }
1174         switch (mAfterScrollAction) {
1175             case FOCUS_PREVIOUS:
1176             case FOCUS_NEXT: {
1177                 if (mFocusedNode.equals(sourceNode)) {
1178                     break;
1179                 }
1180                 AccessibilityNodeInfo target = mNavigator.findFocusableDescendantInDirection(
1181                         sourceNode, mFocusedNode,
1182                         mAfterScrollAction == AfterScrollAction.FOCUS_PREVIOUS
1183                                 ? View.FOCUS_BACKWARD
1184                                 : View.FOCUS_FORWARD);
1185                 if (target == null) {
1186                     break;
1187                 }
1188                 L.d("Focusing "
1189                         + (mAfterScrollAction == AfterScrollAction.FOCUS_PREVIOUS
1190                             ? "previous" : "next")
1191                         + " after scroll");
1192                 if (performFocusAction(target)) {
1193                     mAfterScrollAction = AfterScrollAction.NONE;
1194                 }
1195                 Utils.recycleNode(target);
1196                 break;
1197             }
1198             case FOCUS_FIRST:
1199             case FOCUS_LAST: {
1200                 AccessibilityNodeInfo target =
1201                         mAfterScrollAction == AfterScrollAction.FOCUS_FIRST
1202                                 ? mNavigator.findFirstFocusableDescendant(sourceNode)
1203                                 : mNavigator.findLastFocusableDescendant(sourceNode);
1204                 if (target == null) {
1205                     break;
1206                 }
1207                 L.d("Focusing "
1208                         + (mAfterScrollAction == AfterScrollAction.FOCUS_FIRST ? "first" : "last")
1209                         + " after scroll");
1210                 if (performFocusAction(target)) {
1211                     mAfterScrollAction = AfterScrollAction.NONE;
1212                 }
1213                 Utils.recycleNode(target);
1214                 break;
1215             }
1216             default:
1217                 throw new IllegalStateException(
1218                         "Unknown after scroll action: " + mAfterScrollAction);
1219         }
1220     }
1221 
1222     /**
1223      * Handles a {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} event indicating that a window was
1224      * removed. Attempts to restore the most recent focus when the window containing
1225      * {@link #mFocusedNode} is not an application window and it's removed.
1226      */
handleWindowRemovedEvent(@onNull AccessibilityEvent event)1227     private void handleWindowRemovedEvent(@NonNull AccessibilityEvent event) {
1228         int windowId = event.getWindowId();
1229         // Get the window type. The window was removed, so we can only get it from the cache.
1230         Integer type = mWindowCache.getWindowType(windowId);
1231         if (type != null) {
1232             mWindowCache.remove(windowId);
1233             // No longer need to keep track of the node being edited if the IME window was closed.
1234             if (type == TYPE_INPUT_METHOD) {
1235                 setEditNode(null);
1236             }
1237             // No need to restore the focus if it's an application window. When an application
1238             // window is removed, another window will gain focus shortly and the FocusParkingView
1239             // in that window will restore the focus.
1240             if (type == TYPE_APPLICATION) {
1241                 return;
1242             }
1243         } else {
1244             L.w("No window type found in cache for window ID: " + windowId);
1245         }
1246 
1247         // Nothing more to do if we're in touch mode.
1248         if (!mInRotaryMode) {
1249             return;
1250         }
1251 
1252         // We only care about this event when the window that was removed contains the focused node.
1253         // Ignore other events.
1254         if (mFocusedNode == null || mFocusedNode.getWindowId() != windowId) {
1255             return;
1256         }
1257 
1258         // Restore focus to the last focused node in the last focused window.
1259         AccessibilityNodeInfo recentFocus = mWindowCache.getMostRecentFocusedNode();
1260         if (recentFocus != null) {
1261             performFocusAction(recentFocus);
1262             recentFocus.recycle();
1263         }
1264     }
1265 
1266     /**
1267      * Handles a {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} event indicating that a window was
1268      * added. Moves focus to the IME window when it appears.
1269      */
handleWindowAddedEvent(@onNull AccessibilityEvent event)1270     private void handleWindowAddedEvent(@NonNull AccessibilityEvent event) {
1271         // Save the window type by window ID.
1272         int windowId = event.getWindowId();
1273         List<AccessibilityWindowInfo> windows = getWindows();
1274         AccessibilityWindowInfo window = Utils.findWindowWithId(windows, windowId);
1275         if (window == null) {
1276             Utils.recycleWindows(windows);
1277             return;
1278         }
1279         mWindowCache.saveWindowType(windowId, window.getType());
1280 
1281         // Nothing more to do if we're in touch mode.
1282         if (!mInRotaryMode) {
1283             Utils.recycleWindows(windows);
1284             return;
1285         }
1286 
1287         // We only care about this event when the window that was added doesn't contains the focused
1288         // node. Ignore other events.
1289         if (mFocusedNode != null && mFocusedNode.getWindowId() == windowId) {
1290             Utils.recycleWindows(windows);
1291             return;
1292         }
1293 
1294         // No need to move focus for non-IME window here, because in most cases Android will focus
1295         // the FocusParkingView in the added window, and we'll move focus when handling it.
1296         if (window.getType() != TYPE_INPUT_METHOD) {
1297             Utils.recycleWindows(windows);
1298             return;
1299         }
1300 
1301         // If the new window is an IME, move focus to the IME.
1302         AccessibilityNodeInfo root = window.getRoot();
1303         if (root == null) {
1304             L.w("No root node in " + window);
1305             Utils.recycleWindows(windows);
1306             return;
1307         }
1308         Utils.recycleWindows(windows);
1309 
1310         // If the focused node is editable, save it so that we can return to it when the user
1311         // nudges out of the IME.
1312         if (mFocusedNode != null && mFocusedNode.isEditable()) {
1313             setEditNode(mFocusedNode);
1314         }
1315 
1316         boolean success = restoreDefaultFocusInRoot(root);
1317         if (!success) {
1318             L.d("Failed to restore default focus in " + root);
1319         }
1320         root.recycle();
1321     }
1322 
restoreDefaultFocusInRoot(@onNull AccessibilityNodeInfo root)1323     private boolean restoreDefaultFocusInRoot(@NonNull AccessibilityNodeInfo root) {
1324         AccessibilityNodeInfo fpv = mNavigator.findFocusParkingViewInRoot(root);
1325         // Refresh the node to ensure the focused state is up to date. The node came directly from
1326         // the node tree but it could have been cached by the accessibility framework.
1327         fpv = Utils.refreshNode(fpv);
1328 
1329         if (fpv == null) {
1330             L.e("No FocusParkingView in root " + root);
1331         } else if (Utils.isCarUiFocusParkingView(fpv)
1332                     && fpv.performAction(ACTION_RESTORE_DEFAULT_FOCUS)) {
1333             L.d("Restored focus successfully in root " + root);
1334             fpv.recycle();
1335             updateFocusedNodeAfterPerformingFocusAction(root);
1336             return true;
1337         }
1338         Utils.recycleNode(fpv);
1339 
1340         AccessibilityNodeInfo firstFocusable = mNavigator.findFirstFocusableDescendant(root);
1341         if (firstFocusable == null) {
1342             L.e("No focusable element in the window containing the generic FocusParkingView");
1343             return false;
1344         }
1345         boolean success = performFocusAction(firstFocusable);
1346         firstFocusable.recycle();
1347         return success;
1348     }
1349 
getKeyCode(KeyEvent event)1350     private static int getKeyCode(KeyEvent event) {
1351         int keyCode = event.getKeyCode();
1352         if (Build.IS_DEBUGGABLE) {
1353             Integer mappingKeyCode = TEST_TO_REAL_KEYCODE_MAP.get(keyCode);
1354             if (mappingKeyCode != null) {
1355                 keyCode = mappingKeyCode;
1356             }
1357         }
1358         return keyCode;
1359     }
1360 
1361     /** Handles controller center button event. */
handleCenterButtonEvent(int action)1362     private void handleCenterButtonEvent(int action) {
1363         if (!isValidAction(action)) {
1364             return;
1365         }
1366         if (initFocus()) {
1367             return;
1368         }
1369         // Case 1: the focused node supports rotate directly. We should ignore ACTION_DOWN event,
1370         // and enter direct manipulation mode on ACTION_UP event.
1371         if (DirectManipulationHelper.supportRotateDirectly(mFocusedNode)) {
1372             if (action == ACTION_DOWN) {
1373                 return;
1374             }
1375             if (!mInDirectManipulationMode) {
1376                 mInDirectManipulationMode = true;
1377                 boolean result = mFocusedNode.performAction(ACTION_SELECT);
1378                 if (!result) {
1379                     L.w("Failed to perform ACTION_SELECT on " + mFocusedNode);
1380                 }
1381                 L.d("Enter direct manipulation mode because focused node is clicked.");
1382             }
1383             return;
1384         }
1385 
1386         // Case 2: the focused node doesn't support rotate directly, it's in application window,
1387         // and it's not in the host app.
1388         // We should inject KEYCODE_DPAD_CENTER event (or KEYCODE_ENTER/KEYCODE_SPACE in a WebView),
1389         // then the application will handle the injected event.
1390         if (isInApplicationWindow(mFocusedNode) && !mNavigator.isHostNode(mFocusedNode)) {
1391             L.d("Inject KeyEvent in application window");
1392             int keyCode = KeyEvent.KEYCODE_DPAD_CENTER;
1393             if (mNavigator.isInWebView(mFocusedNode)) {
1394                 keyCode = mFocusedNode.isCheckable()
1395                     ? KeyEvent.KEYCODE_SPACE
1396                     : KeyEvent.KEYCODE_ENTER;
1397             }
1398             injectKeyEvent(keyCode, action);
1399             setIgnoreViewClickedNode(mFocusedNode);
1400             return;
1401         }
1402 
1403         // Case 3: the focused node doesn't support rotate directly, it's in system window or in
1404         // the host app.
1405         // We start a timer on the ACTION_DOWN event. If the ACTION_UP event occurs before the
1406         // timeout, we perform ACTION_CLICK on the focused node and abort the timer. If the timer
1407         // times out before the ACTION_UP event, handleCenterButtonLongPressEvent() will perform
1408         // ACTION_LONG_CLICK on the focused node and we'll ignore the subsequent ACTION_UP event.
1409         if (action == ACTION_DOWN) {
1410             mLongPressTriggered = false;
1411             mHandler.removeMessages(MSG_LONG_PRESS);
1412             mHandler.sendEmptyMessageDelayed(MSG_LONG_PRESS, mLongPressMs);
1413             return;
1414         }
1415         if (mLongPressTriggered) {
1416             mLongPressTriggered = false;
1417             return;
1418         }
1419         mHandler.removeMessages(MSG_LONG_PRESS);
1420         boolean success = mFocusedNode.performAction(ACTION_CLICK);
1421         L.d((success ? "Succeeded in performing" : "Failed to perform")
1422                 + " ACTION_CLICK on " + mFocusedNode);
1423         setIgnoreViewClickedNode(mFocusedNode);
1424     }
1425 
1426     /** Handles controller center button long-press events. */
handleCenterButtonLongPressEvent()1427     private void handleCenterButtonLongPressEvent() {
1428         mLongPressTriggered = true;
1429         if (initFocus()) {
1430             return;
1431         }
1432         boolean success = mFocusedNode.performAction(ACTION_LONG_CLICK);
1433         L.d((success ? "Succeeded in performing" : "Failed to perform")
1434                 + " ACTION_LONG_CLICK on " + mFocusedNode);
1435     }
1436 
handleNudgeEvent(@iew.FocusRealDirection int direction, int action)1437     private void handleNudgeEvent(@View.FocusRealDirection int direction, int action) {
1438         if (!isValidAction(action)) {
1439             return;
1440         }
1441 
1442         // If the focused node is in direct manipulation mode, manipulate it directly.
1443         if (mInDirectManipulationMode) {
1444             if (DirectManipulationHelper.supportRotateDirectly(mFocusedNode)) {
1445                 L.d("Ignore nudge events because we're in DM mode and the focused node only "
1446                         + "supports rotate directly");
1447             } else {
1448                 injectKeyEventForDirection(direction, action);
1449             }
1450             return;
1451         }
1452 
1453         // We're done with ACTION_UP event.
1454         if (action == ACTION_UP) {
1455             return;
1456         }
1457 
1458         List<AccessibilityWindowInfo> windows = getWindows();
1459 
1460         // Don't call initFocus() when handling ACTION_UP nudge events as this event will typically
1461         // arrive before the TYPE_VIEW_FOCUSED event when we delegate focusing to a FocusArea, and
1462         // will cause us to focus a nearby view when we discover that mFocusedNode is no longer
1463         // focused.
1464         if (initFocus(windows, direction)) {
1465             Utils.recycleWindows(windows);
1466             return;
1467         }
1468 
1469         // If the HUN is currently focused, we should only handle nudge events that are in the
1470         // opposite direction of the HUN nudge direction.
1471         if (mNavigator.isHunWindow(mFocusedNode.getWindow())
1472                 && direction != mHunEscapeNudgeDirection) {
1473             Utils.recycleWindows(windows);
1474             return;
1475         }
1476 
1477         // If the focused node is not in direct manipulation mode, try to move the focus to another
1478         // node.
1479         nudgeTo(windows, direction);
1480         Utils.recycleWindows(windows);
1481     }
1482 
1483     @VisibleForTesting
nudgeTo(@onNull List<AccessibilityWindowInfo> windows, @View.FocusRealDirection int direction)1484     void nudgeTo(@NonNull List<AccessibilityWindowInfo> windows,
1485             @View.FocusRealDirection int direction) {
1486         // If the HUN is in the nudge direction, nudge to it.
1487         boolean hunFocusResult = focusHunsWindow(windows, direction);
1488         if (hunFocusResult) {
1489             L.d("Nudge to HUN successful");
1490             return;
1491         }
1492 
1493         // Try to move the focus to the shortcut node.
1494         if (mFocusArea == null) {
1495             L.e("mFocusArea shouldn't be null");
1496             return;
1497         }
1498         Bundle arguments = new Bundle();
1499         arguments.putInt(NUDGE_DIRECTION, direction);
1500         if (mFocusArea.performAction(ACTION_NUDGE_SHORTCUT, arguments)) {
1501             L.d("Nudge to shortcut view");
1502             AccessibilityNodeInfo root = mNavigator.getRoot(mFocusArea);
1503             if (root != null) {
1504                 updateFocusedNodeAfterPerformingFocusAction(root);
1505                 root.recycle();
1506             }
1507             return;
1508         }
1509 
1510         // No shortcut node, so move the focus in the given direction.
1511         // First, try to perform ACTION_NUDGE on mFocusArea to nudge to another FocusArea.
1512         arguments.clear();
1513         arguments.putInt(NUDGE_DIRECTION, direction);
1514         if (mFocusArea.performAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments)) {
1515             L.d("Nudge to user specified FocusArea");
1516             AccessibilityNodeInfo root = mNavigator.getRoot(mFocusArea);
1517             if (root != null) {
1518                 updateFocusedNodeAfterPerformingFocusAction(root);
1519                 root.recycle();
1520             }
1521             return;
1522         }
1523 
1524         // No specified FocusArea or cached FocusArea in the direction, so mFocusArea doesn't know
1525         // what FocusArea to nudge to. In this case, we'll find a target FocusArea using geometry.
1526         AccessibilityNodeInfo targetFocusArea =
1527                 mNavigator.findNudgeTargetFocusArea(windows, mFocusedNode, mFocusArea, direction);
1528 
1529         // If the user is nudging off the edge of the screen, execute the app-specific or app-
1530         // agnostic off-screen nudge action, if either are specified. The former take precedence
1531         // over the latter.
1532         if (targetFocusArea == null) {
1533             if (handleAppSpecificOffScreenNudge(direction)) {
1534                 return;
1535             }
1536             if (handleAppAgnosticOffScreenNudge(direction)) {
1537                 return;
1538             }
1539             L.d("Off-screen nudge ignored");
1540             return;
1541         }
1542 
1543         // If the user is nudging out of the IME, set mFocusedNode to the node being edited (which
1544         // should already be focused) and hide the IME.
1545         if (mEditNode != null && mFocusArea.getWindowId() != targetFocusArea.getWindowId()) {
1546             AccessibilityWindowInfo fromWindow = mFocusArea.getWindow();
1547             if (fromWindow != null && fromWindow.getType() == TYPE_INPUT_METHOD) {
1548                 setFocusedNode(mEditNode);
1549                 L.d("Returned to node being edited");
1550                 // Ask the FocusParkingView to hide the IME.
1551                 AccessibilityNodeInfo fpv = mNavigator.findFocusParkingView(mEditNode);
1552                 if (fpv != null) {
1553                     if (!fpv.performAction(ACTION_HIDE_IME)) {
1554                         L.w("Failed to close IME");
1555                     }
1556                     fpv.recycle();
1557                 }
1558                 setEditNode(null);
1559                 Utils.recycleWindow(fromWindow);
1560                 targetFocusArea.recycle();
1561                 return;
1562             }
1563             Utils.recycleWindow(fromWindow);
1564         }
1565 
1566         // targetFocusArea is an explicit FocusArea (i.e., an instance of the FocusArea class), so
1567         // perform ACTION_FOCUS on it. The FocusArea will handle this by focusing one of its
1568         // descendants.
1569         if (Utils.isFocusArea(targetFocusArea)) {
1570             arguments.clear();
1571             arguments.putInt(NUDGE_DIRECTION, direction);
1572             boolean success = performFocusAction(targetFocusArea, arguments);
1573             L.d("Nudging to the nearest FocusArea "
1574                     + (success ? "succeeded" : "failed: " + targetFocusArea));
1575             targetFocusArea.recycle();
1576             return;
1577         }
1578 
1579         // targetFocusArea is an implicit FocusArea (i.e., the root node of a window without any
1580         // FocusAreas), so restore the focus in it.
1581         boolean success = restoreDefaultFocusInRoot(targetFocusArea);
1582         L.d("Nudging to the nearest implicit focus area "
1583                 + (success ? "succeeded" : "failed: " + targetFocusArea));
1584         targetFocusArea.recycle();
1585     }
1586 
1587     /**
1588      * Executes the app-specific custom nudge action for the given {@code direction} specified in
1589      * {@link #mForegroundActivity}'s metadata, if any, by: <ul>
1590      *     <li>performing the specified global action,
1591      *     <li>injecting {@code ACTION_DOWN} and {@code ACTION_UP} events with the
1592      *         specified key code, or
1593      *     <li>starting an activity with the specified intent.
1594      * </ul>
1595      * Returns whether a custom nudge action was performed.
1596      */
handleAppSpecificOffScreenNudge(@iew.FocusRealDirection int direction)1597     private boolean handleAppSpecificOffScreenNudge(@View.FocusRealDirection int direction) {
1598         Bundle metaData = getForegroundActivityMetaData();
1599         if (metaData == null) {
1600             L.w("Failed to get metadata for " + mForegroundActivity);
1601             return false;
1602         }
1603         String directionString = DIRECTION_TO_STRING.get(direction);
1604         int globalAction = metaData.getInt(
1605                 String.format(OFF_SCREEN_NUDGE_GLOBAL_ACTION_FORMAT, directionString),
1606                 INVALID_GLOBAL_ACTION);
1607         if (globalAction != INVALID_GLOBAL_ACTION) {
1608             L.d("App-specific off-screen nudge: " + globalActionToString(globalAction));
1609             performGlobalAction(globalAction);
1610             return true;
1611         }
1612         int keyCode = metaData.getInt(
1613                 String.format(OFF_SCREEN_NUDGE_KEY_CODE_FORMAT, directionString), KEYCODE_UNKNOWN);
1614         if (keyCode != KEYCODE_UNKNOWN) {
1615             L.d("App-specific off-screen nudge: " + KeyEvent.keyCodeToString(keyCode));
1616             injectKeyEvent(keyCode, ACTION_DOWN);
1617             injectKeyEvent(keyCode, ACTION_UP);
1618             return true;
1619         }
1620         String intentString = metaData.getString(
1621                 String.format(OFF_SCREEN_NUDGE_INTENT_FORMAT, directionString), null);
1622         if (intentString == null) {
1623             return false;
1624         }
1625         Intent intent;
1626         try {
1627             intent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME);
1628         } catch (URISyntaxException e) {
1629             L.w("Failed to parse app-specific off-screen nudge intent: " + intentString);
1630             return false;
1631         }
1632         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1633         List<ResolveInfo> activities =
1634                 getPackageManager().queryIntentActivities(intent, /* flags= */ 0);
1635         if (activities.isEmpty()) {
1636             L.w("No activities for app-specific off-screen nudge: " + intent);
1637             return false;
1638         }
1639         L.d("App-specific off-screen nudge: " + intent);
1640         startActivity(intent);
1641         return true;
1642     }
1643 
1644     /**
1645      * Executes the app-agnostic custom nudge action for the given {@code direction}, if any. This
1646      * method is equivalent to {@link #handleAppSpecificOffScreenNudge} but for global actions
1647      * rather than app-specific ones.
1648      */
handleAppAgnosticOffScreenNudge(@iew.FocusRealDirection int direction)1649     private boolean handleAppAgnosticOffScreenNudge(@View.FocusRealDirection int direction) {
1650         int directionIndex = DIRECTION_TO_INDEX.get(direction);
1651         int globalAction = mOffScreenNudgeGlobalActions[directionIndex];
1652         if (globalAction != INVALID_GLOBAL_ACTION) {
1653             L.d("App-agnostic off-screen nudge: " + globalActionToString(globalAction));
1654             performGlobalAction(globalAction);
1655             return true;
1656         }
1657         int keyCode = mOffScreenNudgeKeyCodes[directionIndex];
1658         if (keyCode != KEYCODE_UNKNOWN) {
1659             L.d("App-agnostic off-screen nudge: " + KeyEvent.keyCodeToString(keyCode));
1660             injectKeyEvent(keyCode, ACTION_DOWN);
1661             injectKeyEvent(keyCode, ACTION_UP);
1662             return true;
1663         }
1664         Intent intent = mOffScreenNudgeIntents[directionIndex];
1665         if (intent == null) {
1666             return false;
1667         }
1668         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1669         PackageManager packageManager = getPackageManager();
1670         List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, /* flags= */ 0);
1671         if (activities.isEmpty()) {
1672             L.w("No activities for app-agnostic off-screen nudge: " + intent);
1673             return false;
1674         }
1675         L.d("App-agnostic off-screen nudge: " + intent);
1676         startActivity(intent);
1677         return true;
1678     }
1679 
1680     @Nullable
getForegroundActivityMetaData()1681     private Bundle getForegroundActivityMetaData() {
1682         // The foreground activity can be null in a cold boot when the user has an active
1683         // lockscreen.
1684         if (mForegroundActivity == null) {
1685             return null;
1686         }
1687 
1688         try {
1689             ActivityInfo activityInfo = getPackageManager().getActivityInfo(mForegroundActivity,
1690                     PackageManager.GET_META_DATA);
1691             return activityInfo.metaData;
1692         } catch (PackageManager.NameNotFoundException e) {
1693             return null;
1694         }
1695     }
1696 
1697     @NonNull
globalActionToString(int globalAction)1698     private static String globalActionToString(int globalAction) {
1699         switch (globalAction) {
1700             case GLOBAL_ACTION_BACK:
1701                 return "GLOBAL_ACTION_BACK";
1702             case GLOBAL_ACTION_HOME:
1703                 return "GLOBAL_ACTION_HOME";
1704             case GLOBAL_ACTION_NOTIFICATIONS:
1705                 return "GLOBAL_ACTION_NOTIFICATIONS";
1706             case GLOBAL_ACTION_QUICK_SETTINGS:
1707                 return "GLOBAL_ACTION_QUICK_SETTINGS";
1708             default:
1709                 return String.format("global action %d", globalAction);
1710         }
1711     }
1712 
handleRotaryEvent(RotaryEvent rotaryEvent)1713     private void handleRotaryEvent(RotaryEvent rotaryEvent) {
1714         if (rotaryEvent.getInputType() != CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION) {
1715             return;
1716         }
1717         boolean clockwise = rotaryEvent.isClockwise();
1718         int count = rotaryEvent.getNumberOfClicks();
1719         // TODO(b/153195148): Use the last eventTime for now. We'll need to improve it later.
1720         long eventTime = rotaryEvent.getUptimeMillisForClick(count - 1);
1721         handleRotateEvent(clockwise, count, eventTime);
1722     }
1723 
handleRotateEvent(boolean clockwise, int count, long eventTime)1724     private void handleRotateEvent(boolean clockwise, int count, long eventTime) {
1725         if (initFocus()) {
1726             return;
1727         }
1728 
1729         int rotationCount = getRotateAcceleration(count, eventTime);
1730 
1731         // If the focused node is in direct manipulation mode, manipulate it directly.
1732         if (mInDirectManipulationMode) {
1733             if (DirectManipulationHelper.supportRotateDirectly(mFocusedNode)) {
1734                 performScrollAction(mFocusedNode, clockwise);
1735             } else {
1736                 AccessibilityWindowInfo window = mFocusedNode.getWindow();
1737                 if (window == null) {
1738                     L.w("Failed to get window of " + mFocusedNode);
1739                     return;
1740                 }
1741                 int displayId = window.getDisplayId();
1742                 window.recycle();
1743                 // TODO(b/155823126): Add config to let OEMs determine the mapping.
1744                 injectMotionEvent(displayId, MotionEvent.AXIS_SCROLL,
1745                         clockwise ? rotationCount : -rotationCount);
1746             }
1747             return;
1748         }
1749 
1750         // If the focused node is not in direct manipulation mode, move the focus.
1751         int remainingRotationCount = rotationCount;
1752         int direction = clockwise ? View.FOCUS_FORWARD : View.FOCUS_BACKWARD;
1753         Navigator.FindRotateTargetResult result =
1754                 mNavigator.findRotateTarget(mFocusedNode, direction, rotationCount);
1755         if (result != null) {
1756             if (performFocusAction(result.node)) {
1757                 remainingRotationCount -= result.advancedCount;
1758             }
1759             Utils.recycleNode(result.node);
1760         } else {
1761             L.w("Failed to find rotate target from " + mFocusedNode);
1762         }
1763 
1764         // If navigation didn't consume all of rotationCount and the focused node either is a
1765         // scrollable container or is a descendant of one, scroll it. The former happens when no
1766         // focusable views are visible in the scrollable container. The latter happens when there
1767         // are focusable views but they're in the wrong direction. Inject a MotionEvent rather than
1768         // performing an action so that the application can control the amount it scrolls. Scrolling
1769         // is only supported in the application window because injected events always go to the
1770         // application window. We don't bother checking whether the scrollable container can
1771         // currently scroll because there's nothing else to do if it can't.
1772         if (remainingRotationCount > 0 && isInApplicationWindow(mFocusedNode)) {
1773             AccessibilityNodeInfo scrollableContainer =
1774                     mNavigator.findScrollableContainer(mFocusedNode);
1775             if (scrollableContainer != null) {
1776                 injectScrollEvent(scrollableContainer, clockwise, remainingRotationCount);
1777                 scrollableContainer.recycle();
1778             }
1779         }
1780     }
1781 
1782     /** Handles Back button event. */
handleBackButtonEvent(int action)1783     private void handleBackButtonEvent(int action) {
1784         if (!isValidAction(action)) {
1785             return;
1786         }
1787         // If the focused node doesn't support rotate directly, inject Back button event, then the
1788         // application will handle the injected event.
1789         if (!DirectManipulationHelper.supportRotateDirectly(mFocusedNode)) {
1790             injectKeyEvent(KeyEvent.KEYCODE_BACK, action);
1791             return;
1792         }
1793 
1794         // Otherwise exit direct manipulation mode on ACTION_UP event.
1795         if (action == ACTION_DOWN) {
1796             return;
1797         }
1798         L.d("Exit direct manipulation mode on back button event");
1799         mInDirectManipulationMode = false;
1800         boolean result = mFocusedNode.performAction(ACTION_CLEAR_SELECTION);
1801         if (!result) {
1802             L.w("Failed to perform ACTION_CLEAR_SELECTION on " + mFocusedNode);
1803         }
1804     }
1805 
onForegroundActivityChanged(@onNull AccessibilityNodeInfo root, CharSequence packageName, CharSequence className)1806     private void onForegroundActivityChanged(@NonNull AccessibilityNodeInfo root,
1807             CharSequence packageName, CharSequence className) {
1808         // If the foreground app is a client app, store its package name.
1809         AccessibilityNodeInfo surfaceView = mNavigator.findSurfaceViewInRoot(root);
1810         if (surfaceView != null) {
1811             mNavigator.addClientApp(surfaceView.getPackageName());
1812             surfaceView.recycle();
1813         }
1814 
1815         ComponentName newActivity = new ComponentName(packageName.toString(), className.toString());
1816         if (newActivity.equals(mForegroundActivity)) {
1817             return;
1818         }
1819         mForegroundActivity = newActivity;
1820         if (mInDirectManipulationMode) {
1821             L.d("Exit direct manipulation mode because the foreground app has changed");
1822             mInDirectManipulationMode = false;
1823         }
1824     }
1825 
isValidAction(int action)1826     private static boolean isValidAction(int action) {
1827         if (action != ACTION_DOWN && action != ACTION_UP) {
1828             L.w("Invalid action " + action);
1829             return false;
1830         }
1831         return true;
1832     }
1833 
1834     /** Performs scroll action on the given {@code targetNode} if it supports scroll action. */
performScrollAction(@onNull AccessibilityNodeInfo targetNode, boolean clockwise)1835     private static void performScrollAction(@NonNull AccessibilityNodeInfo targetNode,
1836             boolean clockwise) {
1837         // TODO(b/155823126): Add config to let OEMs determine the mapping.
1838         AccessibilityNodeInfo.AccessibilityAction actionToPerform =
1839                 clockwise ? ACTION_SCROLL_FORWARD : ACTION_SCROLL_BACKWARD;
1840         if (!targetNode.getActionList().contains(actionToPerform)) {
1841             L.w("Node " + targetNode + " doesn't support action " + actionToPerform);
1842             return;
1843         }
1844         boolean result = targetNode.performAction(actionToPerform.getId());
1845         if (!result) {
1846             L.w("Failed to perform action " + actionToPerform + " on " + targetNode);
1847         }
1848     }
1849 
1850     /** Returns whether the given {@code node} is in the application window. */
1851     @VisibleForTesting
isInApplicationWindow(@onNull AccessibilityNodeInfo node)1852     boolean isInApplicationWindow(@NonNull AccessibilityNodeInfo node) {
1853         AccessibilityWindowInfo window = node.getWindow();
1854         if (window == null) {
1855             L.w("Failed to get window of " + node);
1856             return false;
1857         }
1858         boolean result = window.getType() == TYPE_APPLICATION;
1859         Utils.recycleWindow(window);
1860         return result;
1861     }
1862 
updateDirectManipulationMode(@onNull AccessibilityEvent event, boolean enable)1863     private void updateDirectManipulationMode(@NonNull AccessibilityEvent event, boolean enable) {
1864         if (!mInRotaryMode || !DirectManipulationHelper.isDirectManipulation(event)) {
1865             return;
1866         }
1867         if (enable) {
1868             mFocusedNode = Utils.refreshNode(mFocusedNode);
1869             if (mFocusedNode == null) {
1870                 L.w("Failed to enter direct manipulation mode because mFocusedNode is no longer "
1871                         + "in view tree.");
1872                 return;
1873             }
1874             if (!Utils.hasFocus(mFocusedNode)) {
1875                 L.w("Failed to enter direct manipulation mode because mFocusedNode no longer "
1876                         + "has focus.");
1877                 return;
1878             }
1879         }
1880         if (mInDirectManipulationMode != enable) {
1881             // Toggle direct manipulation mode upon app's request.
1882             mInDirectManipulationMode = enable;
1883             L.d((enable ? "Enter" : "Exit") + " direct manipulation mode upon app's request");
1884         }
1885     }
1886 
1887     /**
1888      * Injects a {@link MotionEvent} to scroll {@code scrollableContainer} by {@code rotationCount}
1889      * steps. The direction depends on the value of {@code clockwise}. Sets
1890      * {@link #mAfterScrollAction} to move the focus once the scroll occurs, as follows:<ul>
1891      *     <li>If the user is spinning the rotary controller quickly, focuses the first or last
1892      *         focusable descendant so that the next rotation event will scroll immediately.
1893      *     <li>If the user is spinning slowly and there are no focusable descendants visible,
1894      *         focuses the first focusable descendant to scroll into view. This will be the last
1895      *         focusable descendant when scrolling up.
1896      *     <li>If the user is spinning slowly and there are focusable descendants visible, focuses
1897      *         the next or previous focusable descendant.
1898      * </ul>
1899      */
injectScrollEvent(@onNull AccessibilityNodeInfo scrollableContainer, boolean clockwise, int rotationCount)1900     private void injectScrollEvent(@NonNull AccessibilityNodeInfo scrollableContainer,
1901             boolean clockwise, int rotationCount) {
1902         // TODO(b/155823126): Add config to let OEMs determine the mappings.
1903         if (rotationCount > 1) {
1904             // Focus last when quickly scrolling down so the next event scrolls.
1905             mAfterScrollAction = clockwise
1906                     ? AfterScrollAction.FOCUS_LAST
1907                     : AfterScrollAction.FOCUS_FIRST;
1908         } else {
1909             if (Utils.isScrollableContainer(mFocusedNode)) {
1910                 // Focus first when scrolling down while no focusable descendants are visible.
1911                 mAfterScrollAction = clockwise
1912                         ? AfterScrollAction.FOCUS_FIRST
1913                         : AfterScrollAction.FOCUS_LAST;
1914             } else {
1915                 // Focus next when scrolling down with a focused descendant.
1916                 mAfterScrollAction = clockwise
1917                         ? AfterScrollAction.FOCUS_NEXT
1918                         : AfterScrollAction.FOCUS_PREVIOUS;
1919             }
1920         }
1921         mAfterScrollActionUntil = SystemClock.uptimeMillis() + mAfterScrollTimeoutMs;
1922         int axis = Utils.isHorizontallyScrollableContainer(scrollableContainer)
1923                 ? MotionEvent.AXIS_HSCROLL
1924                 : MotionEvent.AXIS_VSCROLL;
1925         AccessibilityWindowInfo window = scrollableContainer.getWindow();
1926         if (window == null) {
1927             L.w("Failed to get window of " + scrollableContainer);
1928             return;
1929         }
1930         int displayId = window.getDisplayId();
1931         window.recycle();
1932         injectMotionEvent(displayId, axis, clockwise ? -rotationCount : rotationCount);
1933     }
1934 
injectMotionEvent(int displayId, int axis, int axisValue)1935     private void injectMotionEvent(int displayId, int axis, int axisValue) {
1936         long upTime = SystemClock.uptimeMillis();
1937         MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[1];
1938         properties[0] = new MotionEvent.PointerProperties();
1939         properties[0].id = 0; // Any integer value but -1 (INVALID_POINTER_ID) is fine.
1940         MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[1];
1941         coords[0] = new MotionEvent.PointerCoords();
1942         // No need to set X,Y coordinates. We use a non-pointer source so the event will be routed
1943         // to the focused view.
1944         coords[0].setAxisValue(axis, axisValue);
1945         MotionEvent motionEvent = MotionEvent.obtain(/* downTime= */ upTime,
1946                 /* eventTime= */ upTime,
1947                 MotionEvent.ACTION_SCROLL,
1948                 /* pointerCount= */ 1,
1949                 properties,
1950                 coords,
1951                 /* metaState= */ 0,
1952                 /* buttonState= */ 0,
1953                 /* xPrecision= */ 1.0f,
1954                 /* yPrecision= */ 1.0f,
1955                 /* deviceId= */ 0,
1956                 /* edgeFlags= */ 0,
1957                 InputDevice.SOURCE_ROTARY_ENCODER,
1958                 displayId,
1959                 /* flags= */ 0);
1960 
1961         if (motionEvent != null) {
1962             mInputManager.injectInputEvent(motionEvent,
1963                     InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
1964         } else {
1965             L.w("Unable to obtain MotionEvent");
1966         }
1967     }
1968 
injectKeyEventForDirection(@iew.FocusRealDirection int direction, int action)1969     private void injectKeyEventForDirection(@View.FocusRealDirection int direction, int action) {
1970         Integer keyCode = DIRECTION_TO_KEYCODE_MAP.get(direction);
1971         if (keyCode == null) {
1972             throw new IllegalArgumentException("direction must be one of "
1973                     + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
1974         }
1975         injectKeyEvent(keyCode, action);
1976     }
1977 
1978     @VisibleForTesting
injectKeyEvent(int keyCode, int action)1979     void injectKeyEvent(int keyCode, int action) {
1980         long upTime = SystemClock.uptimeMillis();
1981         KeyEvent keyEvent = new KeyEvent(
1982                 /* downTime= */ upTime, /* eventTime= */ upTime, action, keyCode, /* repeat= */ 0);
1983         mInputManager.injectInputEvent(keyEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
1984     }
1985 
1986     /**
1987      * Updates saved nodes in case the {@link View}s represented by them are no longer in the view
1988      * tree.
1989      */
refreshSavedNodes()1990     private void refreshSavedNodes() {
1991         mFocusedNode = Utils.refreshNode(mFocusedNode);
1992         mEditNode = Utils.refreshNode(mEditNode);
1993         mLastTouchedNode = Utils.refreshNode(mLastTouchedNode);
1994         mFocusArea = Utils.refreshNode(mFocusArea);
1995         mIgnoreViewClickedNode = Utils.refreshNode(mIgnoreViewClickedNode);
1996     }
1997 
1998     /**
1999      * This method should be called when receiving an event from a rotary controller. It does the
2000      * following:<ol>
2001      *     <li>If {@link #mFocusedNode} isn't null and represents a view that still exists, does
2002      *         nothing. The event isn't consumed in this case. This is the normal case.
2003      *     <li>If there is a non-FocusParkingView focused in any window, set mFocusedNode to that
2004      *         view. The event isn't consumed in this case.
2005      *     <li>If {@link #mLastTouchedNode} isn't null and represents a view that still exists,
2006      *         focuses it. The event is consumed in this case. This happens when the user switches
2007      *         from touch to rotary.
2008      *     <li>Otherwise focuses the best target in the node tree and consumes the event.
2009      * </ol>
2010      *
2011      * @return whether the event was consumed by this method. When {@code false},
2012      *         {@link #mFocusedNode} is guaranteed to not be {@code null}.
2013      */
2014     @VisibleForTesting
initFocus()2015     boolean initFocus() {
2016         List<AccessibilityWindowInfo> windows = getWindows();
2017         boolean consumed = initFocus(windows, INVALID_NUDGE_DIRECTION);
2018         Utils.recycleWindows(windows);
2019         return consumed;
2020     }
2021 
2022     /**
2023      * Similar to above, but also checks for heads-up notifications if given a valid nudge direction
2024      * which may be relevant when we're trying to focus the HUNs when coming from touch mode.
2025      *
2026      * @param windows the windows currently available to the Accessibility Service
2027      * @param direction the direction of the nudge that was received (can be
2028      *                  {@link #INVALID_NUDGE_DIRECTION})
2029      * @return whether the event was consumed by this method. When {@code false},
2030      *         {@link #mFocusedNode} is guaranteed to not be {@code null}.
2031      */
initFocus(@onNull List<AccessibilityWindowInfo> windows, @View.FocusRealDirection int direction)2032     private boolean initFocus(@NonNull List<AccessibilityWindowInfo> windows,
2033             @View.FocusRealDirection int direction) {
2034         boolean prevInRotaryMode = mInRotaryMode;
2035         refreshSavedNodes();
2036         setInRotaryMode(true);
2037         if (mFocusedNode != null) {
2038             // If mFocusedNode is focused, we're in a good state and can proceed with whatever
2039             // action the user requested.
2040             if (mFocusedNode.isFocused()) {
2041                 return false;
2042             }
2043             // If the focused node represents an HTML element in a WebView, we just assume the focus
2044             // is already initialized here, and we'll handle it properly when the user uses the
2045             // controller next time.
2046             if (mNavigator.isInWebView(mFocusedNode)) {
2047                 return false;
2048             }
2049         }
2050 
2051         // If we were not in rotary mode before and we can focus the HUNs window for the given
2052         // nudge, focus the window and ensure that there is no previously touched node.
2053         if (!prevInRotaryMode && focusHunsWindow(windows, direction)) {
2054             setLastTouchedNode(null);
2055             return true;
2056         }
2057 
2058         // If there is a non-FocusParkingView focused in any window, set mFocusedNode to that view.
2059         for (AccessibilityWindowInfo window : windows) {
2060             AccessibilityNodeInfo root = window.getRoot();
2061             if (root != null) {
2062                 AccessibilityNodeInfo focusedNode = mNavigator.findFocusedNodeInRoot(root);
2063                 root.recycle();
2064                 if (focusedNode != null) {
2065                     setFocusedNode(focusedNode);
2066                     focusedNode.recycle();
2067                     return false;
2068                 }
2069             }
2070         }
2071 
2072         if (mLastTouchedNode != null && focusLastTouchedNode()) {
2073             return true;
2074         }
2075 
2076         AccessibilityNodeInfo root = getRootInActiveWindow();
2077         if (root != null) {
2078             restoreDefaultFocusInRoot(root);
2079             Utils.recycleNode(root);
2080         }
2081         return true;
2082     }
2083 
2084     /**
2085      * Clears the current rotary focus if {@code targetFocus} is null, or in a different window
2086      * unless focus is moving from an editable field to the IME.
2087      * <p>
2088      * Note: only {@link #setFocusedNode} can call this method, otherwise {@link #mFocusedNode}
2089      * might go out of sync.
2090      */
maybeClearFocusInCurrentWindow(@ullable AccessibilityNodeInfo targetFocus)2091     private void maybeClearFocusInCurrentWindow(@Nullable AccessibilityNodeInfo targetFocus) {
2092         mFocusedNode = Utils.refreshNode(mFocusedNode);
2093         if (mFocusedNode == null
2094                 // No need to clear focus if mFocusedNode is not focused. However, when it's a node
2095                 // in a WebView, its state might not be up to date, so mFocusedNode.isFocused()
2096                 // may return false even if the view represented by mFocusedNode is focused.
2097                 // So don't check the focused state if it's in WebView.
2098                 || (!mFocusedNode.isFocused() && !mNavigator.isInWebView(mFocusedNode))
2099                 || (targetFocus != null
2100                         && mFocusedNode.getWindowId() == targetFocus.getWindowId())) {
2101             return;
2102         }
2103 
2104         // If we're moving from an editable node to the IME, don't clear focus, but save the
2105         // editable node so that we can return to it when the user nudges out of the IME.
2106         if (mFocusedNode.isEditable() && targetFocus != null) {
2107             int targetWindowId = targetFocus.getWindowId();
2108             Integer windowType = mWindowCache.getWindowType(targetWindowId);
2109             if (windowType != null && windowType == TYPE_INPUT_METHOD) {
2110                 L.d("Leaving editable field focused");
2111                 setEditNode(mFocusedNode);
2112                 return;
2113             }
2114         }
2115 
2116         clearFocusInCurrentWindow();
2117     }
2118 
2119     /**
2120      * Clears the current rotary focus.
2121      * <p>
2122      * If we really clear focus in the current window, Android will re-focus a view in the current
2123      * window automatically, resulting in the current window and the target window being focused
2124      * simultaneously. To avoid that we don't really clear the focus. Instead, we "park" the focus
2125      * on a FocusParkingView in the current window. FocusParkingView is transparent no matter
2126      * whether it's focused or not, so it's invisible to the user.
2127      *
2128      * @return whether the FocusParkingView was focused successfully
2129      */
clearFocusInCurrentWindow()2130     private boolean clearFocusInCurrentWindow() {
2131         if (mFocusedNode == null) {
2132             L.e("Don't call clearFocusInCurrentWindow() when mFocusedNode is null");
2133             return false;
2134         }
2135         AccessibilityNodeInfo root = mNavigator.getRoot(mFocusedNode);
2136         boolean result = clearFocusInRoot(root);
2137         root.recycle();
2138         return result;
2139     }
2140 
2141     /**
2142      * Clears the rotary focus in the given {@code window}.
2143      *
2144      * @return whether the FocusParkingView was focused successfully
2145      */
clearFocusInWindow(@onNull AccessibilityWindowInfo window)2146     private boolean clearFocusInWindow(@NonNull AccessibilityWindowInfo window) {
2147         AccessibilityNodeInfo root = window.getRoot();
2148         if (root == null) {
2149             L.e("No root node in the window " + window);
2150             return false;
2151         }
2152 
2153         boolean success = clearFocusInRoot(root);
2154         root.recycle();
2155         return success;
2156     }
2157 
2158     /**
2159      * Clears the rotary focus in the node tree rooted at {@code root}.
2160      * <p>
2161      * If we really clear focus in a window, Android will re-focus a view in that window
2162      * automatically. To avoid that we don't really clear the focus. Instead, we "park" the focus on
2163      * a FocusParkingView in the given window. FocusParkingView is transparent no matter whether
2164      * it's focused or not, so it's invisible to the user.
2165      *
2166      * @return whether the FocusParkingView was focused successfully
2167      */
clearFocusInRoot(@onNull AccessibilityNodeInfo root)2168     private boolean clearFocusInRoot(@NonNull AccessibilityNodeInfo root) {
2169         AccessibilityNodeInfo fpv = mNavigator.findFocusParkingViewInRoot(root);
2170 
2171         // Refresh the node to ensure the focused state is up to date. The node came directly from
2172         // the node tree but it could have been cached by the accessibility framework.
2173         fpv = Utils.refreshNode(fpv);
2174 
2175         if (fpv == null) {
2176             L.e("No FocusParkingView in the window that contains " + root);
2177             return false;
2178         }
2179         if (fpv.isFocused()) {
2180             L.d("FocusParkingView is already focused " + fpv);
2181             fpv.recycle();
2182             return true;
2183         }
2184         boolean result = performFocusAction(fpv);
2185         if (!result) {
2186             L.w("Failed to perform ACTION_FOCUS on " + fpv);
2187         }
2188         fpv.recycle();
2189         return result;
2190     }
2191 
focusHunsWindow(@onNull List<AccessibilityWindowInfo> windows, @View.FocusRealDirection int direction)2192     private boolean focusHunsWindow(@NonNull List<AccessibilityWindowInfo> windows,
2193             @View.FocusRealDirection int direction) {
2194         if (direction != mHunNudgeDirection) {
2195             return false;
2196         }
2197 
2198         AccessibilityWindowInfo hunWindow = mNavigator.findHunWindow(windows);
2199         if (hunWindow == null) {
2200             L.d("No HUN window to focus");
2201             return false;
2202         }
2203 
2204         AccessibilityNodeInfo hunRoot = hunWindow.getRoot();
2205         if (hunRoot == null) {
2206             L.d("No root in HUN Window to focus");
2207             return false;
2208         }
2209 
2210         boolean success = restoreDefaultFocusInRoot(hunRoot);
2211         hunRoot.recycle();
2212         L.d("HUN window focus " + (success ? "successful" : "failed"));
2213         return success;
2214     }
2215 
2216     /**
2217      * Focuses the last touched node, if any.
2218      *
2219      * @return {@code true} if {@link #mLastTouchedNode} isn't {@code null} and it was
2220      *         successfully focused
2221      */
focusLastTouchedNode()2222     private boolean focusLastTouchedNode() {
2223         boolean lastTouchedNodeFocused = false;
2224         if (mLastTouchedNode != null) {
2225             lastTouchedNodeFocused = performFocusAction(mLastTouchedNode);
2226             if (mLastTouchedNode != null) {
2227                 setLastTouchedNode(null);
2228             }
2229         }
2230         return lastTouchedNodeFocused;
2231     }
2232 
2233     /**
2234      * Sets {@link #mFocusedNode} to a copy of the given node, and clears {@link #mLastTouchedNode}.
2235      */
2236     @VisibleForTesting
setFocusedNode(@ullable AccessibilityNodeInfo focusedNode)2237     void setFocusedNode(@Nullable AccessibilityNodeInfo focusedNode) {
2238         // Android doesn't clear focus automatically when focus is set in another window, so we need
2239         // to do it explicitly.
2240         maybeClearFocusInCurrentWindow(focusedNode);
2241 
2242         setFocusedNodeInternal(focusedNode);
2243         if (mFocusedNode != null && mLastTouchedNode != null) {
2244             setLastTouchedNodeInternal(null);
2245         }
2246     }
2247 
setFocusedNodeInternal(@ullable AccessibilityNodeInfo focusedNode)2248     private void setFocusedNodeInternal(@Nullable AccessibilityNodeInfo focusedNode) {
2249         if ((mFocusedNode == null && focusedNode == null) ||
2250                 (mFocusedNode != null && mFocusedNode.equals(focusedNode))) {
2251             L.d("Don't reset mFocusedNode since it stays the same: " + mFocusedNode);
2252             return;
2253         }
2254         if (mInDirectManipulationMode && focusedNode == null) {
2255             // Toggle off direct manipulation mode since there is no focused node.
2256             mInDirectManipulationMode = false;
2257             L.d("Exit direct manipulation mode since there is no focused node");
2258         }
2259 
2260         // Close the IME when navigating from an editable view to a non-editable view.
2261         maybeCloseIme(focusedNode);
2262 
2263         Utils.recycleNode(mFocusedNode);
2264         mFocusedNode = copyNode(focusedNode);
2265         L.d("mFocusedNode set to: " + mFocusedNode);
2266 
2267         Utils.recycleNode(mFocusArea);
2268         mFocusArea = mFocusedNode == null ? null : mNavigator.getAncestorFocusArea(mFocusedNode);
2269 
2270         if (mFocusedNode != null) {
2271             mWindowCache.saveFocusedNode(mFocusedNode.getWindowId(), mFocusedNode);
2272         }
2273     }
2274 
refreshPendingFocusedNode()2275     private void refreshPendingFocusedNode() {
2276         if (mPendingFocusedNode != null) {
2277             if (SystemClock.uptimeMillis() > mPendingFocusedExpirationTime) {
2278                 setPendingFocusedNode(null);
2279             } else {
2280                 mPendingFocusedNode = Utils.refreshNode(mPendingFocusedNode);
2281             }
2282         }
2283     }
2284 
setPendingFocusedNode(@ullable AccessibilityNodeInfo node)2285     private void setPendingFocusedNode(@Nullable AccessibilityNodeInfo node) {
2286         Utils.recycleNode(mPendingFocusedNode);
2287         mPendingFocusedNode = copyNode(node);
2288         L.d("mPendingFocusedNode set to " + mPendingFocusedNode);
2289         mPendingFocusedExpirationTime = SystemClock.uptimeMillis() + mAfterFocusTimeoutMs;
2290     }
2291 
setEditNode(@ullable AccessibilityNodeInfo editNode)2292     private void setEditNode(@Nullable AccessibilityNodeInfo editNode) {
2293         if ((mEditNode == null && editNode == null) ||
2294                 (mEditNode != null && mEditNode.equals(editNode))) {
2295             return;
2296         }
2297         Utils.recycleNode(mEditNode);
2298         mEditNode = copyNode(editNode);
2299     }
2300 
2301     /**
2302      * Closes the IME if {@code newFocusedNode} isn't editable and isn't in the IME, and the
2303      * previously focused node is editable.
2304      */
maybeCloseIme(@ullable AccessibilityNodeInfo newFocusedNode)2305     private void maybeCloseIme(@Nullable AccessibilityNodeInfo newFocusedNode) {
2306         // Don't close the IME unless we're moving from an editable view to a non-editable view.
2307         if (mFocusedNode == null || newFocusedNode == null
2308                 || !mFocusedNode.isEditable() || newFocusedNode.isEditable()) {
2309             return;
2310         }
2311 
2312         // Don't close the IME if we're navigating to the IME.
2313         AccessibilityWindowInfo nextWindow = newFocusedNode.getWindow();
2314         if (nextWindow != null && nextWindow.getType() == TYPE_INPUT_METHOD) {
2315             Utils.recycleWindow(nextWindow);
2316             return;
2317         }
2318         Utils.recycleWindow(nextWindow);
2319 
2320         // To close the IME, we'll ask the FocusParkingView in the previous window to perform
2321         // ACTION_HIDE_IME.
2322         AccessibilityNodeInfo fpv = mNavigator.findFocusParkingView(mFocusedNode);
2323         if (fpv == null) {
2324             return;
2325         }
2326         if (!fpv.performAction(ACTION_HIDE_IME)) {
2327             L.w("Failed to close IME");
2328         }
2329         fpv.recycle();
2330     }
2331 
2332     /**
2333      * Sets {@link #mLastTouchedNode} to a copy of the given node, and clears {@link #mFocusedNode}.
2334      */
2335     @VisibleForTesting
setLastTouchedNode(@ullable AccessibilityNodeInfo lastTouchedNode)2336     void setLastTouchedNode(@Nullable AccessibilityNodeInfo lastTouchedNode) {
2337         setLastTouchedNodeInternal(lastTouchedNode);
2338         if (mLastTouchedNode != null && mFocusedNode != null) {
2339             setFocusedNodeInternal(null);
2340         }
2341     }
2342 
setLastTouchedNodeInternal(@ullable AccessibilityNodeInfo lastTouchedNode)2343     private void setLastTouchedNodeInternal(@Nullable AccessibilityNodeInfo lastTouchedNode) {
2344         if ((mLastTouchedNode == null && lastTouchedNode == null)
2345                 || (mLastTouchedNode != null && mLastTouchedNode.equals(lastTouchedNode))) {
2346             L.d("Don't reset mLastTouchedNode since it stays the same: " + mLastTouchedNode);
2347             return;
2348         }
2349 
2350         Utils.recycleNode(mLastTouchedNode);
2351         mLastTouchedNode = copyNode(lastTouchedNode);
2352     }
2353 
setIgnoreViewClickedNode(@ullable AccessibilityNodeInfo node)2354     private void setIgnoreViewClickedNode(@Nullable AccessibilityNodeInfo node) {
2355         if (mIgnoreViewClickedNode != null) {
2356             mIgnoreViewClickedNode.recycle();
2357         }
2358         mIgnoreViewClickedNode = copyNode(node);
2359         if (node != null) {
2360             mLastViewClickedTime = SystemClock.uptimeMillis();
2361         }
2362     }
2363 
setInRotaryMode(boolean inRotaryMode)2364     private void setInRotaryMode(boolean inRotaryMode) {
2365         mInRotaryMode = inRotaryMode;
2366         if (!mInRotaryMode) {
2367             setEditNode(null);
2368         }
2369         updateIme();
2370 
2371         // If we're controlling direct manipulation mode (i.e., the focused node supports rotate
2372         // directly), exit the mode when the user touches the screen.
2373         if (!mInRotaryMode && mInDirectManipulationMode) {
2374             if (mFocusedNode == null) {
2375                 L.e("mFocused is null in direct manipulation mode");
2376             } else if (DirectManipulationHelper.supportRotateDirectly(mFocusedNode)) {
2377                 L.d("Exit direct manipulation mode on user touch");
2378                 mInDirectManipulationMode = false;
2379                 boolean result = mFocusedNode.performAction(ACTION_CLEAR_SELECTION);
2380                 if (!result) {
2381                     L.w("Failed to perform ACTION_CLEAR_SELECTION on " + mFocusedNode);
2382                 }
2383             } else {
2384                 L.d("The client app should exit direct manipulation mode");
2385             }
2386         }
2387     }
2388 
2389     /** Switches to the rotary IME or the touch IME if needed. */
updateIme()2390     private void updateIme() {
2391         String newIme = mInRotaryMode ? mRotaryInputMethod : mTouchInputMethod;
2392         if (mInRotaryMode && !isValidIme(newIme)) {
2393             L.w("Rotary IME doesn't exist: " + newIme);
2394             return;
2395         }
2396         String oldIme = getCurrentIme();
2397         if (oldIme.equals(newIme)) {
2398             L.v("No need to switch IME: " + newIme);
2399             return;
2400         }
2401         setCurrentIme(newIme);
2402     }
2403 
getCurrentIme()2404     private String getCurrentIme() {
2405         return Settings.Secure.getString(getContentResolver(), DEFAULT_INPUT_METHOD);
2406     }
2407 
setCurrentIme(String newIme)2408     private void setCurrentIme(String newIme) {
2409         boolean result =
2410                 Settings.Secure.putString(getContentResolver(), DEFAULT_INPUT_METHOD, newIme);
2411         L.successOrFailure("Switching to IME: " + newIme, result);
2412     }
2413 
2414     /**
2415      * Performs {@link AccessibilityNodeInfo#ACTION_FOCUS} on a copy of the given {@code
2416      * targetNode}.
2417      *
2418      * @param targetNode the node to perform action on
2419      *
2420      * @return true if {@code targetNode} was focused already or became focused after performing
2421      *         {@link AccessibilityNodeInfo#ACTION_FOCUS}
2422      */
performFocusAction(@onNull AccessibilityNodeInfo targetNode)2423     private boolean performFocusAction(@NonNull AccessibilityNodeInfo targetNode) {
2424         return performFocusAction(targetNode, /* arguments= */ null);
2425     }
2426 
2427     /**
2428      * Performs {@link AccessibilityNodeInfo#ACTION_FOCUS} on a copy of the given {@code
2429      * targetNode}.
2430      *
2431      * @param targetNode the node to perform action on
2432      * @param arguments optional bundle with additional arguments
2433      *
2434      * @return true if {@code targetNode} was focused already or became focused after performing
2435      *         {@link AccessibilityNodeInfo#ACTION_FOCUS}
2436      */
performFocusAction( @onNull AccessibilityNodeInfo targetNode, @Nullable Bundle arguments)2437     private boolean performFocusAction(
2438             @NonNull AccessibilityNodeInfo targetNode, @Nullable Bundle arguments) {
2439         // If performFocusActionInternal is called on a reference to a saved node, for example
2440         // mFocusedNode, mFocusedNode might get recycled. If we use mFocusedNode later, it might
2441         // cause a crash. So let's pass a copy here.
2442         AccessibilityNodeInfo copyNode = copyNode(targetNode);
2443         boolean success = performFocusActionInternal(copyNode, arguments);
2444         copyNode.recycle();
2445         return success;
2446     }
2447 
2448     /**
2449      * Performs {@link AccessibilityNodeInfo#ACTION_FOCUS} on the given {@code targetNode}.
2450      * <p>
2451      * Note: Only {@link #performFocusAction(AccessibilityNodeInfo, Bundle)} can call this method.
2452      */
performFocusActionInternal( @onNull AccessibilityNodeInfo targetNode, @Nullable Bundle arguments)2453     private boolean performFocusActionInternal(
2454             @NonNull AccessibilityNodeInfo targetNode, @Nullable Bundle arguments) {
2455         if (targetNode.equals(mFocusedNode)) {
2456             L.d("No need to focus on targetNode because it's already focused: " + targetNode);
2457             return true;
2458         }
2459         boolean isInWebView = mNavigator.isInWebView(targetNode);
2460         if (!Utils.isFocusArea(targetNode) && Utils.hasFocus(targetNode) && !isInWebView) {
2461             // One of targetNode's descendants is already focused, so we can't perform ACTION_FOCUS
2462             // on targetNode directly unless it's a FocusArea. The workaround is to clear the focus
2463             // first (by focusing on the FocusParkingView), then focus on targetNode. The
2464             // prohibition on focusing a node that has focus doesn't apply in WebViews.
2465             L.d("One of targetNode's descendants is already focused: " + targetNode);
2466             if (!clearFocusInCurrentWindow()) {
2467                 return false;
2468             }
2469         }
2470 
2471         // Now we can perform ACTION_FOCUS on targetNode since it doesn't have focus, its
2472         // descendant's focus has been cleared, or it's a FocusArea.
2473         boolean result = targetNode.performAction(ACTION_FOCUS, arguments);
2474         if (!result) {
2475             L.w("Failed to perform ACTION_FOCUS on node " + targetNode);
2476             return false;
2477         }
2478         L.d("Performed ACTION_FOCUS on node " + targetNode);
2479 
2480         // If we performed ACTION_FOCUS on a FocusArea, find the descendant that was focused as a
2481         // result.
2482         if (Utils.isFocusArea(targetNode)) {
2483             if (updateFocusedNodeAfterPerformingFocusAction(targetNode)) {
2484                 return true;
2485             } else {
2486                 L.w("Unable to find focus after performing ACTION_FOCUS on a FocusArea");
2487             }
2488         }
2489 
2490         // Update mFocusedNode and mPendingFocusedNode.
2491         setFocusedNode(Utils.isFocusParkingView(targetNode) ? null : targetNode);
2492         setPendingFocusedNode(targetNode);
2493         return true;
2494     }
2495 
2496     /**
2497      * Searches {@code node} and its descendants for the focused node. If found, sets
2498      * {@link #mFocusedNode} and {@link #mPendingFocusedNode}. Returns whether the focus was found.
2499      * This method should be called after performing an action which changes the focus where we
2500      * can't predict which node will be focused.
2501      */
updateFocusedNodeAfterPerformingFocusAction( @onNull AccessibilityNodeInfo node)2502     private boolean updateFocusedNodeAfterPerformingFocusAction(
2503             @NonNull AccessibilityNodeInfo node) {
2504         AccessibilityNodeInfo focusedNode = mNavigator.findFocusedNodeInRoot(node);
2505         if (focusedNode == null) {
2506             L.w("Failed to find focused node in " + node);
2507             return false;
2508         }
2509         L.d("Found focused node " + focusedNode);
2510         setFocusedNode(focusedNode);
2511         setPendingFocusedNode(focusedNode);
2512         focusedNode.recycle();
2513         return true;
2514     }
2515 
2516     @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
2517     @VisibleForTesting
setRotateAcceleration(int rotationAcceleration2xMs, int rotationAcceleration3xMs)2518     void setRotateAcceleration(int rotationAcceleration2xMs, int rotationAcceleration3xMs) {
2519         mRotationAcceleration2xMs = rotationAcceleration2xMs;
2520         mRotationAcceleration3xMs = rotationAcceleration3xMs;
2521     }
2522 
2523     /**
2524      * Returns the number of "ticks" to rotate for a single rotate event with the given detent
2525      * {@code count} at the given time. Uses and updates {@link #mLastRotateEventTime}. The result
2526      * will be one, two, or three times the given detent {@code count} depending on the interval
2527      * between the current event and the previous event and the detent {@code count}.
2528      *
2529      * @param count     the number of detents the user rotated
2530      * @param eventTime the {@link SystemClock#uptimeMillis} when the event occurred
2531      * @return the number of "ticks" to rotate
2532      */
2533     @VisibleForTesting
getRotateAcceleration(int count, long eventTime)2534     int getRotateAcceleration(int count, long eventTime) {
2535         // count is 0 when testing key "C" or "V" is pressed.
2536         if (count <= 0) {
2537             count = 1;
2538         }
2539         int result = count;
2540         // TODO(b/153195148): This method can be improved once we've plumbed through the VHAL
2541         //  changes. We'll get timestamps for each detent.
2542         long delta = (eventTime - mLastRotateEventTime) / count;  // Assume constant speed.
2543         if (delta <= mRotationAcceleration3xMs) {
2544             result = count * 3;
2545         } else if (delta <= mRotationAcceleration2xMs) {
2546             result = count * 2;
2547         }
2548         mLastRotateEventTime = eventTime;
2549         return result;
2550     }
2551 
copyNode(@ullable AccessibilityNodeInfo node)2552     private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) {
2553         return mNodeCopier.copy(node);
2554     }
2555 
2556     /** Sets a NodeCopier instance for testing. */
2557     @VisibleForTesting
setNodeCopier(@onNull NodeCopier nodeCopier)2558     void setNodeCopier(@NonNull NodeCopier nodeCopier) {
2559         mNodeCopier = nodeCopier;
2560         mNavigator.setNodeCopier(nodeCopier);
2561         mWindowCache.setNodeCopier(nodeCopier);
2562     }
2563 
2564     /**
2565      * Checks if the {@code componentName} is an enabled input method or a disabled system input
2566      * method. The string should be in the format {@code "package.name/.ClassName"}, e.g. {@code
2567      * "com.android.inputmethod.latin/.CarLatinIME"}. Disabled system input methods are considered
2568      * valid because switching back to the touch IME should occur even if it's disabled and because
2569      * the rotary IME may be disabled so that it doesn't get used for touch.
2570      */
isValidIme(String componentName)2571     private boolean isValidIme(String componentName) {
2572         if (TextUtils.isEmpty(componentName)) {
2573             return false;
2574         }
2575         return imeSettingContains(ENABLED_INPUT_METHODS, componentName)
2576                 || imeSettingContains(DISABLED_SYSTEM_INPUT_METHODS, componentName);
2577     }
2578 
2579     /**
2580      * Fetches the secure setting {@code settingName} containing a colon-separated list of IMEs with
2581      * their subtypes and returns whether {@code componentName} is one of the IMEs.
2582      */
imeSettingContains(@onNull String settingName, @NonNull String componentName)2583     private boolean imeSettingContains(@NonNull String settingName, @NonNull String componentName) {
2584         String colonSeparatedComponentNamesWithSubtypes =
2585                 Settings.Secure.getString(getContentResolver(), settingName);
2586         if (colonSeparatedComponentNamesWithSubtypes == null) {
2587             return false;
2588         }
2589         return Arrays.stream(colonSeparatedComponentNamesWithSubtypes.split(":"))
2590                 .map(componentNameWithSubtypes -> componentNameWithSubtypes.split(";"))
2591                 .anyMatch(componentNameAndSubtypes -> componentNameAndSubtypes.length >= 1
2592                         && componentNameAndSubtypes[0].equals(componentName));
2593     }
2594 
2595     @VisibleForTesting
getFocusedNode()2596     AccessibilityNodeInfo getFocusedNode() {
2597         return mFocusedNode;
2598     }
2599 
2600     @VisibleForTesting
setNavigator(@onNull Navigator navigator)2601     void setNavigator(@NonNull Navigator navigator) {
2602         mNavigator = navigator;
2603     }
2604 
2605     @VisibleForTesting
setInputManager(@onNull InputManager inputManager)2606     void setInputManager(@NonNull InputManager inputManager) {
2607         mInputManager = inputManager;
2608     }
2609 
2610     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
2611     @Override
dump(@onNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args)2612     protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
2613             @Nullable String[] args) {
2614         boolean dumpAsProto = args != null && ArrayUtils.indexOf(args, "proto") != -1;
2615         DualDumpOutputStream dumpOutputStream = dumpAsProto
2616                 ? new DualDumpOutputStream(new ProtoOutputStream(new FileOutputStream(fd)))
2617                 : new DualDumpOutputStream(new IndentingPrintWriter(writer, "  "));
2618         dumpOutputStream.write("rotationAcceleration2xMs",
2619                 RotaryProtos.RotaryService.ROTATION_ACCELERATION_2X_MS, mRotationAcceleration2xMs);
2620         dumpOutputStream.write("rotationAcceleration3xMs",
2621                 RotaryProtos.RotaryService.ROTATION_ACCELERATION_3X_MS, mRotationAcceleration3xMs);
2622         DumpUtils.writeObject(dumpOutputStream, "focusedNode",
2623                 RotaryProtos.RotaryService.FOCUSED_NODE, mFocusedNode);
2624         DumpUtils.writeObject(dumpOutputStream, "editNode", RotaryProtos.RotaryService.EDIT_NODE,
2625                 mEditNode);
2626         DumpUtils.writeObject(dumpOutputStream, "focusArea", RotaryProtos.RotaryService.FOCUS_AREA,
2627                 mFocusArea);
2628         DumpUtils.writeObject(dumpOutputStream, "lastTouchedNode",
2629                 RotaryProtos.RotaryService.LAST_TOUCHED_NODE, mLastTouchedNode);
2630         dumpOutputStream.write("rotaryInputMethod", RotaryProtos.RotaryService.ROTARY_INPUT_METHOD,
2631                 mRotaryInputMethod);
2632         dumpOutputStream.write("defaultTouchInputMethod",
2633                 RotaryProtos.RotaryService.DEFAULT_TOUCH_INPUT_METHOD, mDefaultTouchInputMethod);
2634         dumpOutputStream.write("touchInputMethod", RotaryProtos.RotaryService.TOUCH_INPUT_METHOD,
2635                 mTouchInputMethod);
2636         DumpUtils.writeFocusDirection(dumpOutputStream, dumpAsProto, "hunNudgeDirection",
2637                 RotaryProtos.RotaryService.HUN_NUDGE_DIRECTION, mHunNudgeDirection);
2638         DumpUtils.writeFocusDirection(dumpOutputStream, dumpAsProto, "hunEscapeNudgeDirection",
2639                 RotaryProtos.RotaryService.HUN_ESCAPE_NUDGE_DIRECTION, mHunEscapeNudgeDirection);
2640         DumpUtils.writeInts(dumpOutputStream, dumpAsProto, "offScreenNudgeGlobalActions",
2641                 RotaryProtos.RotaryService.OFF_SCREEN_NUDGE_GLOBAL_ACTIONS,
2642                 mOffScreenNudgeGlobalActions);
2643         DumpUtils.writeKeyCodes(dumpOutputStream, dumpAsProto, "offScreenNudgeKeyCodes",
2644                 RotaryProtos.RotaryService.OFF_SCREEN_NUDGE_KEY_CODES, mOffScreenNudgeKeyCodes);
2645         DumpUtils.writeObjects(dumpOutputStream, dumpAsProto, "offScreenNudgeIntents",
2646                 RotaryProtos.RotaryService.OFF_SCREEN_NUDGE_INTENTS, mOffScreenNudgeIntents);
2647         dumpOutputStream.write("afterScrollTimeoutMs",
2648                 RotaryProtos.RotaryService.AFTER_SCROLL_TIMEOUT_MS, mAfterFocusTimeoutMs);
2649         DumpUtils.writeAfterScrollAction(dumpOutputStream, dumpAsProto, "afterScrollAction",
2650                 RotaryProtos.RotaryService.AFTER_SCROLL_ACTION, mAfterScrollAction);
2651         dumpOutputStream.write("afterScrollActionUntil",
2652                 RotaryProtos.RotaryService.AFTER_SCROLL_ACTION_UNTIL, mAfterScrollActionUntil);
2653         dumpOutputStream.write("inRotaryMode", RotaryProtos.RotaryService.IN_ROTARY_MODE,
2654                 mInRotaryMode);
2655         dumpOutputStream.write("inDirectManipulationMode",
2656                 RotaryProtos.RotaryService.IN_DIRECT_MANIPULATION_MODE, mInDirectManipulationMode);
2657         dumpOutputStream.write("lastRotateEventTime",
2658                 RotaryProtos.RotaryService.LAST_ROTATE_EVENT_TIME, mLastRotateEventTime);
2659         dumpOutputStream.write("longPressMs", RotaryProtos.RotaryService.LONG_PRESS_MS,
2660                 mLongPressMs);
2661         dumpOutputStream.write("longPressTriggered",
2662                 RotaryProtos.RotaryService.LONG_PRESS_TRIGGERED, mLongPressTriggered);
2663         DumpUtils.writeComponentNameToString(dumpOutputStream, "foregroundActivity",
2664                 RotaryProtos.RotaryService.FOREGROUND_ACTIVITY, mForegroundActivity);
2665         dumpOutputStream.write("afterFocusTimeoutMs",
2666                 RotaryProtos.RotaryService.AFTER_FOCUS_TIMEOUT_MS, mAfterFocusTimeoutMs);
2667         DumpUtils.writeObject(dumpOutputStream, "pendingFocusedNode",
2668                 RotaryProtos.RotaryService.PENDING_FOCUSED_NODE, mPendingFocusedNode);
2669         dumpOutputStream.write("pendingFocusedExpirationTime",
2670                 RotaryProtos.RotaryService.PENDING_FOCUSED_EXPIRATION_TIME,
2671                 mPendingFocusedExpirationTime);
2672         mNavigator.dump(dumpOutputStream, dumpAsProto, "navigator",
2673                 RotaryProtos.RotaryService.NAVIGATOR);
2674         mWindowCache.dump(dumpOutputStream, dumpAsProto, "windowCache",
2675                 RotaryProtos.RotaryService.WINDOW_CACHE);
2676         dumpOutputStream.flush();
2677     }
2678 
2679 }
2680