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