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