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