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.compat.annotation.UnsupportedAppUsage; 21 import android.os.Binder; 22 import android.os.Build; 23 import android.os.Bundle; 24 import android.os.Message; 25 import android.os.Process; 26 import android.os.RemoteException; 27 import android.os.SystemClock; 28 import android.util.Log; 29 import android.util.LongSparseArray; 30 import android.util.SparseArray; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.internal.util.ArrayUtils; 34 35 import java.util.ArrayList; 36 import java.util.Collections; 37 import java.util.HashSet; 38 import java.util.LinkedList; 39 import java.util.List; 40 import java.util.Queue; 41 import java.util.concurrent.atomic.AtomicInteger; 42 43 /** 44 * This class is a singleton that performs accessibility interaction 45 * which is it queries remote view hierarchies about snapshots of their 46 * views as well requests from these hierarchies to perform certain 47 * actions on their views. 48 * 49 * Rationale: The content retrieval APIs are synchronous from a client's 50 * perspective but internally they are asynchronous. The client thread 51 * calls into the system requesting an action and providing a callback 52 * to receive the result after which it waits up to a timeout for that 53 * result. The system enforces security and the delegates the request 54 * to a given view hierarchy where a message is posted (from a binder 55 * thread) describing what to be performed by the main UI thread the 56 * result of which it delivered via the mentioned callback. However, 57 * the blocked client thread and the main UI thread of the target view 58 * hierarchy can be the same thread, for example an accessibility service 59 * and an activity run in the same process, thus they are executed on the 60 * same main thread. In such a case the retrieval will fail since the UI 61 * thread that has to process the message describing the work to be done 62 * is blocked waiting for a result is has to compute! To avoid this scenario 63 * when making a call the client also passes its process and thread ids so 64 * the accessed view hierarchy can detect if the client making the request 65 * is running in its main UI thread. In such a case the view hierarchy, 66 * specifically the binder thread performing the IPC to it, does not post a 67 * message to be run on the UI thread but passes it to the singleton 68 * interaction client through which all interactions occur and the latter is 69 * responsible to execute the message before starting to wait for the 70 * asynchronous result delivered via the callback. In this case the expected 71 * result is already received so no waiting is performed. 72 * 73 * @hide 74 */ 75 public final class AccessibilityInteractionClient 76 extends IAccessibilityInteractionConnectionCallback.Stub { 77 78 public static final int NO_ID = -1; 79 80 private static final String LOG_TAG = "AccessibilityInteractionClient"; 81 82 private static final boolean DEBUG = false; 83 84 private static final boolean CHECK_INTEGRITY = true; 85 86 private static final long TIMEOUT_INTERACTION_MILLIS = 5000; 87 88 private static final Object sStaticLock = new Object(); 89 90 private static final LongSparseArray<AccessibilityInteractionClient> sClients = 91 new LongSparseArray<>(); 92 93 private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache = 94 new SparseArray<>(); 95 96 private static AccessibilityCache sAccessibilityCache = 97 new AccessibilityCache(new AccessibilityCache.AccessibilityNodeRefresher()); 98 99 private final AtomicInteger mInteractionIdCounter = new AtomicInteger(); 100 101 private final Object mInstanceLock = new Object(); 102 103 private volatile int mInteractionId = -1; 104 105 private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult; 106 107 private List<AccessibilityNodeInfo> mFindAccessibilityNodeInfosResult; 108 109 private boolean mPerformAccessibilityActionResult; 110 111 private Message mSameThreadMessage; 112 113 /** 114 * @return The client for the current thread. 115 */ 116 @UnsupportedAppUsage getInstance()117 public static AccessibilityInteractionClient getInstance() { 118 final long threadId = Thread.currentThread().getId(); 119 return getInstanceForThread(threadId); 120 } 121 122 /** 123 * <strong>Note:</strong> We keep one instance per interrogating thread since 124 * the instance contains state which can lead to undesired thread interleavings. 125 * We do not have a thread local variable since other threads should be able to 126 * look up the correct client knowing a thread id. See ViewRootImpl for details. 127 * 128 * @return The client for a given <code>threadId</code>. 129 */ getInstanceForThread(long threadId)130 public static AccessibilityInteractionClient getInstanceForThread(long threadId) { 131 synchronized (sStaticLock) { 132 AccessibilityInteractionClient client = sClients.get(threadId); 133 if (client == null) { 134 client = new AccessibilityInteractionClient(); 135 sClients.put(threadId, client); 136 } 137 return client; 138 } 139 } 140 141 /** 142 * Gets a cached accessibility service connection. 143 * 144 * @param connectionId The connection id. 145 * @return The cached connection if such. 146 */ getConnection(int connectionId)147 public static IAccessibilityServiceConnection getConnection(int connectionId) { 148 synchronized (sConnectionCache) { 149 return sConnectionCache.get(connectionId); 150 } 151 } 152 153 /** 154 * Adds a cached accessibility service connection. 155 * 156 * @param connectionId The connection id. 157 * @param connection The connection. 158 */ addConnection(int connectionId, IAccessibilityServiceConnection connection)159 public static void addConnection(int connectionId, IAccessibilityServiceConnection connection) { 160 synchronized (sConnectionCache) { 161 sConnectionCache.put(connectionId, connection); 162 } 163 } 164 165 /** 166 * Removes a cached accessibility service connection. 167 * 168 * @param connectionId The connection id. 169 */ removeConnection(int connectionId)170 public static void removeConnection(int connectionId) { 171 synchronized (sConnectionCache) { 172 sConnectionCache.remove(connectionId); 173 } 174 } 175 176 /** 177 * This method is only for testing. Replacing the cache is a generally terrible idea, but 178 * tests need to be able to verify this class's interactions with the cache 179 */ 180 @VisibleForTesting setCache(AccessibilityCache cache)181 public static void setCache(AccessibilityCache cache) { 182 sAccessibilityCache = cache; 183 } 184 AccessibilityInteractionClient()185 private AccessibilityInteractionClient() { 186 /* reducing constructor visibility */ 187 } 188 189 /** 190 * Sets the message to be processed if the interacted view hierarchy 191 * and the interacting client are running in the same thread. 192 * 193 * @param message The message. 194 */ 195 @UnsupportedAppUsage setSameThreadMessage(Message message)196 public void setSameThreadMessage(Message message) { 197 synchronized (mInstanceLock) { 198 mSameThreadMessage = message; 199 mInstanceLock.notifyAll(); 200 } 201 } 202 203 /** 204 * Gets the root {@link AccessibilityNodeInfo} in the currently active window. 205 * 206 * @param connectionId The id of a connection for interacting with the system. 207 * @return The root {@link AccessibilityNodeInfo} if found, null otherwise. 208 */ getRootInActiveWindow(int connectionId)209 public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) { 210 return findAccessibilityNodeInfoByAccessibilityId(connectionId, 211 AccessibilityWindowInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, 212 false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS, null); 213 } 214 215 /** 216 * Gets the info for a window. 217 * 218 * @param connectionId The id of a connection for interacting with the system. 219 * @param accessibilityWindowId A unique window id. Use 220 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 221 * to query the currently active window. 222 * @return The {@link AccessibilityWindowInfo}. 223 */ getWindow(int connectionId, int accessibilityWindowId)224 public AccessibilityWindowInfo getWindow(int connectionId, int accessibilityWindowId) { 225 try { 226 IAccessibilityServiceConnection connection = getConnection(connectionId); 227 if (connection != null) { 228 AccessibilityWindowInfo window = sAccessibilityCache.getWindow( 229 accessibilityWindowId); 230 if (window != null) { 231 if (DEBUG) { 232 Log.i(LOG_TAG, "Window cache hit"); 233 } 234 return window; 235 } 236 if (DEBUG) { 237 Log.i(LOG_TAG, "Window cache miss"); 238 } 239 final long identityToken = Binder.clearCallingIdentity(); 240 try { 241 window = connection.getWindow(accessibilityWindowId); 242 } finally { 243 Binder.restoreCallingIdentity(identityToken); 244 } 245 if (window != null) { 246 sAccessibilityCache.addWindow(window); 247 return window; 248 } 249 } else { 250 if (DEBUG) { 251 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 252 } 253 } 254 } catch (RemoteException re) { 255 Log.e(LOG_TAG, "Error while calling remote getWindow", re); 256 } 257 return null; 258 } 259 260 /** 261 * Gets the info for all windows. 262 * 263 * @param connectionId The id of a connection for interacting with the system. 264 * @return The {@link AccessibilityWindowInfo} list. 265 */ getWindows(int connectionId)266 public List<AccessibilityWindowInfo> getWindows(int connectionId) { 267 try { 268 IAccessibilityServiceConnection connection = getConnection(connectionId); 269 if (connection != null) { 270 List<AccessibilityWindowInfo> windows = sAccessibilityCache.getWindows(); 271 if (windows != null) { 272 if (DEBUG) { 273 Log.i(LOG_TAG, "Windows cache hit"); 274 } 275 return windows; 276 } 277 if (DEBUG) { 278 Log.i(LOG_TAG, "Windows cache miss"); 279 } 280 final long identityToken = Binder.clearCallingIdentity(); 281 try { 282 windows = connection.getWindows(); 283 } finally { 284 Binder.restoreCallingIdentity(identityToken); 285 } 286 if (windows != null) { 287 sAccessibilityCache.setWindows(windows); 288 return windows; 289 } 290 } else { 291 if (DEBUG) { 292 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 293 } 294 } 295 } catch (RemoteException re) { 296 Log.e(LOG_TAG, "Error while calling remote getWindows", re); 297 } 298 return Collections.emptyList(); 299 } 300 301 /** 302 * Finds an {@link AccessibilityNodeInfo} by accessibility id. 303 * 304 * @param connectionId The id of a connection for interacting with the system. 305 * @param accessibilityWindowId A unique window id. Use 306 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 307 * to query the currently active window. 308 * @param accessibilityNodeId A unique view id or virtual descendant id from 309 * where to start the search. Use 310 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 311 * to start from the root. 312 * @param bypassCache Whether to bypass the cache while looking for the node. 313 * @param prefetchFlags flags to guide prefetching. 314 * @return An {@link AccessibilityNodeInfo} if found, null otherwise. 315 */ findAccessibilityNodeInfoByAccessibilityId(int connectionId, int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache, int prefetchFlags, Bundle arguments)316 public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, 317 int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache, 318 int prefetchFlags, Bundle arguments) { 319 if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0 320 && (prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) == 0) { 321 throw new IllegalArgumentException("FLAG_PREFETCH_SIBLINGS" 322 + " requires FLAG_PREFETCH_PREDECESSORS"); 323 } 324 try { 325 IAccessibilityServiceConnection connection = getConnection(connectionId); 326 if (connection != null) { 327 if (!bypassCache) { 328 AccessibilityNodeInfo cachedInfo = sAccessibilityCache.getNode( 329 accessibilityWindowId, accessibilityNodeId); 330 if (cachedInfo != null) { 331 if (DEBUG) { 332 Log.i(LOG_TAG, "Node cache hit for " 333 + idToString(accessibilityWindowId, accessibilityNodeId)); 334 } 335 return cachedInfo; 336 } 337 if (DEBUG) { 338 Log.i(LOG_TAG, "Node cache miss for " 339 + idToString(accessibilityWindowId, accessibilityNodeId)); 340 } 341 } 342 final int interactionId = mInteractionIdCounter.getAndIncrement(); 343 final long identityToken = Binder.clearCallingIdentity(); 344 final String[] packageNames; 345 try { 346 packageNames = connection.findAccessibilityNodeInfoByAccessibilityId( 347 accessibilityWindowId, accessibilityNodeId, interactionId, this, 348 prefetchFlags, Thread.currentThread().getId(), arguments); 349 } finally { 350 Binder.restoreCallingIdentity(identityToken); 351 } 352 if (packageNames != null) { 353 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 354 interactionId); 355 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, 356 bypassCache, packageNames); 357 if (infos != null && !infos.isEmpty()) { 358 for (int i = 1; i < infos.size(); i++) { 359 infos.get(i).recycle(); 360 } 361 return infos.get(0); 362 } 363 } 364 } else { 365 if (DEBUG) { 366 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 367 } 368 } 369 } catch (RemoteException re) { 370 Log.e(LOG_TAG, "Error while calling remote" 371 + " findAccessibilityNodeInfoByAccessibilityId", re); 372 } 373 return null; 374 } 375 idToString(int accessibilityWindowId, long accessibilityNodeId)376 private static String idToString(int accessibilityWindowId, long accessibilityNodeId) { 377 return accessibilityWindowId + "/" 378 + AccessibilityNodeInfo.idToString(accessibilityNodeId); 379 } 380 381 /** 382 * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in 383 * the window whose id is specified and starts from the node whose accessibility 384 * id is specified. 385 * 386 * @param connectionId The id of a connection for interacting with the system. 387 * @param accessibilityWindowId A unique window id. Use 388 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 389 * to query the currently active window. 390 * @param accessibilityNodeId A unique view id or virtual descendant id from 391 * where to start the search. Use 392 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 393 * to start from the root. 394 * @param viewId The fully qualified resource name of the view id to find. 395 * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise. 396 */ findAccessibilityNodeInfosByViewId(int connectionId, int accessibilityWindowId, long accessibilityNodeId, String viewId)397 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId, 398 int accessibilityWindowId, long accessibilityNodeId, String viewId) { 399 try { 400 IAccessibilityServiceConnection connection = getConnection(connectionId); 401 if (connection != null) { 402 final int interactionId = mInteractionIdCounter.getAndIncrement(); 403 final long identityToken = Binder.clearCallingIdentity(); 404 final String[] packageNames; 405 try { 406 packageNames = connection.findAccessibilityNodeInfosByViewId( 407 accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this, 408 Thread.currentThread().getId()); 409 } finally { 410 Binder.restoreCallingIdentity(identityToken); 411 } 412 413 if (packageNames != null) { 414 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 415 interactionId); 416 if (infos != null) { 417 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, 418 false, packageNames); 419 return infos; 420 } 421 } 422 } else { 423 if (DEBUG) { 424 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 425 } 426 } 427 } catch (RemoteException re) { 428 Log.w(LOG_TAG, "Error while calling remote" 429 + " findAccessibilityNodeInfoByViewIdInActiveWindow", re); 430 } 431 return Collections.emptyList(); 432 } 433 434 /** 435 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case 436 * insensitive containment. The search is performed in the window whose 437 * id is specified and starts from the node whose accessibility id is 438 * specified. 439 * 440 * @param connectionId The id of a connection for interacting with the system. 441 * @param accessibilityWindowId A unique window id. Use 442 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 443 * to query the currently active window. 444 * @param accessibilityNodeId A unique view id or virtual descendant id from 445 * where to start the search. Use 446 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 447 * to start from the root. 448 * @param text The searched text. 449 * @return A list of found {@link AccessibilityNodeInfo}s. 450 */ findAccessibilityNodeInfosByText(int connectionId, int accessibilityWindowId, long accessibilityNodeId, String text)451 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId, 452 int accessibilityWindowId, long accessibilityNodeId, String text) { 453 try { 454 IAccessibilityServiceConnection connection = getConnection(connectionId); 455 if (connection != null) { 456 final int interactionId = mInteractionIdCounter.getAndIncrement(); 457 final long identityToken = Binder.clearCallingIdentity(); 458 final String[] packageNames; 459 try { 460 packageNames = connection.findAccessibilityNodeInfosByText( 461 accessibilityWindowId, accessibilityNodeId, text, interactionId, this, 462 Thread.currentThread().getId()); 463 } finally { 464 Binder.restoreCallingIdentity(identityToken); 465 } 466 467 if (packageNames != null) { 468 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 469 interactionId); 470 if (infos != null) { 471 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, 472 false, packageNames); 473 return infos; 474 } 475 } 476 } else { 477 if (DEBUG) { 478 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 479 } 480 } 481 } catch (RemoteException re) { 482 Log.w(LOG_TAG, "Error while calling remote" 483 + " findAccessibilityNodeInfosByViewText", re); 484 } 485 return Collections.emptyList(); 486 } 487 488 /** 489 * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the 490 * specified focus type. The search is performed in the window whose id is specified 491 * and starts from the node whose accessibility id is specified. 492 * 493 * @param connectionId The id of a connection for interacting with the system. 494 * @param accessibilityWindowId A unique window id. Use 495 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 496 * to query the currently active window. 497 * @param accessibilityNodeId A unique view id or virtual descendant id from 498 * where to start the search. Use 499 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 500 * to start from the root. 501 * @param focusType The focus type. 502 * @return The accessibility focused {@link AccessibilityNodeInfo}. 503 */ findFocus(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int focusType)504 public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId, 505 long accessibilityNodeId, int focusType) { 506 try { 507 IAccessibilityServiceConnection connection = getConnection(connectionId); 508 if (connection != null) { 509 final int interactionId = mInteractionIdCounter.getAndIncrement(); 510 final long identityToken = Binder.clearCallingIdentity(); 511 final String[] packageNames; 512 try { 513 packageNames = connection.findFocus(accessibilityWindowId, 514 accessibilityNodeId, focusType, interactionId, this, 515 Thread.currentThread().getId()); 516 } finally { 517 Binder.restoreCallingIdentity(identityToken); 518 } 519 520 if (packageNames != null) { 521 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 522 interactionId); 523 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames); 524 return info; 525 } 526 } else { 527 if (DEBUG) { 528 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 529 } 530 } 531 } catch (RemoteException re) { 532 Log.w(LOG_TAG, "Error while calling remote findFocus", re); 533 } 534 return null; 535 } 536 537 /** 538 * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}. 539 * The search is performed in the window whose id is specified and starts from the 540 * node whose accessibility id is specified. 541 * 542 * @param connectionId The id of a connection for interacting with the system. 543 * @param accessibilityWindowId A unique window id. Use 544 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 545 * to query the currently active window. 546 * @param accessibilityNodeId A unique view id or virtual descendant id from 547 * where to start the search. Use 548 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 549 * to start from the root. 550 * @param direction The direction in which to search for focusable. 551 * @return The accessibility focused {@link AccessibilityNodeInfo}. 552 */ focusSearch(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int direction)553 public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId, 554 long accessibilityNodeId, int direction) { 555 try { 556 IAccessibilityServiceConnection connection = getConnection(connectionId); 557 if (connection != null) { 558 final int interactionId = mInteractionIdCounter.getAndIncrement(); 559 final long identityToken = Binder.clearCallingIdentity(); 560 final String[] packageNames; 561 try { 562 packageNames = connection.focusSearch(accessibilityWindowId, 563 accessibilityNodeId, direction, interactionId, this, 564 Thread.currentThread().getId()); 565 } finally { 566 Binder.restoreCallingIdentity(identityToken); 567 } 568 569 if (packageNames != null) { 570 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 571 interactionId); 572 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames); 573 return info; 574 } 575 } else { 576 if (DEBUG) { 577 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 578 } 579 } 580 } catch (RemoteException re) { 581 Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re); 582 } 583 return null; 584 } 585 586 /** 587 * Performs an accessibility action on an {@link AccessibilityNodeInfo}. 588 * 589 * @param connectionId The id of a connection for interacting with the system. 590 * @param accessibilityWindowId A unique window id. Use 591 * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} 592 * to query the currently active window. 593 * @param accessibilityNodeId A unique view id or virtual descendant id from 594 * where to start the search. Use 595 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 596 * to start from the root. 597 * @param action The action to perform. 598 * @param arguments Optional action arguments. 599 * @return Whether the action was performed. 600 */ performAccessibilityAction(int connectionId, int accessibilityWindowId, long accessibilityNodeId, int action, Bundle arguments)601 public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId, 602 long accessibilityNodeId, int action, Bundle arguments) { 603 try { 604 IAccessibilityServiceConnection connection = getConnection(connectionId); 605 if (connection != null) { 606 final int interactionId = mInteractionIdCounter.getAndIncrement(); 607 final long identityToken = Binder.clearCallingIdentity(); 608 final boolean success; 609 try { 610 success = connection.performAccessibilityAction( 611 accessibilityWindowId, accessibilityNodeId, action, arguments, 612 interactionId, this, Thread.currentThread().getId()); 613 } finally { 614 Binder.restoreCallingIdentity(identityToken); 615 } 616 617 if (success) { 618 return getPerformAccessibilityActionResultAndClear(interactionId); 619 } 620 } else { 621 if (DEBUG) { 622 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 623 } 624 } 625 } catch (RemoteException re) { 626 Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re); 627 } 628 return false; 629 } 630 631 @UnsupportedAppUsage clearCache()632 public void clearCache() { 633 sAccessibilityCache.clear(); 634 } 635 onAccessibilityEvent(AccessibilityEvent event)636 public void onAccessibilityEvent(AccessibilityEvent event) { 637 sAccessibilityCache.onAccessibilityEvent(event); 638 } 639 640 /** 641 * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}. 642 * 643 * @param interactionId The interaction id to match the result with the request. 644 * @return The result {@link AccessibilityNodeInfo}. 645 */ getFindAccessibilityNodeInfoResultAndClear(int interactionId)646 private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) { 647 synchronized (mInstanceLock) { 648 final boolean success = waitForResultTimedLocked(interactionId); 649 AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null; 650 clearResultLocked(); 651 return result; 652 } 653 } 654 655 /** 656 * {@inheritDoc} 657 */ setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId)658 public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, 659 int interactionId) { 660 synchronized (mInstanceLock) { 661 if (interactionId > mInteractionId) { 662 mFindAccessibilityNodeInfoResult = info; 663 mInteractionId = interactionId; 664 } 665 mInstanceLock.notifyAll(); 666 } 667 } 668 669 /** 670 * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s. 671 * 672 * @param interactionId The interaction id to match the result with the request. 673 * @return The result {@link AccessibilityNodeInfo}s. 674 */ getFindAccessibilityNodeInfosResultAndClear( int interactionId)675 private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear( 676 int interactionId) { 677 synchronized (mInstanceLock) { 678 final boolean success = waitForResultTimedLocked(interactionId); 679 final List<AccessibilityNodeInfo> result; 680 if (success) { 681 result = mFindAccessibilityNodeInfosResult; 682 } else { 683 result = Collections.emptyList(); 684 } 685 clearResultLocked(); 686 if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) { 687 checkFindAccessibilityNodeInfoResultIntegrity(result); 688 } 689 return result; 690 } 691 } 692 693 /** 694 * {@inheritDoc} 695 */ setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, int interactionId)696 public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, 697 int interactionId) { 698 synchronized (mInstanceLock) { 699 if (interactionId > mInteractionId) { 700 if (infos != null) { 701 // If the call is not an IPC, i.e. it is made from the same process, we need to 702 // instantiate new result list to avoid passing internal instances to clients. 703 final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid()); 704 if (!isIpcCall) { 705 mFindAccessibilityNodeInfosResult = new ArrayList<>(infos); 706 } else { 707 mFindAccessibilityNodeInfosResult = infos; 708 } 709 } else { 710 mFindAccessibilityNodeInfosResult = Collections.emptyList(); 711 } 712 mInteractionId = interactionId; 713 } 714 mInstanceLock.notifyAll(); 715 } 716 } 717 718 /** 719 * Gets the result of a request to perform an accessibility action. 720 * 721 * @param interactionId The interaction id to match the result with the request. 722 * @return Whether the action was performed. 723 */ getPerformAccessibilityActionResultAndClear(int interactionId)724 private boolean getPerformAccessibilityActionResultAndClear(int interactionId) { 725 synchronized (mInstanceLock) { 726 final boolean success = waitForResultTimedLocked(interactionId); 727 final boolean result = success ? mPerformAccessibilityActionResult : false; 728 clearResultLocked(); 729 return result; 730 } 731 } 732 733 /** 734 * {@inheritDoc} 735 */ setPerformAccessibilityActionResult(boolean succeeded, int interactionId)736 public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) { 737 synchronized (mInstanceLock) { 738 if (interactionId > mInteractionId) { 739 mPerformAccessibilityActionResult = succeeded; 740 mInteractionId = interactionId; 741 } 742 mInstanceLock.notifyAll(); 743 } 744 } 745 746 /** 747 * Clears the result state. 748 */ clearResultLocked()749 private void clearResultLocked() { 750 mInteractionId = -1; 751 mFindAccessibilityNodeInfoResult = null; 752 mFindAccessibilityNodeInfosResult = null; 753 mPerformAccessibilityActionResult = false; 754 } 755 756 /** 757 * Waits up to a given bound for a result of a request and returns it. 758 * 759 * @param interactionId The interaction id to match the result with the request. 760 * @return Whether the result was received. 761 */ waitForResultTimedLocked(int interactionId)762 private boolean waitForResultTimedLocked(int interactionId) { 763 long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS; 764 final long startTimeMillis = SystemClock.uptimeMillis(); 765 while (true) { 766 try { 767 Message sameProcessMessage = getSameProcessMessageAndClear(); 768 if (sameProcessMessage != null) { 769 sameProcessMessage.getTarget().handleMessage(sameProcessMessage); 770 } 771 772 if (mInteractionId == interactionId) { 773 return true; 774 } 775 if (mInteractionId > interactionId) { 776 return false; 777 } 778 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 779 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis; 780 if (waitTimeMillis <= 0) { 781 return false; 782 } 783 mInstanceLock.wait(waitTimeMillis); 784 } catch (InterruptedException ie) { 785 /* ignore */ 786 } 787 } 788 } 789 790 /** 791 * Finalize an {@link AccessibilityNodeInfo} before passing it to the client. 792 * 793 * @param info The info. 794 * @param connectionId The id of the connection to the system. 795 * @param bypassCache Whether or not to bypass the cache. The node is added to the cache if 796 * this value is {@code false} 797 * @param packageNames The valid package names a node can come from. 798 */ finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId, boolean bypassCache, String[] packageNames)799 private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, 800 int connectionId, boolean bypassCache, String[] packageNames) { 801 if (info != null) { 802 info.setConnectionId(connectionId); 803 // Empty array means any package name is Okay 804 if (!ArrayUtils.isEmpty(packageNames)) { 805 CharSequence packageName = info.getPackageName(); 806 if (packageName == null 807 || !ArrayUtils.contains(packageNames, packageName.toString())) { 808 // If the node package not one of the valid ones, pick the top one - this 809 // is one of the packages running in the introspected UID. 810 info.setPackageName(packageNames[0]); 811 } 812 } 813 info.setSealed(true); 814 if (!bypassCache) { 815 sAccessibilityCache.add(info); 816 } 817 } 818 } 819 820 /** 821 * Finalize {@link AccessibilityNodeInfo}s before passing them to the client. 822 * 823 * @param infos The {@link AccessibilityNodeInfo}s. 824 * @param connectionId The id of the connection to the system. 825 * @param bypassCache Whether or not to bypass the cache. The nodes are added to the cache if 826 * this value is {@code false} 827 * @param packageNames The valid package names a node can come from. 828 */ finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, int connectionId, boolean bypassCache, String[] packageNames)829 private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, 830 int connectionId, boolean bypassCache, String[] packageNames) { 831 if (infos != null) { 832 final int infosCount = infos.size(); 833 for (int i = 0; i < infosCount; i++) { 834 AccessibilityNodeInfo info = infos.get(i); 835 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, 836 bypassCache, packageNames); 837 } 838 } 839 } 840 841 /** 842 * Gets the message stored if the interacted and interacting 843 * threads are the same. 844 * 845 * @return The message. 846 */ getSameProcessMessageAndClear()847 private Message getSameProcessMessageAndClear() { 848 synchronized (mInstanceLock) { 849 Message result = mSameThreadMessage; 850 mSameThreadMessage = null; 851 return result; 852 } 853 } 854 855 /** 856 * Checks whether the infos are a fully connected tree with no duplicates. 857 * 858 * @param infos The result list to check. 859 */ checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos)860 private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) { 861 if (infos.size() == 0) { 862 return; 863 } 864 // Find the root node. 865 AccessibilityNodeInfo root = infos.get(0); 866 final int infoCount = infos.size(); 867 for (int i = 1; i < infoCount; i++) { 868 for (int j = i; j < infoCount; j++) { 869 AccessibilityNodeInfo candidate = infos.get(j); 870 if (root.getParentNodeId() == candidate.getSourceNodeId()) { 871 root = candidate; 872 break; 873 } 874 } 875 } 876 if (root == null) { 877 Log.e(LOG_TAG, "No root."); 878 } 879 // Check for duplicates. 880 HashSet<AccessibilityNodeInfo> seen = new HashSet<>(); 881 Queue<AccessibilityNodeInfo> fringe = new LinkedList<>(); 882 fringe.add(root); 883 while (!fringe.isEmpty()) { 884 AccessibilityNodeInfo current = fringe.poll(); 885 if (!seen.add(current)) { 886 Log.e(LOG_TAG, "Duplicate node."); 887 return; 888 } 889 final int childCount = current.getChildCount(); 890 for (int i = 0; i < childCount; i++) { 891 final long childId = current.getChildId(i); 892 for (int j = 0; j < infoCount; j++) { 893 AccessibilityNodeInfo child = infos.get(j); 894 if (child.getSourceNodeId() == childId) { 895 fringe.add(child); 896 } 897 } 898 } 899 } 900 final int disconnectedCount = infos.size() - seen.size(); 901 if (disconnectedCount > 0) { 902 Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes."); 903 } 904 } 905 } 906