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 false, 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 bypassCache Whether to bypass the cache while looking for the node. 181 * @param prefetchFlags flags to guide prefetching. 182 * @return An {@link AccessibilityNodeInfo} if found, null otherwise. 183 */ 184 public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, 185 int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache, 186 int prefetchFlags) { 187 try { 188 IAccessibilityServiceConnection connection = getConnection(connectionId); 189 if (connection != null) { 190 if (!bypassCache) { 191 AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get( 192 accessibilityNodeId); 193 if (cachedInfo != null) { 194 return cachedInfo; 195 } 196 } 197 final int interactionId = mInteractionIdCounter.getAndIncrement(); 198 final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId( 199 accessibilityWindowId, accessibilityNodeId, interactionId, this, 200 prefetchFlags, Thread.currentThread().getId()); 201 // If the scale is zero the call has failed. 202 if (success) { 203 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 204 interactionId); 205 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); 206 if (infos != null && !infos.isEmpty()) { 207 return infos.get(0); 208 } 209 } 210 } else { 211 if (DEBUG) { 212 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 213 } 214 } 215 } catch (RemoteException re) { 216 if (DEBUG) { 217 Log.w(LOG_TAG, "Error while calling remote" 218 + " findAccessibilityNodeInfoByAccessibilityId", re); 219 } 220 } 221 return null; 222 } 223 224 /** 225 * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in 226 * the window whose id is specified and starts from the node whose accessibility 227 * id is specified. 228 * 229 * @param connectionId The id of a connection for interacting with the system. 230 * @param accessibilityWindowId A unique window id. Use 231 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 232 * to query the currently active window. 233 * @param accessibilityNodeId A unique view id or virtual descendant id from 234 * where to start the search. Use 235 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 236 * to start from the root. 237 * @param viewId The fully qualified resource name of the view id to find. 238 * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise. 239 */ 240 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId, 241 int accessibilityWindowId, long accessibilityNodeId, String viewId) { 242 try { 243 IAccessibilityServiceConnection connection = getConnection(connectionId); 244 if (connection != null) { 245 final int interactionId = mInteractionIdCounter.getAndIncrement(); 246 final boolean success = connection.findAccessibilityNodeInfosByViewId( 247 accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this, 248 Thread.currentThread().getId()); 249 if (success) { 250 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 251 interactionId); 252 if (infos != null) { 253 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); 254 return infos; 255 } 256 } 257 } else { 258 if (DEBUG) { 259 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 260 } 261 } 262 } catch (RemoteException re) { 263 if (DEBUG) { 264 Log.w(LOG_TAG, "Error while calling remote" 265 + " findAccessibilityNodeInfoByViewIdInActiveWindow", re); 266 } 267 } 268 return Collections.emptyList(); 269 } 270 271 /** 272 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case 273 * insensitive containment. The search is performed in the window whose 274 * id is specified and starts from the node whose accessibility id is 275 * specified. 276 * 277 * @param connectionId The id of a connection for interacting with the system. 278 * @param accessibilityWindowId A unique window id. Use 279 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 280 * to query the currently active window. 281 * @param accessibilityNodeId A unique view id or virtual descendant id from 282 * where to start the search. Use 283 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 284 * to start from the root. 285 * @param text The searched text. 286 * @return A list of found {@link AccessibilityNodeInfo}s. 287 */ 288 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId, 289 int accessibilityWindowId, long accessibilityNodeId, String text) { 290 try { 291 IAccessibilityServiceConnection connection = getConnection(connectionId); 292 if (connection != null) { 293 final int interactionId = mInteractionIdCounter.getAndIncrement(); 294 final boolean success = connection.findAccessibilityNodeInfosByText( 295 accessibilityWindowId, accessibilityNodeId, text, interactionId, this, 296 Thread.currentThread().getId()); 297 if (success) { 298 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 299 interactionId); 300 if (infos != null) { 301 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); 302 return infos; 303 } 304 } 305 } else { 306 if (DEBUG) { 307 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 308 } 309 } 310 } catch (RemoteException re) { 311 if (DEBUG) { 312 Log.w(LOG_TAG, "Error while calling remote" 313 + " findAccessibilityNodeInfosByViewText", re); 314 } 315 } 316 return Collections.emptyList(); 317 } 318 319 /** 320 * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the 321 * specified focus type. The search is performed in the window whose id is specified 322 * and starts from the node whose accessibility id is specified. 323 * 324 * @param connectionId The id of a connection for interacting with the system. 325 * @param accessibilityWindowId A unique window id. Use 326 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 327 * to query the currently active window. 328 * @param accessibilityNodeId A unique view id or virtual descendant id from 329 * where to start the search. Use 330 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 331 * to start from the root. 332 * @param focusType The focus type. 333 * @return The accessibility focused {@link AccessibilityNodeInfo}. 334 */ 335 public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId, 336 long accessibilityNodeId, int focusType) { 337 try { 338 IAccessibilityServiceConnection connection = getConnection(connectionId); 339 if (connection != null) { 340 final int interactionId = mInteractionIdCounter.getAndIncrement(); 341 final boolean success = connection.findFocus(accessibilityWindowId, 342 accessibilityNodeId, focusType, interactionId, this, 343 Thread.currentThread().getId()); 344 if (success) { 345 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 346 interactionId); 347 finalizeAndCacheAccessibilityNodeInfo(info, connectionId); 348 return info; 349 } 350 } else { 351 if (DEBUG) { 352 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 353 } 354 } 355 } catch (RemoteException re) { 356 if (DEBUG) { 357 Log.w(LOG_TAG, "Error while calling remote findFocus", re); 358 } 359 } 360 return null; 361 } 362 363 /** 364 * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}. 365 * The search is performed in the window whose id is specified and starts from the 366 * node whose accessibility id is specified. 367 * 368 * @param connectionId The id of a connection for interacting with the system. 369 * @param accessibilityWindowId A unique window id. Use 370 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 371 * to query the currently active window. 372 * @param accessibilityNodeId A unique view id or virtual descendant id from 373 * where to start the search. Use 374 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 375 * to start from the root. 376 * @param direction The direction in which to search for focusable. 377 * @return The accessibility focused {@link AccessibilityNodeInfo}. 378 */ 379 public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId, 380 long accessibilityNodeId, int direction) { 381 try { 382 IAccessibilityServiceConnection connection = getConnection(connectionId); 383 if (connection != null) { 384 final int interactionId = mInteractionIdCounter.getAndIncrement(); 385 final boolean success = connection.focusSearch(accessibilityWindowId, 386 accessibilityNodeId, direction, interactionId, this, 387 Thread.currentThread().getId()); 388 if (success) { 389 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 390 interactionId); 391 finalizeAndCacheAccessibilityNodeInfo(info, connectionId); 392 return info; 393 } 394 } else { 395 if (DEBUG) { 396 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 397 } 398 } 399 } catch (RemoteException re) { 400 if (DEBUG) { 401 Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re); 402 } 403 } 404 return null; 405 } 406 407 /** 408 * Performs an accessibility action on an {@link AccessibilityNodeInfo}. 409 * 410 * @param connectionId The id of a connection for interacting with the system. 411 * @param accessibilityWindowId A unique window id. Use 412 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 413 * to query the currently active window. 414 * @param accessibilityNodeId A unique view id or virtual descendant id from 415 * where to start the search. Use 416 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 417 * to start from the root. 418 * @param action The action to perform. 419 * @param arguments Optional action arguments. 420 * @return Whether the action was performed. 421 */ 422 public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId, 423 long accessibilityNodeId, int action, Bundle arguments) { 424 try { 425 IAccessibilityServiceConnection connection = getConnection(connectionId); 426 if (connection != null) { 427 final int interactionId = mInteractionIdCounter.getAndIncrement(); 428 final boolean success = connection.performAccessibilityAction( 429 accessibilityWindowId, accessibilityNodeId, action, arguments, 430 interactionId, this, Thread.currentThread().getId()); 431 if (success) { 432 return getPerformAccessibilityActionResultAndClear(interactionId); 433 } 434 } else { 435 if (DEBUG) { 436 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 437 } 438 } 439 } catch (RemoteException re) { 440 if (DEBUG) { 441 Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re); 442 } 443 } 444 return false; 445 } 446 447 public void clearCache() { 448 sAccessibilityNodeInfoCache.clear(); 449 } 450 451 public void onAccessibilityEvent(AccessibilityEvent event) { 452 sAccessibilityNodeInfoCache.onAccessibilityEvent(event); 453 } 454 455 /** 456 * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}. 457 * 458 * @param interactionId The interaction id to match the result with the request. 459 * @return The result {@link AccessibilityNodeInfo}. 460 */ 461 private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) { 462 synchronized (mInstanceLock) { 463 final boolean success = waitForResultTimedLocked(interactionId); 464 AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null; 465 clearResultLocked(); 466 return result; 467 } 468 } 469 470 /** 471 * {@inheritDoc} 472 */ 473 public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, 474 int interactionId) { 475 synchronized (mInstanceLock) { 476 if (interactionId > mInteractionId) { 477 mFindAccessibilityNodeInfoResult = info; 478 mInteractionId = interactionId; 479 } 480 mInstanceLock.notifyAll(); 481 } 482 } 483 484 /** 485 * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s. 486 * 487 * @param interactionId The interaction id to match the result with the request. 488 * @return The result {@link AccessibilityNodeInfo}s. 489 */ 490 private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear( 491 int interactionId) { 492 synchronized (mInstanceLock) { 493 final boolean success = waitForResultTimedLocked(interactionId); 494 List<AccessibilityNodeInfo> result = null; 495 if (success) { 496 result = mFindAccessibilityNodeInfosResult; 497 } else { 498 result = Collections.emptyList(); 499 } 500 clearResultLocked(); 501 if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) { 502 checkFindAccessibilityNodeInfoResultIntegrity(result); 503 } 504 return result; 505 } 506 } 507 508 /** 509 * {@inheritDoc} 510 */ 511 public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, 512 int interactionId) { 513 synchronized (mInstanceLock) { 514 if (interactionId > mInteractionId) { 515 if (infos != null) { 516 // If the call is not an IPC, i.e. it is made from the same process, we need to 517 // instantiate new result list to avoid passing internal instances to clients. 518 final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid()); 519 if (!isIpcCall) { 520 mFindAccessibilityNodeInfosResult = 521 new ArrayList<AccessibilityNodeInfo>(infos); 522 } else { 523 mFindAccessibilityNodeInfosResult = infos; 524 } 525 } else { 526 mFindAccessibilityNodeInfosResult = Collections.emptyList(); 527 } 528 mInteractionId = interactionId; 529 } 530 mInstanceLock.notifyAll(); 531 } 532 } 533 534 /** 535 * Gets the result of a request to perform an accessibility action. 536 * 537 * @param interactionId The interaction id to match the result with the request. 538 * @return Whether the action was performed. 539 */ 540 private boolean getPerformAccessibilityActionResultAndClear(int interactionId) { 541 synchronized (mInstanceLock) { 542 final boolean success = waitForResultTimedLocked(interactionId); 543 final boolean result = success ? mPerformAccessibilityActionResult : false; 544 clearResultLocked(); 545 return result; 546 } 547 } 548 549 /** 550 * {@inheritDoc} 551 */ 552 public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) { 553 synchronized (mInstanceLock) { 554 if (interactionId > mInteractionId) { 555 mPerformAccessibilityActionResult = succeeded; 556 mInteractionId = interactionId; 557 } 558 mInstanceLock.notifyAll(); 559 } 560 } 561 562 /** 563 * Clears the result state. 564 */ 565 private void clearResultLocked() { 566 mInteractionId = -1; 567 mFindAccessibilityNodeInfoResult = null; 568 mFindAccessibilityNodeInfosResult = null; 569 mPerformAccessibilityActionResult = false; 570 } 571 572 /** 573 * Waits up to a given bound for a result of a request and returns it. 574 * 575 * @param interactionId The interaction id to match the result with the request. 576 * @return Whether the result was received. 577 */ 578 private boolean waitForResultTimedLocked(int interactionId) { 579 long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS; 580 final long startTimeMillis = SystemClock.uptimeMillis(); 581 while (true) { 582 try { 583 Message sameProcessMessage = getSameProcessMessageAndClear(); 584 if (sameProcessMessage != null) { 585 sameProcessMessage.getTarget().handleMessage(sameProcessMessage); 586 } 587 588 if (mInteractionId == interactionId) { 589 return true; 590 } 591 if (mInteractionId > interactionId) { 592 return false; 593 } 594 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 595 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis; 596 if (waitTimeMillis <= 0) { 597 return false; 598 } 599 mInstanceLock.wait(waitTimeMillis); 600 } catch (InterruptedException ie) { 601 /* ignore */ 602 } 603 } 604 } 605 606 /** 607 * Finalize an {@link AccessibilityNodeInfo} before passing it to the client. 608 * 609 * @param info The info. 610 * @param connectionId The id of the connection to the system. 611 */ 612 private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, 613 int connectionId) { 614 if (info != null) { 615 info.setConnectionId(connectionId); 616 info.setSealed(true); 617 sAccessibilityNodeInfoCache.add(info); 618 } 619 } 620 621 /** 622 * Finalize {@link AccessibilityNodeInfo}s before passing them to the client. 623 * 624 * @param infos The {@link AccessibilityNodeInfo}s. 625 * @param connectionId The id of the connection to the system. 626 */ 627 private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, 628 int connectionId) { 629 if (infos != null) { 630 final int infosCount = infos.size(); 631 for (int i = 0; i < infosCount; i++) { 632 AccessibilityNodeInfo info = infos.get(i); 633 finalizeAndCacheAccessibilityNodeInfo(info, connectionId); 634 } 635 } 636 } 637 638 /** 639 * Gets the message stored if the interacted and interacting 640 * threads are the same. 641 * 642 * @return The message. 643 */ 644 private Message getSameProcessMessageAndClear() { 645 synchronized (mInstanceLock) { 646 Message result = mSameThreadMessage; 647 mSameThreadMessage = null; 648 return result; 649 } 650 } 651 652 /** 653 * Gets a cached accessibility service connection. 654 * 655 * @param connectionId The connection id. 656 * @return The cached connection if such. 657 */ 658 public IAccessibilityServiceConnection getConnection(int connectionId) { 659 synchronized (sConnectionCache) { 660 return sConnectionCache.get(connectionId); 661 } 662 } 663 664 /** 665 * Adds a cached accessibility service connection. 666 * 667 * @param connectionId The connection id. 668 * @param connection The connection. 669 */ 670 public void addConnection(int connectionId, IAccessibilityServiceConnection connection) { 671 synchronized (sConnectionCache) { 672 sConnectionCache.put(connectionId, connection); 673 } 674 } 675 676 /** 677 * Removes a cached accessibility service connection. 678 * 679 * @param connectionId The connection id. 680 */ 681 public void removeConnection(int connectionId) { 682 synchronized (sConnectionCache) { 683 sConnectionCache.remove(connectionId); 684 } 685 } 686 687 /** 688 * Checks whether the infos are a fully connected tree with no duplicates. 689 * 690 * @param infos The result list to check. 691 */ 692 private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) { 693 if (infos.size() == 0) { 694 return; 695 } 696 // Find the root node. 697 AccessibilityNodeInfo root = infos.get(0); 698 final int infoCount = infos.size(); 699 for (int i = 1; i < infoCount; i++) { 700 for (int j = i; j < infoCount; j++) { 701 AccessibilityNodeInfo candidate = infos.get(j); 702 if (root.getParentNodeId() == candidate.getSourceNodeId()) { 703 root = candidate; 704 break; 705 } 706 } 707 } 708 if (root == null) { 709 Log.e(LOG_TAG, "No root."); 710 } 711 // Check for duplicates. 712 HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>(); 713 Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>(); 714 fringe.add(root); 715 while (!fringe.isEmpty()) { 716 AccessibilityNodeInfo current = fringe.poll(); 717 if (!seen.add(current)) { 718 Log.e(LOG_TAG, "Duplicate node."); 719 return; 720 } 721 SparseLongArray childIds = current.getChildNodeIds(); 722 final int childCount = childIds.size(); 723 for (int i = 0; i < childCount; i++) { 724 final long childId = childIds.valueAt(i); 725 for (int j = 0; j < infoCount; j++) { 726 AccessibilityNodeInfo child = infos.get(j); 727 if (child.getSourceNodeId() == childId) { 728 fringe.add(child); 729 } 730 } 731 } 732 } 733 final int disconnectedCount = infos.size() - seen.size(); 734 if (disconnectedCount > 0) { 735 Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes."); 736 } 737 } 738 } 739