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