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