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