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