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.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED; 19 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED; 20 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED; 21 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; 22 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED; 23 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOWS_CHANGED; 24 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; 25 import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_REMOVED; 26 import static android.view.Display.DEFAULT_DISPLAY; 27 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 28 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD; 29 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD; 30 31 import android.accessibilityservice.AccessibilityService; 32 import android.accessibilityservice.AccessibilityServiceInfo; 33 import android.car.Car; 34 import android.car.input.CarInputManager; 35 import android.car.input.RotaryEvent; 36 import android.content.Context; 37 import android.content.res.Resources; 38 import android.hardware.display.DisplayManager; 39 import android.hardware.input.InputManager; 40 import android.os.Build; 41 import android.os.SystemClock; 42 import android.text.TextUtils; 43 import android.view.Display; 44 import android.view.InputDevice; 45 import android.view.KeyEvent; 46 import android.view.MotionEvent; 47 import android.view.View; 48 import android.view.WindowManager; 49 import android.view.accessibility.AccessibilityEvent; 50 import android.view.accessibility.AccessibilityNodeInfo; 51 import android.view.accessibility.AccessibilityWindowInfo; 52 53 import androidx.annotation.NonNull; 54 import androidx.annotation.Nullable; 55 56 import com.android.car.ui.utils.DirectManipulationHelper; 57 58 import java.util.Collections; 59 import java.util.HashMap; 60 import java.util.List; 61 import java.util.Map; 62 63 /** 64 * A service that can change focus based on rotary controller rotation and nudges, and perform 65 * clicks based on rotary controller center button clicks. 66 * <p> 67 * As an {@link AccessibilityService}, this service responds to {@link KeyEvent}s (on debug builds 68 * only) and {@link AccessibilityEvent}s. 69 * <p> 70 * On debug builds, {@link KeyEvent}s coming from the keyboard are handled by clicking the view, or 71 * moving the focus, sometimes within a window and sometimes between windows. 72 * <p> 73 * This service listens to two types of {@link AccessibilityEvent}s: {@link 74 * AccessibilityEvent#TYPE_VIEW_FOCUSED} and {@link AccessibilityEvent#TYPE_VIEW_CLICKED}. The 75 * former is used to keep {@link #mFocusedNode} up to date as the focus changes. The latter is used 76 * to detect when the user switches from rotary mode to touch mode and to keep {@link 77 * #mLastTouchedNode} up to date. 78 * <p> 79 * As a {@link CarInputManager.CarInputCaptureCallback}, this service responds to {@link KeyEvent}s 80 * and {@link RotaryEvent}s, both of which are coming from the controller. 81 * <p> 82 * {@link KeyEvent}s are handled by clicking the view, or moving the focus, sometimes within a 83 * window and sometimes between windows. 84 * <p> 85 * {@link RotaryEvent}s are handled by moving the focus within the same {@link 86 * com.android.car.ui.FocusArea}. 87 * <p> 88 * Note: onFoo methods are all called on the main thread so no locks are needed. 89 */ 90 public class RotaryService extends AccessibilityService implements 91 CarInputManager.CarInputCaptureCallback { 92 93 /* 94 * Whether to treat the application window as system window for direct manipulation mode. Set it 95 * to {@code true} for testing only. 96 */ 97 private static final boolean TREAT_APP_WINDOW_AS_SYSTEM_WINDOW = false; 98 99 /** 100 * How many detents to rotate when the user holds in shift while pressing C, V, Q, or E on a 101 * debug build. 102 */ 103 private static final int SHIFT_DETENTS = 10; 104 105 @NonNull 106 private NodeCopier mNodeCopier = new NodeCopier(); 107 108 private Navigator mNavigator; 109 110 /** Input types to capture. */ 111 private final int[] mInputTypes = new int[]{ 112 // Capture controller rotation. 113 CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION, 114 // Capture controller center button clicks. 115 CarInputManager.INPUT_TYPE_DPAD_KEYS, 116 // Capture controller nudges. 117 CarInputManager.INPUT_TYPE_SYSTEM_NAVIGATE_KEYS}; 118 119 /** 120 * Time interval in milliseconds to decide whether we should accelerate the rotation by 3 times 121 * for a rotate event. 122 */ 123 private int mRotationAcceleration3xMs; 124 125 /** 126 * Time interval in milliseconds to decide whether we should accelerate the rotation by 2 times 127 * for a rotate event. 128 */ 129 private int mRotationAcceleration2xMs; 130 131 /** Whether to clear focus area history when the user rotates the controller. */ 132 private boolean mClearFocusAreaHistoryWhenRotating; 133 134 /** 135 * The currently focused node, if any. It's null if no nodes are focused or a {@link 136 * com.android.car.ui.FocusParkingView} is focused. 137 */ 138 private AccessibilityNodeInfo mFocusedNode = null; 139 140 /** 141 * The previously focused node, if any. It's null if no nodes were focused or a {@link 142 * com.android.car.ui.FocusParkingView} was focused. 143 */ 144 private AccessibilityNodeInfo mPreviousFocusedNode = null; 145 146 /** 147 * The currently focused {@link com.android.car.ui.FocusParkingView} that was focused by us to 148 * clear the focus, if any. 149 */ 150 private AccessibilityNodeInfo mFocusParkingView = null; 151 152 /** 153 * The current scrollable container, if any. Either {@link #mFocusedNode} or an ancestor of it. 154 */ 155 private AccessibilityNodeInfo mScrollableContainer = null; 156 157 /** 158 * The last clicked node by touching the screen, if any were clicked since we last navigated. 159 */ 160 private AccessibilityNodeInfo mLastTouchedNode = null; 161 162 /** 163 * How many milliseconds to ignore {@link AccessibilityEvent#TYPE_VIEW_CLICKED} events after 164 * performing {@link AccessibilityNodeInfo#ACTION_CLICK} or injecting a {@link 165 * KeyEvent#KEYCODE_DPAD_CENTER} event. 166 */ 167 private int mIgnoreViewClickedMs; 168 169 /** 170 * When not {@code null}, {@link AccessibilityEvent#TYPE_VIEW_CLICKED} events with this node 171 * are ignored if they occur before {@link #mIgnoreViewClickedUntil}. 172 */ 173 private AccessibilityNodeInfo mIgnoreViewClickedNode; 174 175 /** 176 * When to stop ignoring {@link AccessibilityEvent#TYPE_VIEW_CLICKED} events for {@link 177 * #mIgnoreViewClickedNode} in {@link SystemClock#uptimeMillis}. 178 */ 179 private long mIgnoreViewClickedUntil; 180 181 /** 182 * Possible actions to do after receiving {@link AccessibilityEvent#TYPE_VIEW_SCROLLED}. 183 * 184 * @see #injectScrollEvent 185 */ 186 private enum AfterScrollAction { 187 /** Do nothing. */ 188 NONE, 189 /** 190 * Focus the view before the focused view in Tab order in the scrollable container, if any. 191 */ 192 FOCUS_PREVIOUS, 193 /** 194 * Focus the view after the focused view in Tab order in the scrollable container, if any. 195 */ 196 FOCUS_NEXT, 197 /** Focus the first view in the scrollable container, if any. */ 198 FOCUS_FIRST, 199 /** Focus the last view in the scrollable container, if any. */ 200 FOCUS_LAST, 201 } 202 203 private AfterScrollAction mAfterScrollAction = AfterScrollAction.NONE; 204 205 /** 206 * How many milliseconds to wait for a {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event after 207 * scrolling. 208 */ 209 private int mAfterScrollTimeoutMs; 210 211 /** 212 * When to give up on receiving {@link AccessibilityEvent#TYPE_VIEW_SCROLLED}, in 213 * {@link SystemClock#uptimeMillis}. 214 */ 215 private long mAfterScrollActionUntil; 216 217 /** Whether we're in rotary mode (vs touch mode). */ 218 private boolean mInRotaryMode; 219 220 /** Whether we're in direct manipulation mode. */ 221 private boolean mInDirectManipulationMode; 222 223 /** The {@link SystemClock#uptimeMillis} when the last rotary rotation event occurred. */ 224 private long mLastRotateEventTime; 225 226 /** 227 * The repeat count of {@link KeyEvent#KEYCODE_DPAD_CENTER}. Use to prevent processing a center 228 * button click when the center button is released after a long press. 229 */ 230 private int mCenterButtonRepeatCount; 231 232 private static final Map<Integer, Integer> TEST_TO_REAL_KEYCODE_MAP; 233 234 private static final Map<Integer, Integer> DIRECTION_TO_KEYCODE_MAP; 235 236 static { 237 Map<Integer, Integer> map = new HashMap<>(); map.put(KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT)238 map.put(KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT); map.put(KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT)239 map.put(KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT); map.put(KeyEvent.KEYCODE_W, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP)240 map.put(KeyEvent.KEYCODE_W, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP); map.put(KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN)241 map.put(KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN); map.put(KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_DPAD_CENTER)242 map.put(KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_DPAD_CENTER); map.put(KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_BACK)243 map.put(KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_BACK); 244 // Legacy map map.put(KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT)245 map.put(KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT); map.put(KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT)246 map.put(KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT); map.put(KeyEvent.KEYCODE_I, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP)247 map.put(KeyEvent.KEYCODE_I, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP); map.put(KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN)248 map.put(KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN); map.put(KeyEvent.KEYCODE_COMMA, KeyEvent.KEYCODE_DPAD_CENTER)249 map.put(KeyEvent.KEYCODE_COMMA, KeyEvent.KEYCODE_DPAD_CENTER); map.put(KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_BACK)250 map.put(KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_BACK); 251 252 TEST_TO_REAL_KEYCODE_MAP = Collections.unmodifiableMap(map); 253 } 254 255 static { 256 Map<Integer, Integer> map = new HashMap<>(); map.put(View.FOCUS_UP, KeyEvent.KEYCODE_DPAD_UP)257 map.put(View.FOCUS_UP, KeyEvent.KEYCODE_DPAD_UP); map.put(View.FOCUS_DOWN, KeyEvent.KEYCODE_DPAD_DOWN)258 map.put(View.FOCUS_DOWN, KeyEvent.KEYCODE_DPAD_DOWN); map.put(View.FOCUS_LEFT, KeyEvent.KEYCODE_DPAD_LEFT)259 map.put(View.FOCUS_LEFT, KeyEvent.KEYCODE_DPAD_LEFT); map.put(View.FOCUS_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT)260 map.put(View.FOCUS_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT); 261 262 DIRECTION_TO_KEYCODE_MAP = Collections.unmodifiableMap(map); 263 } 264 265 private Car mCar; 266 private CarInputManager mCarInputManager; 267 private InputManager mInputManager; 268 269 /** Package name of foreground app. */ 270 private CharSequence mForegroundApp; 271 272 private WindowManager mWindowManager; 273 274 @Override onCreate()275 public void onCreate() { 276 super.onCreate(); 277 Resources res = getResources(); 278 mRotationAcceleration3xMs = res.getInteger(R.integer.rotation_acceleration_3x_ms); 279 mRotationAcceleration2xMs = res.getInteger(R.integer.rotation_acceleration_2x_ms); 280 281 mClearFocusAreaHistoryWhenRotating = 282 res.getBoolean(R.bool.clear_focus_area_history_when_rotating); 283 284 @RotaryCache.CacheType int focusHistoryCacheType = 285 res.getInteger(R.integer.focus_history_cache_type); 286 int focusHistoryCacheSize = 287 res.getInteger(R.integer.focus_history_cache_size); 288 int focusHistoryExpirationTimeMs = 289 res.getInteger(R.integer.focus_history_expiration_time_ms); 290 291 @RotaryCache.CacheType int focusAreaHistoryCacheType = 292 res.getInteger(R.integer.focus_area_history_cache_type); 293 int focusAreaHistoryCacheSize = 294 res.getInteger(R.integer.focus_area_history_cache_size); 295 int focusAreaHistoryExpirationTimeMs = 296 res.getInteger(R.integer.focus_area_history_expiration_time_ms); 297 298 @RotaryCache.CacheType int focusWindowCacheType = 299 res.getInteger(R.integer.focus_window_cache_type); 300 int focusWindowCacheSize = 301 res.getInteger(R.integer.focus_window_cache_size); 302 int focusWindowExpirationTimeMs = 303 res.getInteger(R.integer.focus_window_expiration_time_ms); 304 305 int hunMarginHorizontal = 306 res.getDimensionPixelSize(R.dimen.notification_headsup_card_margin_horizontal); 307 int hunLeft = hunMarginHorizontal; 308 WindowManager windowManager = getSystemService(WindowManager.class); 309 int displayWidth = windowManager.getCurrentWindowMetrics().getBounds().width(); 310 int hunRight = displayWidth - hunMarginHorizontal; 311 boolean showHunOnBottom = res.getBoolean(R.bool.config_showHeadsUpNotificationOnBottom); 312 313 mIgnoreViewClickedMs = res.getInteger(R.integer.ignore_view_clicked_ms); 314 mAfterScrollTimeoutMs = res.getInteger(R.integer.after_scroll_timeout_ms); 315 316 mNavigator = new Navigator( 317 focusHistoryCacheType, 318 focusHistoryCacheSize, 319 focusHistoryExpirationTimeMs, 320 focusAreaHistoryCacheType, 321 focusAreaHistoryCacheSize, 322 focusAreaHistoryExpirationTimeMs, 323 focusWindowCacheType, 324 focusWindowCacheSize, 325 focusWindowExpirationTimeMs, 326 hunLeft, 327 hunRight, 328 showHunOnBottom); 329 } 330 331 /** 332 * {@inheritDoc} 333 * <p> 334 * We need to access WindowManager in onCreate() and 335 * IAccessibilityServiceClientWrapper.Callbacks#init(). Since WindowManager is a visual 336 * service, only Activity or other visual Context can access it. So we create a window context 337 * (a visual context) and delegate getSystemService() to it. 338 */ 339 @Override getSystemService(@erviceName @onNull String name)340 public Object getSystemService(@ServiceName @NonNull String name) { 341 // Guarantee that we always return the same WindowManager instance. 342 if (WINDOW_SERVICE.equals(name)) { 343 if (mWindowManager == null) { 344 // We need to set the display before creating the WindowContext. 345 DisplayManager displayManager = getSystemService(DisplayManager.class); 346 Display primaryDisplay = displayManager.getDisplay(DEFAULT_DISPLAY); 347 updateDisplay(primaryDisplay.getDisplayId()); 348 349 Context windowContext = createWindowContext(TYPE_APPLICATION_OVERLAY, null); 350 mWindowManager = (WindowManager) windowContext.getSystemService(WINDOW_SERVICE); 351 } 352 return mWindowManager; 353 } 354 return super.getSystemService(name); 355 } 356 357 @Override onServiceConnected()358 public void onServiceConnected() { 359 super.onServiceConnected(); 360 361 mCar = Car.createCar(this, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 362 (car, ready) -> { 363 mCar = car; 364 if (ready) { 365 mCarInputManager = 366 (CarInputManager) mCar.getCarManager(Car.CAR_INPUT_SERVICE); 367 mCarInputManager.requestInputEventCapture(this, 368 CarInputManager.TARGET_DISPLAY_TYPE_MAIN, 369 mInputTypes, 370 CarInputManager.CAPTURE_REQ_FLAGS_ALLOW_DELAYED_GRANT); 371 } 372 }); 373 374 if (Build.IS_DEBUGGABLE) { 375 AccessibilityServiceInfo serviceInfo = getServiceInfo(); 376 // Filter testing KeyEvents from a keyboard. 377 serviceInfo.flags |= AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS; 378 setServiceInfo(serviceInfo); 379 } 380 381 mInputManager = getSystemService(InputManager.class); 382 } 383 384 @Override onInterrupt()385 public void onInterrupt() { 386 L.v("onInterrupt()"); 387 } 388 389 @Override onDestroy()390 public void onDestroy() { 391 if (mCarInputManager != null) { 392 mCarInputManager.releaseInputEventCapture(CarInputManager.TARGET_DISPLAY_TYPE_MAIN); 393 } 394 if (mCar != null) { 395 mCar.disconnect(); 396 } 397 super.onDestroy(); 398 } 399 400 @Override onAccessibilityEvent(AccessibilityEvent event)401 public void onAccessibilityEvent(AccessibilityEvent event) { 402 switch (event.getEventType()) { 403 case TYPE_VIEW_FOCUSED: { 404 handleViewFocusedEvent(event); 405 break; 406 } 407 case TYPE_VIEW_CLICKED: { 408 handleViewClickedEvent(event); 409 break; 410 } 411 case TYPE_VIEW_ACCESSIBILITY_FOCUSED: { 412 updateDirectManipulationMode(event, true); 413 break; 414 } 415 case TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: { 416 updateDirectManipulationMode(event, false); 417 break; 418 } 419 case TYPE_VIEW_SCROLLED: { 420 handleViewScrolledEvent(event); 421 break; 422 } 423 case TYPE_WINDOW_STATE_CHANGED: { 424 CharSequence packageName = event.getPackageName(); 425 onForegroundAppChanged(packageName); 426 break; 427 } 428 case TYPE_WINDOWS_CHANGED: { 429 handleWindowsChangedEvent(event); 430 break; 431 } 432 default: 433 // Do nothing. 434 } 435 } 436 437 /** 438 * Callback of {@link AccessibilityService}. It allows us to observe testing {@link KeyEvent}s 439 * from keyboard, including keys "C" and "V" to emulate controller rotation, keys "J" "L" "I" 440 * "K" to emulate controller nudges, and key "Comma" to emulate center button clicks. 441 */ 442 @Override onKeyEvent(KeyEvent event)443 protected boolean onKeyEvent(KeyEvent event) { 444 if (Build.IS_DEBUGGABLE) { 445 return handleKeyEvent(event); 446 } 447 return false; 448 } 449 450 /** 451 * Callback of {@link CarInputManager.CarInputCaptureCallback}. It allows us to capture {@link 452 * KeyEvent}s generated by a navigation controller, such as controller nudge and controller 453 * click events. 454 */ 455 @Override onKeyEvents(int targetDisplayId, List<KeyEvent> events)456 public void onKeyEvents(int targetDisplayId, List<KeyEvent> events) { 457 if (!isValidDisplayId(targetDisplayId)) { 458 return; 459 } 460 for (KeyEvent event : events) { 461 handleKeyEvent(event); 462 } 463 } 464 465 /** 466 * Callback of {@link CarInputManager.CarInputCaptureCallback}. It allows us to capture {@link 467 * RotaryEvent}s generated by a navigation controller. 468 */ 469 @Override onRotaryEvents(int targetDisplayId, List<RotaryEvent> events)470 public void onRotaryEvents(int targetDisplayId, List<RotaryEvent> events) { 471 if (!isValidDisplayId(targetDisplayId)) { 472 return; 473 } 474 for (RotaryEvent rotaryEvent : events) { 475 handleRotaryEvent(rotaryEvent); 476 } 477 } 478 479 @Override onCaptureStateChanged(int targetDisplayId, @android.annotation.NonNull @CarInputManager.InputTypeEnum int[] activeInputTypes)480 public void onCaptureStateChanged(int targetDisplayId, 481 @android.annotation.NonNull @CarInputManager.InputTypeEnum int[] activeInputTypes) { 482 // Do nothing. 483 } 484 isValidDisplayId(int displayId)485 private static boolean isValidDisplayId(int displayId) { 486 if (displayId == CarInputManager.TARGET_DISPLAY_TYPE_MAIN) { 487 return true; 488 } 489 L.e("RotaryService shouldn't capture events from display ID " + displayId); 490 return false; 491 } 492 493 /** 494 * Handles key events. Returns whether the key event was consumed. To avoid invalid event stream 495 * getting through to the application, if a key down event is consumed, the corresponding key up 496 * event must be consumed too, and vice versa. 497 */ handleKeyEvent(KeyEvent event)498 private boolean handleKeyEvent(KeyEvent event) { 499 int action = event.getAction(); 500 boolean isActionDown = action == KeyEvent.ACTION_DOWN; 501 int keyCode = getKeyCode(event); 502 int detents = event.isShiftPressed() ? SHIFT_DETENTS : 1; 503 switch (keyCode) { 504 case KeyEvent.KEYCODE_Q: 505 case KeyEvent.KEYCODE_C: 506 if (isActionDown) { 507 handleRotateEvent(/* clockwise= */ false, detents, 508 event.getEventTime()); 509 } 510 return true; 511 case KeyEvent.KEYCODE_E: 512 case KeyEvent.KEYCODE_V: 513 if (isActionDown) { 514 handleRotateEvent(/* clockwise= */ true, detents, 515 event.getEventTime()); 516 } 517 return true; 518 case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT: 519 handleNudgeEvent(View.FOCUS_LEFT, action); 520 return true; 521 case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT: 522 handleNudgeEvent(View.FOCUS_RIGHT, action); 523 return true; 524 case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP: 525 handleNudgeEvent(View.FOCUS_UP, action); 526 return true; 527 case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN: 528 handleNudgeEvent(View.FOCUS_DOWN, action); 529 return true; 530 case KeyEvent.KEYCODE_DPAD_CENTER: 531 if (isActionDown) { 532 mCenterButtonRepeatCount = event.getRepeatCount(); 533 } 534 if (mCenterButtonRepeatCount == 0) { 535 handleCenterButtonEvent(action, /* longClick= */ false); 536 } else if (mCenterButtonRepeatCount == 1) { 537 handleCenterButtonEvent(action, /* longClick= */ true); 538 } 539 return true; 540 case KeyEvent.KEYCODE_BACK: 541 if (mInDirectManipulationMode) { 542 handleBackButtonEvent(action); 543 return true; 544 } 545 return false; 546 default: 547 // Do nothing 548 } 549 return false; 550 } 551 552 /** Handles {@link AccessibilityEvent#TYPE_VIEW_FOCUSED} event. */ handleViewFocusedEvent(@onNull AccessibilityEvent event)553 private void handleViewFocusedEvent(@NonNull AccessibilityEvent event) { 554 // A view was focused. We ignore focus changes in touch mode. We don't use 555 // TYPE_VIEW_FOCUSED to keep mLastTouchedNode up to date because most views can't be 556 // focused in touch mode. In rotary mode, we use TYPE_VIEW_FOCUSED events to keep 557 // mFocusedNode up to date, to clear the focus when moving between windows, to detect when 558 // a scrollable container scrolls and pushes the focused descendant out of the viewport, 559 // and to detect when the focused view is removed. 560 if (!mInRotaryMode) { 561 return; 562 } 563 AccessibilityNodeInfo sourceNode = event.getSource(); 564 // No need to handle TYPE_VIEW_FOCUSED event if sourceNode is null or the focused node stays 565 // the same. 566 if (sourceNode == null || sourceNode.equals(mFocusedNode)) { 567 Utils.recycleNode(sourceNode); 568 return; 569 } 570 // Case 1: the focused view is not a FocusParkingView. In this case we just update 571 // mFocusedNode. 572 if (!Utils.isFocusParkingView(sourceNode)) { 573 // Android doesn't clear focus automatically when focus is set in another window. 574 maybeClearFocusInCurrentWindow(sourceNode); 575 setFocusedNode(sourceNode); 576 } 577 // Case 2: the focused view is a FocusParkingView and it was focused by us to clear the 578 // focus in another window. In this case we should do nothing but reset mFocusParkingView. 579 else if (sourceNode.equals(mFocusParkingView)) { 580 Utils.recycleNode(mFocusParkingView); 581 mFocusParkingView = null; 582 } 583 // Case 3: the focused view is a FocusParkingView and it was focused when scrolling pushed 584 // the focused view out of the viewport. When this happens, focus the scrollable container. 585 else if (mFocusedNode != null && mScrollableContainer != null) { 586 mScrollableContainer = Utils.refreshNode(mScrollableContainer); 587 if (mScrollableContainer != null) { 588 L.d("Moving focus from FocusParkingView to scrollable container"); 589 performFocusAction(mScrollableContainer); 590 } else { 591 L.d("mScrollableContainer is not in the view tree"); 592 } 593 } 594 // Case 4 (all other cases): the focused view is a FocusParkingView and it's none 595 // of the cases above. For example: 596 // 1. When the previously focused view is removed by the app, Android will focus on 597 // the first focusable view in the window, which is the FocusParkingView. 598 // 2. When a dialog window shows up, Android will focus on the first focusable view 599 // in the dialog window, which is the FocusParkingView. 600 // In both cases we should try to move focus to another view nearby. 601 else { 602 moveFocusToNearbyView(sourceNode); 603 } 604 605 // Recycle sourceNode no matter in which case above. 606 Utils.recycleNode(sourceNode); 607 } 608 609 /** Handles {@link AccessibilityEvent#TYPE_VIEW_CLICKED} event. */ handleViewClickedEvent(@onNull AccessibilityEvent event)610 private void handleViewClickedEvent(@NonNull AccessibilityEvent event) { 611 // A view was clicked. If we triggered the click via performAction(ACTION_CLICK) or 612 // by injecting KEYCODE_DPAD_CENTER, we ignore it. Otherwise, we assume the user 613 // touched the screen. In this case, we exit rotary mode if necessary, update 614 // mLastTouchedNode, and clear the focus if the user touched a view in a different 615 // window. 616 // To decide whether the click was triggered by us, we can compare the source node 617 // in the event with mIgnoreViewClickedNode. If they're equal, the click was 618 // triggered by us. But there is a corner case. If a dialog shows up after we 619 // clicked the view, the window containing the view will be removed. We still 620 // receive click event (TYPE_VIEW_CLICKED) but the source node in the event will be 621 // null. 622 // Note: there is no way to tell whether the window is removed in click event 623 // because window remove event (TYPE_WINDOWS_CHANGED with type 624 // WINDOWS_CHANGE_REMOVED) comes AFTER click event. 625 AccessibilityNodeInfo sourceNode = event.getSource(); 626 if (mIgnoreViewClickedNode != null 627 && event.getEventTime() < mIgnoreViewClickedUntil 628 && ((sourceNode == null) || mIgnoreViewClickedNode.equals(sourceNode))) { 629 setIgnoreViewClickedNode(null); 630 } else { 631 // Enter touch mode once the user touches the screen. 632 mInRotaryMode = false; 633 if (sourceNode != null) { 634 // Explicitly clear focus when user uses touch in another window. 635 maybeClearFocusInCurrentWindow(sourceNode); 636 637 if (!sourceNode.equals(mLastTouchedNode)) { 638 setLastTouchedNode(sourceNode); 639 } 640 } 641 } 642 Utils.recycleNode(sourceNode); 643 } 644 645 /** Handles {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event. */ handleViewScrolledEvent(@onNull AccessibilityEvent event)646 private void handleViewScrolledEvent(@NonNull AccessibilityEvent event) { 647 if (mAfterScrollAction == AfterScrollAction.NONE 648 || SystemClock.uptimeMillis() >= mAfterScrollActionUntil) { 649 return; 650 } 651 AccessibilityNodeInfo sourceNode = event.getSource(); 652 if (sourceNode == null || !Utils.isScrollableContainer(sourceNode)) { 653 Utils.recycleNode(sourceNode); 654 return; 655 } 656 switch (mAfterScrollAction) { 657 case FOCUS_PREVIOUS: 658 case FOCUS_NEXT: { 659 if (mFocusedNode.equals(sourceNode)) { 660 break; 661 } 662 AccessibilityNodeInfo target = Navigator.findFocusableDescendantInDirection( 663 sourceNode, mFocusedNode, 664 mAfterScrollAction == AfterScrollAction.FOCUS_PREVIOUS 665 ? View.FOCUS_BACKWARD 666 : View.FOCUS_FORWARD); 667 if (target == null) { 668 break; 669 } 670 L.d("Focusing %s after scroll", 671 mAfterScrollAction == AfterScrollAction.FOCUS_PREVIOUS 672 ? "previous" 673 : "next"); 674 if (performFocusAction(target)) { 675 mAfterScrollAction = AfterScrollAction.NONE; 676 } 677 Utils.recycleNode(target); 678 break; 679 } 680 case FOCUS_FIRST: 681 case FOCUS_LAST: { 682 AccessibilityNodeInfo target = 683 mAfterScrollAction == AfterScrollAction.FOCUS_FIRST 684 ? mNavigator.findFirstFocusableDescendant(sourceNode) 685 : mNavigator.findLastFocusableDescendant(sourceNode); 686 if (target == null) { 687 break; 688 } 689 L.d("Focusing %s after scroll", 690 mAfterScrollAction == AfterScrollAction.FOCUS_FIRST ? "first" : "last"); 691 if (performFocusAction(target)) { 692 mAfterScrollAction = AfterScrollAction.NONE; 693 } 694 Utils.recycleNode(target); 695 break; 696 } 697 default: 698 throw new IllegalStateException( 699 "Unknown after scroll action: " + mAfterScrollAction); 700 } 701 Utils.recycleNode(sourceNode); 702 } 703 704 /** Handles {@link AccessibilityEvent#TYPE_WINDOWS_CHANGED} event. */ handleWindowsChangedEvent(@onNull AccessibilityEvent event)705 private void handleWindowsChangedEvent(@NonNull AccessibilityEvent event) { 706 if ((event.getWindowChanges() & WINDOWS_CHANGE_REMOVED) != 0 707 && mInRotaryMode 708 && mFocusedNode != null 709 && mFocusedNode.getWindowId() == event.getWindowId()) { 710 // The window containing the focused node is gone. Restore focus to the last 711 // focused node in the last focused window. 712 setFocusedNode(null); 713 AccessibilityNodeInfo newFocus = mNavigator.getMostRecentFocus(); 714 if (newFocus != null) { 715 performFocusAction(newFocus); 716 newFocus.recycle(); 717 } 718 } 719 } 720 getKeyCode(KeyEvent event)721 private static int getKeyCode(KeyEvent event) { 722 int keyCode = event.getKeyCode(); 723 if (Build.IS_DEBUGGABLE) { 724 Integer mappingKeyCode = TEST_TO_REAL_KEYCODE_MAP.get(keyCode); 725 if (mappingKeyCode != null) { 726 keyCode = mappingKeyCode; 727 } 728 } 729 return keyCode; 730 } 731 732 /** Handles controller center button event. */ handleCenterButtonEvent(int action, boolean longClick)733 private void handleCenterButtonEvent(int action, boolean longClick) { 734 if (!isValidAction(action)) { 735 return; 736 } 737 if (initFocus()) { 738 return; 739 } 740 // Case 1: the focus is in application window, inject KeyEvent.KEYCODE_DPAD_CENTER event and 741 // the application will handle it. 742 if (isInApplicationWindow(mFocusedNode)) { 743 injectKeyEvent(KeyEvent.KEYCODE_DPAD_CENTER, action); 744 setIgnoreViewClickedNode(mFocusedNode); 745 return; 746 } 747 // We're done with ACTION_DOWN event. 748 if (action == KeyEvent.ACTION_DOWN) { 749 return; 750 } 751 752 // Case 2: the focus is not in application window (e.g., in system window) and the focused 753 // node supports direct manipulation, enter direct manipulation mode. 754 if (DirectManipulationHelper.supportDirectManipulation(mFocusedNode)) { 755 if (!mInDirectManipulationMode) { 756 mInDirectManipulationMode = true; 757 L.d("Enter direct manipulation mode because focused node is clicked."); 758 } 759 return; 760 } 761 762 // Case 3: the focus is not in application window and the focused node doesn't support 763 // direct manipulation, perform click or long click on the focused node. 764 boolean result = mFocusedNode.performAction( 765 longClick 766 ? AccessibilityNodeInfo.ACTION_LONG_CLICK 767 : AccessibilityNodeInfo.ACTION_CLICK); 768 if (!result) { 769 L.w("Failed to perform " + (longClick ? "ACTION_LONG_CLICK" : "ACTION_CLICK") 770 + " on " + mFocusedNode); 771 } 772 if (!longClick) { 773 setIgnoreViewClickedNode(mFocusedNode); 774 } 775 } 776 handleNudgeEvent(int direction, int action)777 private void handleNudgeEvent(int direction, int action) { 778 if (!isValidAction(action)) { 779 return; 780 } 781 if (initFocus()) { 782 return; 783 } 784 785 // If the focused node is in direct manipulation mode, manipulate it directly. 786 if (mInDirectManipulationMode) { 787 if (isInApplicationWindow(mFocusedNode)) { 788 injectKeyEventForDirection(direction, action); 789 } else { 790 L.d("Ignore nudge events because we're in DM mode and the focus is not in" 791 + " application window"); 792 } 793 return; 794 } 795 796 // We're done with ACTION_UP event. 797 if (action == KeyEvent.ACTION_UP) { 798 return; 799 } 800 801 // If the focused node is not in direct manipulation mode, move the focus. 802 // TODO(b/152438801): sometimes getWindows() takes 10s after boot. 803 List<AccessibilityWindowInfo> windows = getWindows(); 804 AccessibilityNodeInfo targetNode = 805 mNavigator.findNudgeTarget(windows, mFocusedNode, direction); 806 Utils.recycleWindows(windows); 807 if (targetNode == null) { 808 L.w("Failed to find nudge target"); 809 return; 810 } 811 812 // Android doesn't clear focus automatically when focus is set in another window. 813 maybeClearFocusInCurrentWindow(targetNode); 814 815 performFocusAction(targetNode); 816 Utils.recycleNode(targetNode); 817 } 818 handleRotaryEvent(RotaryEvent rotaryEvent)819 private void handleRotaryEvent(RotaryEvent rotaryEvent) { 820 if (rotaryEvent.getInputType() != CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION) { 821 return; 822 } 823 boolean clockwise = rotaryEvent.isClockwise(); 824 int count = rotaryEvent.getNumberOfClicks(); 825 // TODO(b/153195148): Use the first eventTime for now. We'll need to improve it later. 826 long eventTime = rotaryEvent.getUptimeMillisForClick(0); 827 handleRotateEvent(clockwise, count, eventTime); 828 } 829 handleRotateEvent(boolean clockwise, int count, long eventTime)830 private void handleRotateEvent(boolean clockwise, int count, long eventTime) { 831 // Clear focus area history if configured to do so, but not when rotating in the HUN. The 832 // HUN overlaps the application window so it's common for focus areas to overlap, causing 833 // geometric searches to fail. History is essential here. 834 if (mClearFocusAreaHistoryWhenRotating && !isFocusInHunWindow()) { 835 mNavigator.clearFocusAreaHistory(); 836 } 837 if (initFocus()) { 838 return; 839 } 840 841 int rotationCount = getRotateAcceleration(count, eventTime); 842 843 // If a scrollable container is focused, no focusable descendants are visible, so scroll the 844 // container. 845 AccessibilityNodeInfo.AccessibilityAction scrollAction = 846 clockwise ? ACTION_SCROLL_FORWARD : ACTION_SCROLL_BACKWARD; 847 if (mFocusedNode != null && Utils.isScrollableContainer(mFocusedNode) 848 && mFocusedNode.getActionList().contains(scrollAction)) { 849 injectScrollEvent(mFocusedNode, clockwise, rotationCount); 850 return; 851 } 852 853 // If the focused node is in direct manipulation mode, manipulate it directly. 854 if (mInDirectManipulationMode) { 855 if (isInApplicationWindow(mFocusedNode)) { 856 AccessibilityWindowInfo window = mFocusedNode.getWindow(); 857 if (window == null) { 858 L.w("Failed to get window of " + mFocusedNode); 859 return; 860 } 861 int displayId = window.getDisplayId(); 862 window.recycle(); 863 // TODO(b/155823126): Add config to let OEMs determine the mapping. 864 injectMotionEvent(displayId, MotionEvent.AXIS_SCROLL, 865 clockwise ? rotationCount : -rotationCount); 866 } else { 867 performScrollAction(mFocusedNode, clockwise); 868 } 869 return; 870 } 871 872 // If the focused node is not in direct manipulation mode, move the focus. Skip over 873 // mScrollableContainer; we don't want to navigate from a focusable descendant to the 874 // scrollable container except as a side-effect of scrolling. 875 int remainingRotationCount = rotationCount; 876 int direction = clockwise ? View.FOCUS_FORWARD : View.FOCUS_BACKWARD; 877 Navigator.FindRotateTargetResult result = mNavigator.findRotateTarget(mFocusedNode, 878 /* skipNode= */ mScrollableContainer, direction, rotationCount); 879 if (result != null) { 880 if (performFocusAction(result.node)) { 881 remainingRotationCount -= result.advancedCount; 882 } 883 Utils.recycleNode(result.node); 884 } else { 885 L.w("Failed to find rotate target"); 886 } 887 888 // If navigation didn't consume all of rotationCount and the focused node either is a 889 // scrollable container or is a descendant of one, scroll it. The former happens when no 890 // focusable views are visible in the scrollable container. The latter happens when there 891 // are focusable views but they're in the wrong direction. Inject a MotionEvent rather than 892 // performing an action so that the application can control the amount it scrolls. Scrolling 893 // is only supported in the application window because injected events always go to the 894 // application window. We don't bother checking whether the scrollable container can 895 // currently scroll because there's nothing else to do if it can't. 896 if (remainingRotationCount > 0 && isInApplicationWindow(mFocusedNode) 897 && mScrollableContainer != null) { 898 injectScrollEvent(mScrollableContainer, clockwise, remainingRotationCount); 899 } 900 } 901 902 /** Handles Back button event. */ handleBackButtonEvent(int action)903 private void handleBackButtonEvent(int action) { 904 if (!isValidAction(action)) { 905 return; 906 } 907 908 // If the focus is in application window, inject Back button event and the application will 909 // handle it. If the focus is not in application window, exit direct manipulation mode on 910 // key up. 911 if (isInApplicationWindow(mFocusedNode)) { 912 injectKeyEvent(KeyEvent.KEYCODE_BACK, action); 913 } else if (action == KeyEvent.ACTION_UP) { 914 L.d("Exit direct manipulation mode on back button event"); 915 mInDirectManipulationMode = false; 916 } 917 } 918 onForegroundAppChanged(CharSequence packageName)919 private void onForegroundAppChanged(CharSequence packageName) { 920 if (TextUtils.equals(mForegroundApp, packageName)) { 921 return; 922 } 923 mForegroundApp = packageName; 924 if (mInDirectManipulationMode) { 925 L.d("Exit direct manipulation mode because the foreground app has changed"); 926 mInDirectManipulationMode = false; 927 } 928 } 929 isValidAction(int action)930 private static boolean isValidAction(int action) { 931 if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) { 932 L.w("Invalid action " + action); 933 return false; 934 } 935 return true; 936 } 937 938 /** Performs scroll action on the given {@code targetNode} if it supports scroll action. */ performScrollAction(@onNull AccessibilityNodeInfo targetNode, boolean clockwise)939 private static void performScrollAction(@NonNull AccessibilityNodeInfo targetNode, 940 boolean clockwise) { 941 // TODO(b/155823126): Add config to let OEMs determine the mapping. 942 int actionToPerform = clockwise 943 ? AccessibilityNodeInfo.ACTION_SCROLL_FORWARD 944 : AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD; 945 int supportedActions = targetNode.getActions(); 946 if ((actionToPerform & supportedActions) == 0) { 947 L.w("Node " + targetNode + " doesn't support action " + actionToPerform); 948 return; 949 } 950 boolean result = targetNode.performAction(actionToPerform); 951 if (!result) { 952 L.w("Failed to perform action " + actionToPerform + " on " + targetNode); 953 } 954 } 955 956 /** Returns whether the given {@code node} is in the application window. */ isInApplicationWindow(@onNull AccessibilityNodeInfo node)957 private static boolean isInApplicationWindow(@NonNull AccessibilityNodeInfo node) { 958 if (TREAT_APP_WINDOW_AS_SYSTEM_WINDOW) { 959 return false; 960 } 961 AccessibilityWindowInfo window = node.getWindow(); 962 if (window == null) { 963 L.w("Failed to get window of " + node); 964 return false; 965 } 966 boolean result = window.getType() == AccessibilityWindowInfo.TYPE_APPLICATION; 967 Utils.recycleWindow(window); 968 return result; 969 } 970 971 /** Returns whether {@link #mFocusedNode} is in the HUN window. */ isFocusInHunWindow()972 private boolean isFocusInHunWindow() { 973 if (mFocusedNode == null) { 974 return false; 975 } 976 AccessibilityWindowInfo window = mFocusedNode.getWindow(); 977 if (window == null) { 978 L.w("Failed to get window of " + mFocusedNode); 979 return false; 980 } 981 boolean result = mNavigator.isHunWindow(window); 982 Utils.recycleWindow(window); 983 return result; 984 } 985 updateDirectManipulationMode(AccessibilityEvent event, boolean enable)986 private void updateDirectManipulationMode(AccessibilityEvent event, boolean enable) { 987 if (!mInRotaryMode || !DirectManipulationHelper.isDirectManipulation(event)) { 988 return; 989 } 990 if (enable) { 991 mFocusedNode = Utils.refreshNode(mFocusedNode); 992 if (mFocusedNode == null) { 993 L.w("Failed to enter direct manipulation mode because mFocusedNode is no longer " 994 + "in view tree."); 995 return; 996 } 997 if (!mFocusedNode.isFocused()) { 998 L.w("Failed to enter direct manipulation mode because mFocusedNode is no longer " 999 + "focused."); 1000 return; 1001 } 1002 } 1003 if (mInDirectManipulationMode != enable) { 1004 // Toggle direct manipulation mode upon app's request. 1005 mInDirectManipulationMode = enable; 1006 L.d((enable ? "Enter" : "Exit") + " direct manipulation mode upon app's request"); 1007 } 1008 } 1009 1010 /** 1011 * Injects a {@link MotionEvent} to scroll {@code scrollableContainer} by {@code rotationCount} 1012 * steps. The direction depends on the value of {@code clockwise}. Sets 1013 * {@link #mAfterScrollAction} to move the focus once the scroll occurs, as follows:<ul> 1014 * <li>If the user is spinning the rotary controller quickly, focuses the first or last 1015 * focusable descendant so that the next rotation event will scroll immediately. 1016 * <li>If the user is spinning slowly and there are no focusable descendants visible, 1017 * focuses the first focusable descendant to scroll into view. This will be the last 1018 * focusable descendant when scrolling up. 1019 * <li>If the user is spinning slowly and there are focusable descendants visible, focuses 1020 * the next or previous focusable descendant. 1021 * </ul> 1022 */ injectScrollEvent(@onNull AccessibilityNodeInfo scrollableContainer, boolean clockwise, int rotationCount)1023 private void injectScrollEvent(@NonNull AccessibilityNodeInfo scrollableContainer, 1024 boolean clockwise, int rotationCount) { 1025 // TODO(b/155823126): Add config to let OEMs determine the mappings. 1026 if (rotationCount > 1) { 1027 // Focus last when quickly scrolling down so the next event scrolls. 1028 mAfterScrollAction = clockwise 1029 ? AfterScrollAction.FOCUS_LAST 1030 : AfterScrollAction.FOCUS_FIRST; 1031 } else { 1032 if (Utils.isScrollableContainer(mFocusedNode)) { 1033 // Focus first when scrolling down while no focusable descendants are visible. 1034 mAfterScrollAction = clockwise 1035 ? AfterScrollAction.FOCUS_FIRST 1036 : AfterScrollAction.FOCUS_LAST; 1037 } else { 1038 // Focus next when scrolling down with a focused descendant. 1039 mAfterScrollAction = clockwise 1040 ? AfterScrollAction.FOCUS_NEXT 1041 : AfterScrollAction.FOCUS_PREVIOUS; 1042 } 1043 } 1044 mAfterScrollActionUntil = SystemClock.uptimeMillis() + mAfterScrollTimeoutMs; 1045 int axis = Utils.isHorizontallyScrollableContainer(scrollableContainer) 1046 ? MotionEvent.AXIS_HSCROLL 1047 : MotionEvent.AXIS_VSCROLL; 1048 AccessibilityWindowInfo window = scrollableContainer.getWindow(); 1049 if (window == null) { 1050 L.w("Failed to get window of " + scrollableContainer); 1051 return; 1052 } 1053 int displayId = window.getDisplayId(); 1054 window.recycle(); 1055 injectMotionEvent(displayId, axis, clockwise ? -rotationCount : rotationCount); 1056 } 1057 injectMotionEvent(int displayId, int axis, int axisValue)1058 private void injectMotionEvent(int displayId, int axis, int axisValue) { 1059 long upTime = SystemClock.uptimeMillis(); 1060 MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[1]; 1061 properties[0] = new MotionEvent.PointerProperties(); 1062 properties[0].id = 0; // Any integer value but -1 (INVALID_POINTER_ID) is fine. 1063 MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[1]; 1064 coords[0] = new MotionEvent.PointerCoords(); 1065 // No need to set X,Y coordinates. We use a non-pointer source so the event will be routed 1066 // to the focused view. 1067 coords[0].setAxisValue(axis, axisValue); 1068 MotionEvent motionEvent = MotionEvent.obtain(/* downTime= */ upTime, 1069 /* eventTime= */ upTime, 1070 MotionEvent.ACTION_SCROLL, 1071 /* pointerCount= */ 1, 1072 properties, 1073 coords, 1074 /* metaState= */ 0, 1075 /* buttonState= */ 0, 1076 /* xPrecision= */ 1.0f, 1077 /* yPrecision= */ 1.0f, 1078 /* deviceId= */ 0, 1079 /* edgeFlags= */ 0, 1080 InputDevice.SOURCE_ROTARY_ENCODER, 1081 displayId, 1082 /* flags= */ 0); 1083 1084 if (motionEvent != null) { 1085 mInputManager.injectInputEvent(motionEvent, 1086 InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); 1087 } else { 1088 L.w("Unable to obtain MotionEvent"); 1089 } 1090 } 1091 injectKeyEventForDirection(int direction, int action)1092 private boolean injectKeyEventForDirection(int direction, int action) { 1093 Integer keyCode = DIRECTION_TO_KEYCODE_MAP.get(direction); 1094 if (keyCode == null) { 1095 throw new IllegalArgumentException("direction must be one of " 1096 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 1097 } 1098 return injectKeyEvent(keyCode, action); 1099 } 1100 injectKeyEvent(int keyCode, int action)1101 private boolean injectKeyEvent(int keyCode, int action) { 1102 long upTime = SystemClock.uptimeMillis(); 1103 KeyEvent keyEvent = new KeyEvent( 1104 /* downTime= */ upTime, /* eventTime= */ upTime, action, keyCode, /* repeat= */ 0); 1105 return mInputManager.injectInputEvent(keyEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); 1106 } 1107 1108 /** 1109 * Updates {@link #mFocusedNode} and {@link #mLastTouchedNode} in case the {@link View}s 1110 * represented by them are no longer in the view tree. 1111 */ refreshSavedNodes()1112 private void refreshSavedNodes() { 1113 mFocusedNode = Utils.refreshNode(mFocusedNode); 1114 mLastTouchedNode = Utils.refreshNode(mLastTouchedNode); 1115 mScrollableContainer = Utils.refreshNode(mScrollableContainer); 1116 mPreviousFocusedNode = Utils.refreshNode(mPreviousFocusedNode); 1117 } 1118 1119 /** 1120 * This method should be called when receiving an event from a rotary controller. It does the 1121 * following:<ol> 1122 * <li>If {@link #mFocusedNode} isn't null and represents a view that still exists, does 1123 * nothing. The event isn't consumed in this case. This is the normal case. 1124 * <li>If {@link #mScrollableContainer} isn't null and represents a view that still exists, 1125 * focuses it. The event isn't consumed in this case. This can happen when the user 1126 * rotates quickly as they scroll into a section without any focusable views. 1127 * <li>If {@link #mLastTouchedNode} isn't null and represents a view that still exists, 1128 * focuses it. The event is consumed in this case. This happens when the user switches 1129 * from touch to rotary. 1130 * </ol> 1131 * 1132 * @return whether the event was consumed by this method. When {@code false}, 1133 * {@link #mFocusedNode} is guaranteed to not be {@code null}. 1134 */ initFocus()1135 private boolean initFocus() { 1136 refreshSavedNodes(); 1137 mInRotaryMode = true; 1138 if (mFocusedNode != null) { 1139 return false; 1140 } 1141 if (mScrollableContainer != null) { 1142 if (performFocusAction(mScrollableContainer)) { 1143 return false; 1144 } 1145 } 1146 if (mLastTouchedNode != null) { 1147 if (focusLastTouchedNode()) { 1148 return true; 1149 } 1150 } 1151 focusFirstFocusDescendant(); 1152 return true; 1153 } 1154 1155 /** Clears the current rotary focus if {@code targetFocus} is in a different window. */ maybeClearFocusInCurrentWindow(@onNull AccessibilityNodeInfo targetFocus)1156 private void maybeClearFocusInCurrentWindow(@NonNull AccessibilityNodeInfo targetFocus) { 1157 if (mFocusedNode == null || !mFocusedNode.isFocused() 1158 || mFocusedNode.getWindowId() == targetFocus.getWindowId()) { 1159 return; 1160 } 1161 if (clearFocusInCurrentWindow()) { 1162 setFocusedNode(null); 1163 } 1164 } 1165 1166 /** 1167 * Clears the current rotary focus. 1168 * <p> 1169 * If we really clear focus in the current window, Android will re-focus a view in the current 1170 * window automatically, resulting in the current window and the target window being focused 1171 * simultaneously. To avoid that we don't really clear the focus. Instead, we "park" the focus 1172 * on a FocusParkingView in the current window. FocusParkingView is transparent no matter 1173 * whether it's focused or not, so it's invisible to the user. 1174 * 1175 * @return whether the FocusParkingView was focused successfully 1176 */ clearFocusInCurrentWindow()1177 private boolean clearFocusInCurrentWindow() { 1178 if (mFocusedNode == null) { 1179 L.e("Don't call clearFocusInCurrentWindow() when mFocusedNode is null"); 1180 return false; 1181 } 1182 AccessibilityWindowInfo window = mFocusedNode.getWindow(); 1183 if (window == null) { 1184 L.w("Failed to get window of " + mFocusedNode); 1185 return false; 1186 } 1187 AccessibilityNodeInfo focusParkingView = mNavigator.findFocusParkingView(window); 1188 if (focusParkingView == null) { 1189 L.e("No FocusParkingView in " + window); 1190 window.recycle(); 1191 return false; 1192 } 1193 window.recycle(); 1194 boolean result = focusParkingView.performAction(AccessibilityNodeInfo.ACTION_FOCUS); 1195 if (result) { 1196 if (mFocusParkingView != null) { 1197 L.e("mFocusParkingView should be null but is " + mFocusParkingView); 1198 Utils.recycleNode(mFocusParkingView); 1199 } 1200 mFocusParkingView = copyNode(focusParkingView); 1201 } else { 1202 L.w("Failed to perform ACTION_FOCUS on " + focusParkingView); 1203 } 1204 focusParkingView.recycle(); 1205 return result; 1206 } 1207 1208 /** 1209 * Focuses the last touched node, if any. 1210 * 1211 * @return {@code true} if {@link #mLastTouchedNode} isn't {@code null} and it was 1212 * successfully focused 1213 */ focusLastTouchedNode()1214 private boolean focusLastTouchedNode() { 1215 boolean lastTouchedNodeFocused = false; 1216 if (mLastTouchedNode != null) { 1217 lastTouchedNodeFocused = performFocusAction(mLastTouchedNode); 1218 if (mLastTouchedNode != null) { 1219 setLastTouchedNode(null); 1220 } 1221 } 1222 return lastTouchedNodeFocused; 1223 } 1224 1225 /** 1226 * Focuses the first focus descendant (a node inside a focus area that can take focus) in the 1227 * currently active window, if any. 1228 */ focusFirstFocusDescendant()1229 private void focusFirstFocusDescendant() { 1230 AccessibilityNodeInfo rootNode = getRootInActiveWindow(); 1231 if (rootNode == null) { 1232 L.e("rootNode of active window is null"); 1233 return; 1234 } 1235 AccessibilityNodeInfo targetNode = mNavigator.findFirstFocusDescendant(rootNode); 1236 rootNode.recycle(); 1237 if (targetNode == null) { 1238 L.w("Failed to find the first focus descendant"); 1239 return; 1240 } 1241 performFocusAction(targetNode); 1242 targetNode.recycle(); 1243 } 1244 1245 /** 1246 * Sets {@link #mFocusedNode} to a copy of the given node, and clears {@link #mLastTouchedNode}. 1247 */ setFocusedNode(@ullable AccessibilityNodeInfo focusedNode)1248 private void setFocusedNode(@Nullable AccessibilityNodeInfo focusedNode) { 1249 setFocusedNodeInternal(focusedNode); 1250 if (mFocusedNode != null && mLastTouchedNode != null) { 1251 setLastTouchedNodeInternal(null); 1252 } 1253 } 1254 setFocusedNodeInternal(@ullable AccessibilityNodeInfo focusedNode)1255 private void setFocusedNodeInternal(@Nullable AccessibilityNodeInfo focusedNode) { 1256 if ((mFocusedNode == null && focusedNode == null) || 1257 (mFocusedNode != null && mFocusedNode.equals(focusedNode))) { 1258 L.d("Don't reset mFocusedNode since it stays the same: " + mFocusedNode); 1259 return; 1260 } 1261 if (mInDirectManipulationMode && focusedNode == null) { 1262 // Toggle off direct manipulation mode since there is no focused node. 1263 mInDirectManipulationMode = false; 1264 L.d("Exit direct manipulation mode since there is no focused node"); 1265 } 1266 1267 // Recycle mPreviousFocusedNode only when it's not the same with focusedNode. 1268 if (mPreviousFocusedNode != focusedNode) { 1269 Utils.recycleNode(mPreviousFocusedNode); 1270 } else { 1271 // TODO(b/159949186) 1272 L.e("mPreviousFocusedNode shouldn't be the same with focusedNode " + focusedNode); 1273 } 1274 1275 mPreviousFocusedNode = mFocusedNode; 1276 mFocusedNode = copyNode(focusedNode); 1277 1278 // Set mScrollableContainer to the scrollable container which contains mFocusedNode, if any. 1279 // Skip if mFocusedNode is a FocusParkingView. The FocusParkingView is focused when the 1280 // focus view is scrolled off the screen. We'll focus the scrollable container when we 1281 // receive the TYPE_VIEW_FOCUSED event in this case. 1282 if (mFocusedNode == null) { 1283 setScrollableContainer(null); 1284 } else if (!Utils.isFocusParkingView(mFocusedNode)) { 1285 setScrollableContainer(mNavigator.findScrollableContainer(mFocusedNode)); 1286 } 1287 1288 // Cache the focused node by focus area. 1289 if (mFocusedNode != null) { 1290 mNavigator.saveFocusedNode(mFocusedNode); 1291 } 1292 } 1293 setScrollableContainer(@ullable AccessibilityNodeInfo scrollableContainer)1294 private void setScrollableContainer(@Nullable AccessibilityNodeInfo scrollableContainer) { 1295 if ((mScrollableContainer == null && scrollableContainer == null) 1296 || (mScrollableContainer != null 1297 && mScrollableContainer.equals(scrollableContainer))) { 1298 return; 1299 } 1300 1301 Utils.recycleNode(mScrollableContainer); 1302 mScrollableContainer = copyNode(scrollableContainer); 1303 } 1304 1305 /** 1306 * Sets {@link #mLastTouchedNode} to a copy of the given node, and clears {@link #mFocusedNode}. 1307 */ setLastTouchedNode(@ullable AccessibilityNodeInfo lastTouchedNode)1308 private void setLastTouchedNode(@Nullable AccessibilityNodeInfo lastTouchedNode) { 1309 setLastTouchedNodeInternal(lastTouchedNode); 1310 if (mLastTouchedNode != null && mFocusedNode != null) { 1311 setFocusedNodeInternal(null); 1312 } 1313 } 1314 setLastTouchedNodeInternal(@ullable AccessibilityNodeInfo lastTouchedNode)1315 private void setLastTouchedNodeInternal(@Nullable AccessibilityNodeInfo lastTouchedNode) { 1316 if ((mLastTouchedNode == null && lastTouchedNode == null) 1317 || (mLastTouchedNode != null && mLastTouchedNode.equals(lastTouchedNode))) { 1318 L.d("Don't reset mLastTouchedNode since it stays the same: " + mLastTouchedNode); 1319 return; 1320 } 1321 1322 Utils.recycleNode(mLastTouchedNode); 1323 mLastTouchedNode = copyNode(lastTouchedNode); 1324 } 1325 setIgnoreViewClickedNode(@ullable AccessibilityNodeInfo node)1326 private void setIgnoreViewClickedNode(@Nullable AccessibilityNodeInfo node) { 1327 if (mIgnoreViewClickedNode != null) { 1328 mIgnoreViewClickedNode.recycle(); 1329 } 1330 mIgnoreViewClickedNode = copyNode(node); 1331 if (node != null) { 1332 mIgnoreViewClickedUntil = SystemClock.uptimeMillis() + mIgnoreViewClickedMs; 1333 } 1334 } 1335 1336 /** 1337 * Performs {@link AccessibilityNodeInfo#ACTION_FOCUS} on the given {@code targetNode}. 1338 * 1339 * @return true if {@code targetNode} was focused already or became focused after performing 1340 * {@link AccessibilityNodeInfo#ACTION_FOCUS} 1341 */ performFocusAction(@onNull AccessibilityNodeInfo targetNode)1342 private boolean performFocusAction(@NonNull AccessibilityNodeInfo targetNode) { 1343 if (targetNode.equals(mFocusedNode)) { 1344 return true; 1345 } 1346 if (targetNode.isFocused()) { 1347 L.w("targetNode is already focused: " + targetNode); 1348 setFocusedNode(targetNode); 1349 return true; 1350 } 1351 boolean focusCleared = false; 1352 if (Utils.hasFocus(targetNode)){ 1353 // One of targetNode's descendants is already focused, so we can't perform ACTION_FOCUS 1354 // on targetNode directly. The workaround is to clear the focus first (by focusing on 1355 // the FocusParkingView), then focus on targetNode. 1356 L.d("One of targetNode's descendants is already focused: " + targetNode); 1357 if (!clearFocusInCurrentWindow()) { 1358 return false; 1359 } 1360 focusCleared = true; 1361 } 1362 // Now we can perform ACTION_FOCUS on targetNode since it doesn't have focus, or its 1363 // descendant's focus has been cleared. 1364 boolean result = targetNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS); 1365 if (!result) { 1366 L.w("Failed to perform ACTION_FOCUS on node " + targetNode); 1367 // Previously we cleared the focus of targetNode's descendant, which won't reset the 1368 // focused node to null. So we need to reset it manually. 1369 if (focusCleared) { 1370 setFocusedNode(null); 1371 } 1372 return false; 1373 } 1374 1375 setFocusedNode(targetNode); 1376 return true; 1377 } 1378 1379 /** 1380 * Returns the number of "ticks" to rotate for a single rotate event with the given detent 1381 * {@code count} at the given time. Uses and updates {@link #mLastRotateEventTime}. The result 1382 * will be one, two, or three times the given detent {@code count} depending on the interval 1383 * between the current event and the previous event and the detent {@code count}. 1384 * 1385 * @param count the number of detents the user rotated 1386 * @param eventTime the {@link SystemClock#uptimeMillis} when the event occurred 1387 * @return the number of "ticks" to rotate 1388 */ getRotateAcceleration(int count, long eventTime)1389 private int getRotateAcceleration(int count, long eventTime) { 1390 // count is 0 when testing key "C" or "V" is pressed. 1391 if (count <= 0) { 1392 count = 1; 1393 } 1394 int result = count; 1395 // TODO(b/153195148): This method can be improved once we've plumbed through the VHAL 1396 // changes. We'll get timestamps for each detent. 1397 long delta = (eventTime - mLastRotateEventTime) / count; // Assume constant speed. 1398 if (delta <= mRotationAcceleration3xMs) { 1399 result = count * 3; 1400 } else if (delta <= mRotationAcceleration2xMs) { 1401 result = count * 2; 1402 } 1403 mLastRotateEventTime = eventTime; 1404 return result; 1405 } 1406 copyNode(@ullable AccessibilityNodeInfo node)1407 private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) { 1408 return mNodeCopier.copy(node); 1409 } 1410 1411 /** 1412 * Moves focus from the given {@code focusParkingView} to a view near the previously focused 1413 * view, which is chosen in the following order: 1414 * <ol> 1415 * <li> the previously focused view ({@link #mPreviousFocusedNode}), if any 1416 * <li> the default focus (app:defaultFocus) in the FocusArea that contains {@link 1417 * #mFocusedNode}, if any 1418 * <li> the first focusable view in the FocusArea that contains {@link #mFocusedNode}, if any, 1419 * excluding any FocusParkingViews 1420 * <li> the default focus in the window, if any, excluding any FocusParkingViews 1421 * <li> the first focusable view in the window, if any, excluding any FocusParkingViews 1422 * </ol> 1423 */ moveFocusToNearbyView(@onNull AccessibilityNodeInfo focusParkingView)1424 private void moveFocusToNearbyView(@NonNull AccessibilityNodeInfo focusParkingView) { 1425 mPreviousFocusedNode = Utils.refreshNode(mPreviousFocusedNode); 1426 if (mPreviousFocusedNode != null && performFocusAction(mPreviousFocusedNode)) { 1427 L.d("Move focus to the previously focused node"); 1428 return; 1429 } 1430 // TODO(b/158797952): do 2,3. 1431 if (focusParkingView.performAction(AccessibilityNodeInfo.ACTION_DISMISS)) { 1432 L.d("Move focus to the default focus in the window"); 1433 return; 1434 } 1435 L.d("Try to focus on the first focusable view in the window"); 1436 focusFirstFocusDescendant(); 1437 } 1438 } 1439