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