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