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