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