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