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