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