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