1 /* 2 ** Copyright 2011, 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 17 package android.view.accessibility; 18 19 import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_INTERACTION_CLIENT; 20 import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK; 21 22 import android.accessibilityservice.IAccessibilityServiceConnection; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.compat.annotation.UnsupportedAppUsage; 26 import android.content.Context; 27 import android.os.Binder; 28 import android.os.Build; 29 import android.os.Bundle; 30 import android.os.IBinder; 31 import android.os.Message; 32 import android.os.Process; 33 import android.os.RemoteException; 34 import android.os.SystemClock; 35 import android.util.Log; 36 import android.util.LongSparseArray; 37 import android.util.SparseArray; 38 import android.util.SparseLongArray; 39 import android.view.Display; 40 import android.view.ViewConfiguration; 41 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.android.internal.util.ArrayUtils; 44 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.Collections; 48 import java.util.HashSet; 49 import java.util.LinkedList; 50 import java.util.List; 51 import java.util.Queue; 52 import java.util.concurrent.atomic.AtomicInteger; 53 54 /** 55 * This class is a singleton that performs accessibility interaction 56 * which is it queries remote view hierarchies about snapshots of their 57 * views as well requests from these hierarchies to perform certain 58 * actions on their views. 59 * 60 * Rationale: The content retrieval APIs are synchronous from a client's 61 * perspective but internally they are asynchronous. The client thread 62 * calls into the system requesting an action and providing a callback 63 * to receive the result after which it waits up to a timeout for that 64 * result. The system enforces security and the delegates the request 65 * to a given view hierarchy where a message is posted (from a binder 66 * thread) describing what to be performed by the main UI thread the 67 * result of which it delivered via the mentioned callback. However, 68 * the blocked client thread and the main UI thread of the target view 69 * hierarchy can be the same thread, for example an accessibility service 70 * and an activity run in the same process, thus they are executed on the 71 * same main thread. In such a case the retrieval will fail since the UI 72 * thread that has to process the message describing the work to be done 73 * is blocked waiting for a result is has to compute! To avoid this scenario 74 * when making a call the client also passes its process and thread ids so 75 * the accessed view hierarchy can detect if the client making the request 76 * is running in its main UI thread. In such a case the view hierarchy, 77 * specifically the binder thread performing the IPC to it, does not post a 78 * message to be run on the UI thread but passes it to the singleton 79 * interaction client through which all interactions occur and the latter is 80 * responsible to execute the message before starting to wait for the 81 * asynchronous result delivered via the callback. In this case the expected 82 * result is already received so no waiting is performed. 83 * 84 * @hide 85 */ 86 public final class AccessibilityInteractionClient 87 extends IAccessibilityInteractionConnectionCallback.Stub { 88 89 public static final int NO_ID = -1; 90 91 public static final String CALL_STACK = "call_stack"; 92 public static final String IGNORE_CALL_STACK = "ignore_call_stack"; 93 94 private static final String LOG_TAG = "AccessibilityInteractionClient"; 95 96 private static final boolean DEBUG = false; 97 98 private static final boolean CHECK_INTEGRITY = true; 99 100 private static final long TIMEOUT_INTERACTION_MILLIS = 5000; 101 102 private static final long DISABLE_PREFETCHING_FOR_SCROLLING_MILLIS = 103 (long) (ViewConfiguration.getSendRecurringAccessibilityEventsInterval() * 1.5); 104 105 private static final Object sStaticLock = new Object(); 106 107 private static final LongSparseArray<AccessibilityInteractionClient> sClients = 108 new LongSparseArray<>(); 109 110 private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache = 111 new SparseArray<>(); 112 113 /** List of timestamps which indicate the latest time an a11y service receives a scroll event 114 from a window, mapping from windowId -> timestamp. */ 115 private static final SparseLongArray sScrollingWindows = new SparseLongArray(); 116 117 private static AccessibilityCache sAccessibilityCache = 118 new AccessibilityCache(new AccessibilityCache.AccessibilityNodeRefresher()); 119 120 private final AtomicInteger mInteractionIdCounter = new AtomicInteger(); 121 122 private final Object mInstanceLock = new Object(); 123 124 private final AccessibilityManager mAccessibilityManager; 125 126 private volatile int mInteractionId = -1; 127 private volatile int mCallingUid = Process.INVALID_UID; 128 // call stack for IAccessibilityInteractionConnectionCallback APIs. These callback APIs are 129 // shared by multiple requests APIs in IAccessibilityServiceConnection. To correctly log the 130 // request API which triggers the callback, we log trace entries for callback after the 131 // request API thread waiting for the callback returns. To log the correct callback stack in 132 // the request API thread, we save the callback stack in this member variables. 133 private List<StackTraceElement> mCallStackOfCallback; 134 135 private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult; 136 137 private List<AccessibilityNodeInfo> mFindAccessibilityNodeInfosResult; 138 139 private boolean mPerformAccessibilityActionResult; 140 141 private Message mSameThreadMessage; 142 143 private int mInteractionIdWaitingForPrefetchResult = -1; 144 private int mConnectionIdWaitingForPrefetchResult; 145 private String[] mPackageNamesForNextPrefetchResult; 146 147 /** 148 * @return The client for the current thread. 149 */ 150 @UnsupportedAppUsage() getInstance()151 public static AccessibilityInteractionClient getInstance() { 152 final long threadId = Thread.currentThread().getId(); 153 return getInstanceForThread(threadId); 154 } 155 156 /** 157 * <strong>Note:</strong> We keep one instance per interrogating thread since 158 * the instance contains state which can lead to undesired thread interleavings. 159 * We do not have a thread local variable since other threads should be able to 160 * look up the correct client knowing a thread id. See ViewRootImpl for details. 161 * 162 * @return The client for a given <code>threadId</code>. 163 */ getInstanceForThread(long threadId)164 public static AccessibilityInteractionClient getInstanceForThread(long threadId) { 165 synchronized (sStaticLock) { 166 AccessibilityInteractionClient client = sClients.get(threadId); 167 if (client == null) { 168 client = new AccessibilityInteractionClient(); 169 sClients.put(threadId, client); 170 } 171 return client; 172 } 173 } 174 175 /** 176 * @return The client for the current thread. 177 */ getInstance(Context context)178 public static AccessibilityInteractionClient getInstance(Context context) { 179 final long threadId = Thread.currentThread().getId(); 180 if (context != null) { 181 return getInstanceForThread(threadId, context); 182 } 183 return getInstanceForThread(threadId); 184 } 185 186 /** 187 * <strong>Note:</strong> We keep one instance per interrogating thread since 188 * the instance contains state which can lead to undesired thread interleavings. 189 * We do not have a thread local variable since other threads should be able to 190 * look up the correct client knowing a thread id. See ViewRootImpl for details. 191 * 192 * @return The client for a given <code>threadId</code>. 193 */ getInstanceForThread( long threadId, Context context)194 public static AccessibilityInteractionClient getInstanceForThread( 195 long threadId, Context context) { 196 synchronized (sStaticLock) { 197 AccessibilityInteractionClient client = sClients.get(threadId); 198 if (client == null) { 199 client = new AccessibilityInteractionClient(context); 200 sClients.put(threadId, client); 201 } 202 return client; 203 } 204 } 205 206 /** 207 * Gets a cached accessibility service connection. 208 * 209 * @param connectionId The connection id. 210 * @return The cached connection if such. 211 */ getConnection(int connectionId)212 public static IAccessibilityServiceConnection getConnection(int connectionId) { 213 synchronized (sConnectionCache) { 214 return sConnectionCache.get(connectionId); 215 } 216 } 217 218 /** 219 * Adds a cached accessibility service connection. 220 * 221 * @param connectionId The connection id. 222 * @param connection The connection. 223 */ addConnection(int connectionId, IAccessibilityServiceConnection connection)224 public static void addConnection(int connectionId, IAccessibilityServiceConnection connection) { 225 synchronized (sConnectionCache) { 226 sConnectionCache.put(connectionId, connection); 227 } 228 } 229 230 /** 231 * Removes a cached accessibility service connection. 232 * 233 * @param connectionId The connection id. 234 */ removeConnection(int connectionId)235 public static void removeConnection(int connectionId) { 236 synchronized (sConnectionCache) { 237 sConnectionCache.remove(connectionId); 238 } 239 } 240 241 /** 242 * This method is only for testing. Replacing the cache is a generally terrible idea, but 243 * tests need to be able to verify this class's interactions with the cache 244 */ 245 @VisibleForTesting setCache(AccessibilityCache cache)246 public static void setCache(AccessibilityCache cache) { 247 sAccessibilityCache = cache; 248 } 249 AccessibilityInteractionClient()250 private AccessibilityInteractionClient() { 251 /* reducing constructor visibility */ 252 mAccessibilityManager = null; 253 } 254 AccessibilityInteractionClient(Context context)255 private AccessibilityInteractionClient(Context context) { 256 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 257 } 258 259 /** 260 * Sets the message to be processed if the interacted view hierarchy 261 * and the interacting client are running in the same thread. 262 * 263 * @param message The message. 264 */ 265 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) setSameThreadMessage(Message message)266 public void setSameThreadMessage(Message message) { 267 synchronized (mInstanceLock) { 268 mSameThreadMessage = message; 269 mInstanceLock.notifyAll(); 270 } 271 } 272 273 /** 274 * Gets the root {@link AccessibilityNodeInfo} in the currently active window. 275 * 276 * @param connectionId The id of a connection for interacting with the system. 277 * @return The root {@link AccessibilityNodeInfo} if found, null otherwise. 278 */ getRootInActiveWindow(int connectionId)279 public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) { 280 return findAccessibilityNodeInfoByAccessibilityId(connectionId, 281 AccessibilityWindowInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, 282 false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS, null); 283 } 284 285 /** 286 * Gets the info for a window. 287 * 288 * @param connectionId The id of a connection for interacting with the system. 289 * @param accessibilityWindowId A unique window id. Use 290 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 291 * to query the currently active window. 292 * @return The {@link AccessibilityWindowInfo}. 293 */ getWindow(int connectionId, int accessibilityWindowId)294 public AccessibilityWindowInfo getWindow(int connectionId, int accessibilityWindowId) { 295 return getWindow(connectionId, accessibilityWindowId, /* bypassCache */ false); 296 } 297 298 /** 299 * Gets the info for a window. 300 * 301 * @param connectionId The id of a connection for interacting with the system. 302 * @param accessibilityWindowId A unique window id. Use 303 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 304 * to query the currently active window. 305 * @param bypassCache Whether to bypass the cache. 306 * @return The {@link AccessibilityWindowInfo}. 307 */ getWindow(int connectionId, int accessibilityWindowId, boolean bypassCache)308 public AccessibilityWindowInfo getWindow(int connectionId, int accessibilityWindowId, 309 boolean bypassCache) { 310 try { 311 IAccessibilityServiceConnection connection = getConnection(connectionId); 312 if (connection != null) { 313 AccessibilityWindowInfo window; 314 if (!bypassCache) { 315 window = sAccessibilityCache.getWindow(accessibilityWindowId); 316 if (window != null) { 317 if (DEBUG) { 318 Log.i(LOG_TAG, "Window cache hit"); 319 } 320 if (shouldTraceClient()) { 321 logTraceClient(connection, "getWindow cache", 322 "connectionId=" + connectionId + ";accessibilityWindowId=" 323 + accessibilityWindowId + ";bypassCache=false"); 324 } 325 return window; 326 } 327 if (DEBUG) { 328 Log.i(LOG_TAG, "Window cache miss"); 329 } 330 } 331 332 final long identityToken = Binder.clearCallingIdentity(); 333 try { 334 window = connection.getWindow(accessibilityWindowId); 335 } finally { 336 Binder.restoreCallingIdentity(identityToken); 337 } 338 if (shouldTraceClient()) { 339 logTraceClient(connection, "getWindow", "connectionId=" + connectionId 340 + ";accessibilityWindowId=" + accessibilityWindowId + ";bypassCache=" 341 + bypassCache); 342 } 343 344 if (window != null) { 345 if (!bypassCache) { 346 sAccessibilityCache.addWindow(window); 347 } 348 return window; 349 } 350 } else { 351 if (DEBUG) { 352 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 353 } 354 } 355 } catch (RemoteException re) { 356 Log.e(LOG_TAG, "Error while calling remote getWindow", re); 357 } 358 return null; 359 } 360 361 /** 362 * Gets the info for all windows of the default display. 363 * 364 * @param connectionId The id of a connection for interacting with the system. 365 * @return The {@link AccessibilityWindowInfo} list. 366 */ getWindows(int connectionId)367 public List<AccessibilityWindowInfo> getWindows(int connectionId) { 368 final SparseArray<List<AccessibilityWindowInfo>> windows = 369 getWindowsOnAllDisplays(connectionId); 370 if (windows.size() > 0) { 371 return windows.valueAt(Display.DEFAULT_DISPLAY); 372 } 373 return Collections.emptyList(); 374 } 375 376 /** 377 * Gets the info for all windows of all displays. 378 * 379 * @param connectionId The id of a connection for interacting with the system. 380 * @return The SparseArray of {@link AccessibilityWindowInfo} list. 381 * The key of SparseArray is display ID. 382 */ getWindowsOnAllDisplays(int connectionId)383 public SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays(int connectionId) { 384 try { 385 IAccessibilityServiceConnection connection = getConnection(connectionId); 386 if (connection != null) { 387 SparseArray<List<AccessibilityWindowInfo>> windows = 388 sAccessibilityCache.getWindowsOnAllDisplays(); 389 if (windows != null) { 390 if (DEBUG) { 391 Log.i(LOG_TAG, "Windows cache hit"); 392 } 393 if (shouldTraceClient()) { 394 logTraceClient( 395 connection, "getWindows cache", "connectionId=" + connectionId); 396 } 397 return windows; 398 } 399 if (DEBUG) { 400 Log.i(LOG_TAG, "Windows cache miss"); 401 } 402 final long identityToken = Binder.clearCallingIdentity(); 403 try { 404 windows = connection.getWindows(); 405 } finally { 406 Binder.restoreCallingIdentity(identityToken); 407 } 408 if (shouldTraceClient()) { 409 logTraceClient(connection, "getWindows", "connectionId=" + connectionId); 410 } 411 if (windows != null) { 412 sAccessibilityCache.setWindowsOnAllDisplays(windows); 413 return windows; 414 } 415 } else { 416 if (DEBUG) { 417 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 418 } 419 } 420 } catch (RemoteException re) { 421 Log.e(LOG_TAG, "Error while calling remote getWindowsOnAllDisplays", re); 422 } 423 424 final SparseArray<List<AccessibilityWindowInfo>> emptyWindows = new SparseArray<>(); 425 return emptyWindows; 426 } 427 428 429 /** 430 * Finds an {@link AccessibilityNodeInfo} by accessibility id and given leash token instead of 431 * window id. This method is used to find the leashed node on the embedded view hierarchy. 432 * 433 * @param connectionId The id of a connection for interacting with the system. 434 * @param leashToken The token of the embedded hierarchy. 435 * @param accessibilityNodeId A unique view id or virtual descendant id from 436 * where to start the search. Use 437 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 438 * to start from the root. 439 * @param bypassCache Whether to bypass the cache while looking for the node. 440 * @param prefetchFlags flags to guide prefetching. 441 * @param arguments Optional action arguments. 442 * @return An {@link AccessibilityNodeInfo} if found, null otherwise. 443 */ findAccessibilityNodeInfoByAccessibilityId( int connectionId, @NonNull IBinder leashToken, long accessibilityNodeId, boolean bypassCache, int prefetchFlags, Bundle arguments)444 public @Nullable AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId( 445 int connectionId, @NonNull IBinder leashToken, long accessibilityNodeId, 446 boolean bypassCache, int prefetchFlags, Bundle arguments) { 447 if (leashToken == null) { 448 return null; 449 } 450 int windowId = -1; 451 try { 452 IAccessibilityServiceConnection connection = getConnection(connectionId); 453 if (connection != null) { 454 windowId = connection.getWindowIdForLeashToken(leashToken); 455 } else { 456 if (DEBUG) { 457 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 458 } 459 } 460 } catch (RemoteException re) { 461 Log.e(LOG_TAG, "Error while calling remote getWindowIdForLeashToken", re); 462 } 463 if (windowId == -1) { 464 return null; 465 } 466 return findAccessibilityNodeInfoByAccessibilityId(connectionId, windowId, 467 accessibilityNodeId, bypassCache, prefetchFlags, arguments); 468 } 469 470 /** 471 * Finds an {@link AccessibilityNodeInfo} by accessibility id. 472 * 473 * @param connectionId The id of a connection for interacting with the system. 474 * @param accessibilityWindowId A unique window id. Use 475 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 476 * to query the currently active window. 477 * @param accessibilityNodeId A unique view id or virtual descendant id from 478 * where to start the search. Use 479 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 480 * to start from the root. 481 * @param bypassCache Whether to bypass the cache while looking for the node. 482 * @param prefetchFlags flags to guide prefetching. 483 * @return An {@link AccessibilityNodeInfo} if found, null otherwise. 484 */ findAccessibilityNodeInfoByAccessibilityId(int connectionId, int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache, int prefetchFlags, Bundle arguments)485 public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, 486 int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache, 487 int prefetchFlags, Bundle arguments) { 488 if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0 489 && (prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) == 0) { 490 throw new IllegalArgumentException("FLAG_PREFETCH_SIBLINGS" 491 + " requires FLAG_PREFETCH_PREDECESSORS"); 492 } 493 try { 494 IAccessibilityServiceConnection connection = getConnection(connectionId); 495 if (connection != null) { 496 if (!bypassCache) { 497 AccessibilityNodeInfo cachedInfo = sAccessibilityCache.getNode( 498 accessibilityWindowId, accessibilityNodeId); 499 if (cachedInfo != null) { 500 if (DEBUG) { 501 Log.i(LOG_TAG, "Node cache hit for " 502 + idToString(accessibilityWindowId, accessibilityNodeId)); 503 } 504 if (shouldTraceClient()) { 505 logTraceClient(connection, 506 "findAccessibilityNodeInfoByAccessibilityId cache", 507 "connectionId=" + connectionId + ";accessibilityWindowId=" 508 + accessibilityWindowId + ";accessibilityNodeId=" 509 + accessibilityNodeId + ";bypassCache=" + bypassCache 510 + ";prefetchFlags=" + prefetchFlags + ";arguments=" 511 + arguments); 512 } 513 return cachedInfo; 514 } 515 if (DEBUG) { 516 Log.i(LOG_TAG, "Node cache miss for " 517 + idToString(accessibilityWindowId, accessibilityNodeId)); 518 } 519 } else { 520 // No need to prefech nodes in bypass cache case. 521 prefetchFlags &= ~AccessibilityNodeInfo.FLAG_PREFETCH_MASK; 522 } 523 // Skip prefetching if window is scrolling. 524 if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_MASK) != 0 525 && isWindowScrolling(accessibilityWindowId)) { 526 prefetchFlags &= ~AccessibilityNodeInfo.FLAG_PREFETCH_MASK; 527 } 528 final int interactionId = mInteractionIdCounter.getAndIncrement(); 529 if (shouldTraceClient()) { 530 logTraceClient(connection, "findAccessibilityNodeInfoByAccessibilityId", 531 "InteractionId:" + interactionId + "connectionId=" + connectionId 532 + ";accessibilityWindowId=" + accessibilityWindowId 533 + ";accessibilityNodeId=" + accessibilityNodeId + ";bypassCache=" 534 + bypassCache + ";prefetchFlags=" + prefetchFlags + ";arguments=" 535 + arguments); 536 } 537 final String[] packageNames; 538 final long identityToken = Binder.clearCallingIdentity(); 539 try { 540 packageNames = connection.findAccessibilityNodeInfoByAccessibilityId( 541 accessibilityWindowId, accessibilityNodeId, interactionId, this, 542 prefetchFlags, Thread.currentThread().getId(), arguments); 543 } finally { 544 Binder.restoreCallingIdentity(identityToken); 545 } 546 if (packageNames != null) { 547 AccessibilityNodeInfo info = 548 getFindAccessibilityNodeInfoResultAndClear(interactionId); 549 if (shouldTraceCallback()) { 550 logTraceCallback(connection, "findAccessibilityNodeInfoByAccessibilityId", 551 "InteractionId:" + interactionId + ";connectionId=" 552 + connectionId + ";Result: " + info); 553 } 554 if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_MASK) != 0 555 && info != null) { 556 setInteractionWaitingForPrefetchResult(interactionId, connectionId, 557 packageNames); 558 } 559 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, 560 bypassCache, packageNames); 561 return info; 562 } 563 } else { 564 if (DEBUG) { 565 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 566 } 567 } 568 } catch (RemoteException re) { 569 Log.e(LOG_TAG, "Error while calling remote" 570 + " findAccessibilityNodeInfoByAccessibilityId", re); 571 } 572 return null; 573 } 574 setInteractionWaitingForPrefetchResult(int interactionId, int connectionId, String[] packageNames)575 private void setInteractionWaitingForPrefetchResult(int interactionId, int connectionId, 576 String[] packageNames) { 577 synchronized (mInstanceLock) { 578 mInteractionIdWaitingForPrefetchResult = interactionId; 579 mConnectionIdWaitingForPrefetchResult = connectionId; 580 mPackageNamesForNextPrefetchResult = packageNames; 581 } 582 } 583 idToString(int accessibilityWindowId, long accessibilityNodeId)584 private static String idToString(int accessibilityWindowId, long accessibilityNodeId) { 585 return accessibilityWindowId + "/" 586 + AccessibilityNodeInfo.idToString(accessibilityNodeId); 587 } 588 589 /** 590 * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in 591 * the window whose id is specified and starts from the node whose accessibility 592 * id is specified. 593 * 594 * @param connectionId The id of a connection for interacting with the system. 595 * @param accessibilityWindowId A unique window id. Use 596 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 597 * to query the currently active window. 598 * @param accessibilityNodeId A unique view id or virtual descendant id from 599 * where to start the search. Use 600 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 601 * to start from the root. 602 * @param viewId The fully qualified resource name of the view id to find. 603 * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise. 604 */ findAccessibilityNodeInfosByViewId(int connectionId, int accessibilityWindowId, long accessibilityNodeId, String viewId)605 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId, 606 int accessibilityWindowId, long accessibilityNodeId, String viewId) { 607 try { 608 IAccessibilityServiceConnection connection = getConnection(connectionId); 609 if (connection != null) { 610 final int interactionId = mInteractionIdCounter.getAndIncrement(); 611 final String[] packageNames; 612 final long identityToken = Binder.clearCallingIdentity(); 613 try { 614 if (shouldTraceClient()) { 615 logTraceClient(connection, "findAccessibilityNodeInfosByViewId", 616 "InteractionId=" + interactionId + ";connectionId=" + connectionId 617 + ";accessibilityWindowId=" + accessibilityWindowId 618 + ";accessibilityNodeId=" + accessibilityNodeId + ";viewId=" 619 + viewId); 620 } 621 622 packageNames = connection.findAccessibilityNodeInfosByViewId( 623 accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this, 624 Thread.currentThread().getId()); 625 } finally { 626 Binder.restoreCallingIdentity(identityToken); 627 } 628 629 if (packageNames != null) { 630 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 631 interactionId); 632 if (shouldTraceCallback()) { 633 logTraceCallback(connection, "findAccessibilityNodeInfosByViewId", 634 "InteractionId=" + interactionId + ";connectionId=" + connectionId 635 + ":Result: " + infos); 636 } 637 if (infos != null) { 638 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, 639 false, packageNames); 640 return infos; 641 } 642 } 643 } else { 644 if (DEBUG) { 645 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 646 } 647 } 648 } catch (RemoteException re) { 649 Log.w(LOG_TAG, "Error while calling remote" 650 + " findAccessibilityNodeInfoByViewIdInActiveWindow", re); 651 } 652 return Collections.emptyList(); 653 } 654 655 /** 656 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case 657 * insensitive containment. The search is performed in the window whose 658 * id is specified and starts from the node whose accessibility id is 659 * specified. 660 * 661 * @param connectionId The id of a connection for interacting with the system. 662 * @param accessibilityWindowId A unique window id. Use 663 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 664 * to query the currently active window. 665 * @param accessibilityNodeId A unique view id or virtual descendant id from 666 * where to start the search. Use 667 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 668 * to start from the root. 669 * @param text The searched text. 670 * @return A list of found {@link AccessibilityNodeInfo}s. 671 */ findAccessibilityNodeInfosByText(int connectionId, int accessibilityWindowId, long accessibilityNodeId, String text)672 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId, 673 int accessibilityWindowId, long accessibilityNodeId, String text) { 674 try { 675 IAccessibilityServiceConnection connection = getConnection(connectionId); 676 if (connection != null) { 677 final int interactionId = mInteractionIdCounter.getAndIncrement(); 678 if (shouldTraceClient()) { 679 logTraceClient(connection, "findAccessibilityNodeInfosByText", 680 "InteractionId:" + interactionId + "connectionId=" + connectionId 681 + ";accessibilityWindowId=" + accessibilityWindowId 682 + ";accessibilityNodeId=" + accessibilityNodeId + ";text=" + text); 683 } 684 final String[] packageNames; 685 final long identityToken = Binder.clearCallingIdentity(); 686 try { 687 packageNames = connection.findAccessibilityNodeInfosByText( 688 accessibilityWindowId, accessibilityNodeId, text, interactionId, this, 689 Thread.currentThread().getId()); 690 } finally { 691 Binder.restoreCallingIdentity(identityToken); 692 } 693 694 if (packageNames != null) { 695 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 696 interactionId); 697 if (shouldTraceCallback()) { 698 logTraceCallback(connection, "findAccessibilityNodeInfosByText", 699 "InteractionId=" + interactionId + ";connectionId=" + connectionId 700 + ";Result: " + infos); 701 } 702 if (infos != null) { 703 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, 704 false, packageNames); 705 return infos; 706 } 707 } 708 } else { 709 if (DEBUG) { 710 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 711 } 712 } 713 } catch (RemoteException re) { 714 Log.w(LOG_TAG, "Error while calling remote" 715 + " findAccessibilityNodeInfosByViewText", re); 716 } 717 return Collections.emptyList(); 718 } 719 720 /** 721 * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the 722 * specified focus type. The search is performed in the window whose id is specified 723 * and starts from the node whose accessibility id is specified. 724 * 725 * @param connectionId The id of a connection for interacting with the system. 726 * @param accessibilityWindowId A unique window id. Use 727 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 728 * to query the currently active window. 729 * @param accessibilityNodeId A unique view id or virtual descendant id from 730 * where to start the search. Use 731 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 732 * to start from the root. 733 * @param focusType The focus type. 734 * @return The accessibility focused {@link AccessibilityNodeInfo}. 735 */ findFocus(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int focusType)736 public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId, 737 long accessibilityNodeId, int focusType) { 738 try { 739 IAccessibilityServiceConnection connection = getConnection(connectionId); 740 if (connection != null) { 741 final int interactionId = mInteractionIdCounter.getAndIncrement(); 742 if (shouldTraceClient()) { 743 logTraceClient(connection, "findFocus", 744 "InteractionId:" + interactionId + "connectionId=" + connectionId 745 + ";accessibilityWindowId=" + accessibilityWindowId 746 + ";accessibilityNodeId=" + accessibilityNodeId + ";focusType=" 747 + focusType); 748 } 749 final String[] packageNames; 750 final long identityToken = Binder.clearCallingIdentity(); 751 try { 752 packageNames = connection.findFocus(accessibilityWindowId, 753 accessibilityNodeId, focusType, interactionId, this, 754 Thread.currentThread().getId()); 755 } finally { 756 Binder.restoreCallingIdentity(identityToken); 757 } 758 759 if (packageNames != null) { 760 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 761 interactionId); 762 if (shouldTraceCallback()) { 763 logTraceCallback(connection, "findFocus", "InteractionId=" + interactionId 764 + ";connectionId=" + connectionId + ";Result:" + info); 765 } 766 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames); 767 return info; 768 } 769 } else { 770 if (DEBUG) { 771 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 772 } 773 } 774 } catch (RemoteException re) { 775 Log.w(LOG_TAG, "Error while calling remote findFocus", re); 776 } 777 return null; 778 } 779 780 /** 781 * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}. 782 * The search is performed in the window whose id is specified and starts from the 783 * node whose accessibility id is specified. 784 * 785 * @param connectionId The id of a connection for interacting with the system. 786 * @param accessibilityWindowId A unique window id. Use 787 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 788 * to query the currently active window. 789 * @param accessibilityNodeId A unique view id or virtual descendant id from 790 * where to start the search. Use 791 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 792 * to start from the root. 793 * @param direction The direction in which to search for focusable. 794 * @return The accessibility focused {@link AccessibilityNodeInfo}. 795 */ focusSearch(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int direction)796 public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId, 797 long accessibilityNodeId, int direction) { 798 try { 799 IAccessibilityServiceConnection connection = getConnection(connectionId); 800 if (connection != null) { 801 final int interactionId = mInteractionIdCounter.getAndIncrement(); 802 if (shouldTraceClient()) { 803 logTraceClient(connection, "focusSearch", 804 "InteractionId:" + interactionId + "connectionId=" + connectionId 805 + ";accessibilityWindowId=" + accessibilityWindowId 806 + ";accessibilityNodeId=" + accessibilityNodeId + ";direction=" 807 + direction); 808 } 809 final String[] packageNames; 810 final long identityToken = Binder.clearCallingIdentity(); 811 try { 812 packageNames = connection.focusSearch(accessibilityWindowId, 813 accessibilityNodeId, direction, interactionId, this, 814 Thread.currentThread().getId()); 815 } finally { 816 Binder.restoreCallingIdentity(identityToken); 817 } 818 819 if (packageNames != null) { 820 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 821 interactionId); 822 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames); 823 if (shouldTraceCallback()) { 824 logTraceCallback(connection, "focusSearch", "InteractionId=" + interactionId 825 + ";connectionId=" + connectionId + ";Result:" + info); 826 } 827 return info; 828 } 829 } else { 830 if (DEBUG) { 831 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 832 } 833 } 834 } catch (RemoteException re) { 835 Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re); 836 } 837 return null; 838 } 839 840 /** 841 * Performs an accessibility action on an {@link AccessibilityNodeInfo}. 842 * 843 * @param connectionId The id of a connection for interacting with the system. 844 * @param accessibilityWindowId A unique window id. Use 845 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 846 * to query the currently active window. 847 * @param accessibilityNodeId A unique view id or virtual descendant id from 848 * where to start the search. Use 849 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 850 * to start from the root. 851 * @param action The action to perform. 852 * @param arguments Optional action arguments. 853 * @return Whether the action was performed. 854 */ performAccessibilityAction(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int action, Bundle arguments)855 public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId, 856 long accessibilityNodeId, int action, Bundle arguments) { 857 try { 858 IAccessibilityServiceConnection connection = getConnection(connectionId); 859 if (connection != null) { 860 final int interactionId = mInteractionIdCounter.getAndIncrement(); 861 if (shouldTraceClient()) { 862 logTraceClient(connection, "performAccessibilityAction", 863 "InteractionId:" + interactionId + "connectionId=" + connectionId 864 + ";accessibilityWindowId=" + accessibilityWindowId 865 + ";accessibilityNodeId=" + accessibilityNodeId + ";action=" + action 866 + ";arguments=" + arguments); 867 } 868 final boolean success; 869 final long identityToken = Binder.clearCallingIdentity(); 870 try { 871 success = connection.performAccessibilityAction( 872 accessibilityWindowId, accessibilityNodeId, action, arguments, 873 interactionId, this, Thread.currentThread().getId()); 874 } finally { 875 Binder.restoreCallingIdentity(identityToken); 876 } 877 878 if (success) { 879 final boolean result = 880 getPerformAccessibilityActionResultAndClear(interactionId); 881 if (shouldTraceCallback()) { 882 logTraceCallback(connection, "performAccessibilityAction", 883 "InteractionId=" + interactionId + ";connectionId=" + connectionId 884 + ";Result: " + result); 885 } 886 return result; 887 } 888 } else { 889 if (DEBUG) { 890 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 891 } 892 } 893 } catch (RemoteException re) { 894 Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re); 895 } 896 return false; 897 } 898 899 /** 900 * Clears the accessibility cache. 901 */ 902 @UnsupportedAppUsage() clearCache()903 public void clearCache() { 904 sAccessibilityCache.clear(); 905 } 906 onAccessibilityEvent(AccessibilityEvent event)907 public void onAccessibilityEvent(AccessibilityEvent event) { 908 switch (event.getEventType()) { 909 case AccessibilityEvent.TYPE_VIEW_SCROLLED: 910 updateScrollingWindow(event.getWindowId(), SystemClock.uptimeMillis()); 911 break; 912 case AccessibilityEvent.TYPE_WINDOWS_CHANGED: 913 if (event.getWindowChanges() == AccessibilityEvent.WINDOWS_CHANGE_REMOVED) { 914 deleteScrollingWindow(event.getWindowId()); 915 } 916 break; 917 default: 918 break; 919 } 920 sAccessibilityCache.onAccessibilityEvent(event); 921 } 922 923 /** 924 * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}. 925 * 926 * @param interactionId The interaction id to match the result with the request. 927 * @return The result {@link AccessibilityNodeInfo}. 928 */ getFindAccessibilityNodeInfoResultAndClear(int interactionId)929 private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) { 930 synchronized (mInstanceLock) { 931 final boolean success = waitForResultTimedLocked(interactionId); 932 AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null; 933 clearResultLocked(); 934 return result; 935 } 936 } 937 938 /** 939 * {@inheritDoc} 940 */ setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId)941 public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, 942 int interactionId) { 943 synchronized (mInstanceLock) { 944 if (interactionId > mInteractionId) { 945 mFindAccessibilityNodeInfoResult = info; 946 mInteractionId = interactionId; 947 mCallingUid = Binder.getCallingUid(); 948 mCallStackOfCallback = new ArrayList<StackTraceElement>( 949 Arrays.asList(Thread.currentThread().getStackTrace())); 950 } 951 mInstanceLock.notifyAll(); 952 } 953 } 954 955 /** 956 * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s. 957 * 958 * @param interactionId The interaction id to match the result with the request. 959 * @return The result {@link AccessibilityNodeInfo}s. 960 */ getFindAccessibilityNodeInfosResultAndClear( int interactionId)961 private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear( 962 int interactionId) { 963 synchronized (mInstanceLock) { 964 final boolean success = waitForResultTimedLocked(interactionId); 965 final List<AccessibilityNodeInfo> result; 966 if (success) { 967 result = mFindAccessibilityNodeInfosResult; 968 } else { 969 result = Collections.emptyList(); 970 } 971 clearResultLocked(); 972 if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) { 973 checkFindAccessibilityNodeInfoResultIntegrity(result); 974 } 975 return result; 976 } 977 } 978 979 /** 980 * {@inheritDoc} 981 */ setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, int interactionId)982 public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, 983 int interactionId) { 984 synchronized (mInstanceLock) { 985 if (interactionId > mInteractionId) { 986 if (infos != null) { 987 // If the call is not an IPC, i.e. it is made from the same process, we need to 988 // instantiate new result list to avoid passing internal instances to clients. 989 final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid()); 990 if (!isIpcCall) { 991 mFindAccessibilityNodeInfosResult = new ArrayList<>(infos); 992 } else { 993 mFindAccessibilityNodeInfosResult = infos; 994 } 995 } else { 996 mFindAccessibilityNodeInfosResult = Collections.emptyList(); 997 } 998 mInteractionId = interactionId; 999 mCallingUid = Binder.getCallingUid(); 1000 mCallStackOfCallback = new ArrayList<StackTraceElement>( 1001 Arrays.asList(Thread.currentThread().getStackTrace())); 1002 } 1003 mInstanceLock.notifyAll(); 1004 } 1005 } 1006 1007 /** 1008 * {@inheritDoc} 1009 */ 1010 @Override setPrefetchAccessibilityNodeInfoResult(@onNull List<AccessibilityNodeInfo> infos, int interactionId)1011 public void setPrefetchAccessibilityNodeInfoResult(@NonNull List<AccessibilityNodeInfo> infos, 1012 int interactionId) { 1013 int interactionIdWaitingForPrefetchResultCopy = -1; 1014 int connectionIdWaitingForPrefetchResultCopy = -1; 1015 String[] packageNamesForNextPrefetchResultCopy = null; 1016 1017 if (infos.isEmpty()) { 1018 return; 1019 } 1020 1021 synchronized (mInstanceLock) { 1022 if (mInteractionIdWaitingForPrefetchResult == interactionId) { 1023 interactionIdWaitingForPrefetchResultCopy = mInteractionIdWaitingForPrefetchResult; 1024 connectionIdWaitingForPrefetchResultCopy = 1025 mConnectionIdWaitingForPrefetchResult; 1026 if (mPackageNamesForNextPrefetchResult != null) { 1027 packageNamesForNextPrefetchResultCopy = 1028 new String[mPackageNamesForNextPrefetchResult.length]; 1029 for (int i = 0; i < mPackageNamesForNextPrefetchResult.length; i++) { 1030 packageNamesForNextPrefetchResultCopy[i] = 1031 mPackageNamesForNextPrefetchResult[i]; 1032 } 1033 } 1034 } 1035 } 1036 1037 if (interactionIdWaitingForPrefetchResultCopy == interactionId) { 1038 finalizeAndCacheAccessibilityNodeInfos( 1039 infos, connectionIdWaitingForPrefetchResultCopy, false, 1040 packageNamesForNextPrefetchResultCopy); 1041 if (shouldTraceCallback()) { 1042 logTrace(getConnection(connectionIdWaitingForPrefetchResultCopy), 1043 "setPrefetchAccessibilityNodeInfoResult", 1044 "InteractionId:" + interactionId + ";connectionId=" 1045 + connectionIdWaitingForPrefetchResultCopy + ";Result: " + infos, 1046 Binder.getCallingUid(), 1047 Arrays.asList(Thread.currentThread().getStackTrace()), 1048 new HashSet<String>(Arrays.asList("getStackTrace")), 1049 FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK); 1050 } 1051 } else if (DEBUG) { 1052 Log.w(LOG_TAG, "Prefetching for interaction with id " + interactionId + " dropped " 1053 + infos.size() + " nodes"); 1054 } 1055 } 1056 1057 /** 1058 * Gets the result of a request to perform an accessibility action. 1059 * 1060 * @param interactionId The interaction id to match the result with the request. 1061 * @return Whether the action was performed. 1062 */ getPerformAccessibilityActionResultAndClear(int interactionId)1063 private boolean getPerformAccessibilityActionResultAndClear(int interactionId) { 1064 synchronized (mInstanceLock) { 1065 final boolean success = waitForResultTimedLocked(interactionId); 1066 final boolean result = success ? mPerformAccessibilityActionResult : false; 1067 clearResultLocked(); 1068 return result; 1069 } 1070 } 1071 1072 /** 1073 * {@inheritDoc} 1074 */ setPerformAccessibilityActionResult(boolean succeeded, int interactionId)1075 public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) { 1076 synchronized (mInstanceLock) { 1077 if (interactionId > mInteractionId) { 1078 mPerformAccessibilityActionResult = succeeded; 1079 mInteractionId = interactionId; 1080 mCallingUid = Binder.getCallingUid(); 1081 mCallStackOfCallback = new ArrayList<StackTraceElement>( 1082 Arrays.asList(Thread.currentThread().getStackTrace())); 1083 } 1084 mInstanceLock.notifyAll(); 1085 } 1086 } 1087 1088 /** 1089 * Clears the result state. 1090 */ clearResultLocked()1091 private void clearResultLocked() { 1092 mInteractionId = -1; 1093 mFindAccessibilityNodeInfoResult = null; 1094 mFindAccessibilityNodeInfosResult = null; 1095 mPerformAccessibilityActionResult = false; 1096 } 1097 1098 /** 1099 * Waits up to a given bound for a result of a request and returns it. 1100 * 1101 * @param interactionId The interaction id to match the result with the request. 1102 * @return Whether the result was received. 1103 */ waitForResultTimedLocked(int interactionId)1104 private boolean waitForResultTimedLocked(int interactionId) { 1105 long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS; 1106 final long startTimeMillis = SystemClock.uptimeMillis(); 1107 while (true) { 1108 try { 1109 Message sameProcessMessage = getSameProcessMessageAndClear(); 1110 if (sameProcessMessage != null) { 1111 sameProcessMessage.getTarget().handleMessage(sameProcessMessage); 1112 } 1113 1114 if (mInteractionId == interactionId) { 1115 return true; 1116 } 1117 if (mInteractionId > interactionId) { 1118 return false; 1119 } 1120 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 1121 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis; 1122 if (waitTimeMillis <= 0) { 1123 return false; 1124 } 1125 mInstanceLock.wait(waitTimeMillis); 1126 } catch (InterruptedException ie) { 1127 /* ignore */ 1128 } 1129 } 1130 } 1131 1132 /** 1133 * Finalize an {@link AccessibilityNodeInfo} before passing it to the client. 1134 * 1135 * @param info The info. 1136 * @param connectionId The id of the connection to the system. 1137 * @param bypassCache Whether or not to bypass the cache. The node is added to the cache if 1138 * this value is {@code false} 1139 * @param packageNames The valid package names a node can come from. 1140 */ finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId, boolean bypassCache, String[] packageNames)1141 private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, 1142 int connectionId, boolean bypassCache, String[] packageNames) { 1143 if (info != null) { 1144 info.setConnectionId(connectionId); 1145 // Empty array means any package name is Okay 1146 if (!ArrayUtils.isEmpty(packageNames)) { 1147 CharSequence packageName = info.getPackageName(); 1148 if (packageName == null 1149 || !ArrayUtils.contains(packageNames, packageName.toString())) { 1150 // If the node package not one of the valid ones, pick the top one - this 1151 // is one of the packages running in the introspected UID. 1152 info.setPackageName(packageNames[0]); 1153 } 1154 } 1155 info.setSealed(true); 1156 if (!bypassCache) { 1157 sAccessibilityCache.add(info); 1158 } 1159 } 1160 } 1161 1162 /** 1163 * Finalize {@link AccessibilityNodeInfo}s before passing them to the client. 1164 * 1165 * @param infos The {@link AccessibilityNodeInfo}s. 1166 * @param connectionId The id of the connection to the system. 1167 * @param bypassCache Whether or not to bypass the cache. The nodes are added to the cache if 1168 * this value is {@code false} 1169 * @param packageNames The valid package names a node can come from. 1170 */ finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, int connectionId, boolean bypassCache, String[] packageNames)1171 private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, 1172 int connectionId, boolean bypassCache, String[] packageNames) { 1173 if (infos != null) { 1174 final int infosCount = infos.size(); 1175 for (int i = 0; i < infosCount; i++) { 1176 AccessibilityNodeInfo info = infos.get(i); 1177 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, 1178 bypassCache, packageNames); 1179 } 1180 } 1181 } 1182 1183 /** 1184 * Gets the message stored if the interacted and interacting 1185 * threads are the same. 1186 * 1187 * @return The message. 1188 */ getSameProcessMessageAndClear()1189 private Message getSameProcessMessageAndClear() { 1190 synchronized (mInstanceLock) { 1191 Message result = mSameThreadMessage; 1192 mSameThreadMessage = null; 1193 return result; 1194 } 1195 } 1196 1197 /** 1198 * Checks whether the infos are a fully connected tree with no duplicates. 1199 * 1200 * @param infos The result list to check. 1201 */ checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos)1202 private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) { 1203 if (infos.size() == 0) { 1204 return; 1205 } 1206 // Find the root node. 1207 AccessibilityNodeInfo root = infos.get(0); 1208 final int infoCount = infos.size(); 1209 for (int i = 1; i < infoCount; i++) { 1210 for (int j = i; j < infoCount; j++) { 1211 AccessibilityNodeInfo candidate = infos.get(j); 1212 if (root.getParentNodeId() == candidate.getSourceNodeId()) { 1213 root = candidate; 1214 break; 1215 } 1216 } 1217 } 1218 if (root == null) { 1219 Log.e(LOG_TAG, "No root."); 1220 } 1221 // Check for duplicates. 1222 HashSet<AccessibilityNodeInfo> seen = new HashSet<>(); 1223 Queue<AccessibilityNodeInfo> fringe = new LinkedList<>(); 1224 fringe.add(root); 1225 while (!fringe.isEmpty()) { 1226 AccessibilityNodeInfo current = fringe.poll(); 1227 if (!seen.add(current)) { 1228 Log.e(LOG_TAG, "Duplicate node."); 1229 return; 1230 } 1231 final int childCount = current.getChildCount(); 1232 for (int i = 0; i < childCount; i++) { 1233 final long childId = current.getChildId(i); 1234 for (int j = 0; j < infoCount; j++) { 1235 AccessibilityNodeInfo child = infos.get(j); 1236 if (child.getSourceNodeId() == childId) { 1237 fringe.add(child); 1238 } 1239 } 1240 } 1241 } 1242 final int disconnectedCount = infos.size() - seen.size(); 1243 if (disconnectedCount > 0) { 1244 Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes."); 1245 } 1246 } 1247 1248 /** 1249 * Update scroll event timestamp of a given window. 1250 * 1251 * @param windowId The window id. 1252 * @param uptimeMillis Device uptime millis. 1253 */ updateScrollingWindow(int windowId, long uptimeMillis)1254 private void updateScrollingWindow(int windowId, long uptimeMillis) { 1255 synchronized (sScrollingWindows) { 1256 sScrollingWindows.put(windowId, uptimeMillis); 1257 } 1258 } 1259 1260 /** 1261 * Remove a window from the scrolling windows list. 1262 * 1263 * @param windowId The window id. 1264 */ deleteScrollingWindow(int windowId)1265 private void deleteScrollingWindow(int windowId) { 1266 synchronized (sScrollingWindows) { 1267 sScrollingWindows.delete(windowId); 1268 } 1269 } 1270 1271 /** 1272 * Whether or not the window is scrolling. 1273 * 1274 * @param windowId 1275 * @return true if it's scrolling. 1276 */ isWindowScrolling(int windowId)1277 private boolean isWindowScrolling(int windowId) { 1278 synchronized (sScrollingWindows) { 1279 final long latestScrollingTime = sScrollingWindows.get(windowId); 1280 if (latestScrollingTime == 0) { 1281 return false; 1282 } 1283 final long currentUptime = SystemClock.uptimeMillis(); 1284 if (currentUptime > (latestScrollingTime + DISABLE_PREFETCHING_FOR_SCROLLING_MILLIS)) { 1285 sScrollingWindows.delete(windowId); 1286 return false; 1287 } 1288 } 1289 return true; 1290 } 1291 shouldTraceClient()1292 private boolean shouldTraceClient() { 1293 return (mAccessibilityManager != null) 1294 && mAccessibilityManager.isA11yInteractionClientTraceEnabled(); 1295 } 1296 shouldTraceCallback()1297 private boolean shouldTraceCallback() { 1298 return (mAccessibilityManager != null) 1299 && mAccessibilityManager.isA11yInteractionConnectionCBTraceEnabled(); 1300 } 1301 logTrace( IAccessibilityServiceConnection connection, String method, String params, int callingUid, List<StackTraceElement> callStack, HashSet<String> ignoreSet, long logTypes)1302 private void logTrace( 1303 IAccessibilityServiceConnection connection, String method, String params, 1304 int callingUid, List<StackTraceElement> callStack, HashSet<String> ignoreSet, 1305 long logTypes) { 1306 try { 1307 Bundle b = new Bundle(); 1308 b.putSerializable(CALL_STACK, new ArrayList<StackTraceElement>(callStack)); 1309 if (ignoreSet != null) { 1310 b.putSerializable(IGNORE_CALL_STACK, ignoreSet); 1311 } 1312 connection.logTrace(SystemClock.elapsedRealtimeNanos(), 1313 LOG_TAG + "." + method, 1314 logTypes, params, Process.myPid(), Thread.currentThread().getId(), 1315 callingUid, b); 1316 } catch (RemoteException e) { 1317 Log.e(LOG_TAG, "Failed to log trace. " + e); 1318 } 1319 } 1320 logTraceCallback( IAccessibilityServiceConnection connection, String method, String params)1321 private void logTraceCallback( 1322 IAccessibilityServiceConnection connection, String method, String params) { 1323 logTrace(connection, method + " callback", params, mCallingUid, mCallStackOfCallback, 1324 new HashSet<String>(Arrays.asList("getStackTrace")), 1325 FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK); 1326 } 1327 logTraceClient( IAccessibilityServiceConnection connection, String method, String params)1328 private void logTraceClient( 1329 IAccessibilityServiceConnection connection, String method, String params) { 1330 logTrace(connection, method, params, Binder.getCallingUid(), 1331 Collections.emptyList(), null, FLAGS_ACCESSIBILITY_INTERACTION_CLIENT); 1332 } 1333 } 1334