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