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.graphics.Rect; 21import android.os.Binder; 22import android.os.Build; 23import android.os.Bundle; 24import android.os.Message; 25import android.os.Process; 26import android.os.RemoteException; 27import android.os.SystemClock; 28import android.util.Log; 29import android.util.LongSparseArray; 30import android.util.SparseArray; 31import android.util.SparseLongArray; 32 33import java.util.ArrayList; 34import java.util.Collections; 35import java.util.HashSet; 36import java.util.LinkedList; 37import java.util.List; 38import java.util.Queue; 39import java.util.concurrent.atomic.AtomicInteger; 40 41/** 42 * This class is a singleton that performs accessibility interaction 43 * which is it queries remote view hierarchies about snapshots of their 44 * views as well requests from these hierarchies to perform certain 45 * actions on their views. 46 * 47 * Rationale: The content retrieval APIs are synchronous from a client's 48 * perspective but internally they are asynchronous. The client thread 49 * calls into the system requesting an action and providing a callback 50 * to receive the result after which it waits up to a timeout for that 51 * result. The system enforces security and the delegates the request 52 * to a given view hierarchy where a message is posted (from a binder 53 * thread) describing what to be performed by the main UI thread the 54 * result of which it delivered via the mentioned callback. However, 55 * the blocked client thread and the main UI thread of the target view 56 * hierarchy can be the same thread, for example an accessibility service 57 * and an activity run in the same process, thus they are executed on the 58 * same main thread. In such a case the retrieval will fail since the UI 59 * thread that has to process the message describing the work to be done 60 * is blocked waiting for a result is has to compute! To avoid this scenario 61 * when making a call the client also passes its process and thread ids so 62 * the accessed view hierarchy can detect if the client making the request 63 * is running in its main UI thread. In such a case the view hierarchy, 64 * specifically the binder thread performing the IPC to it, does not post a 65 * message to be run on the UI thread but passes it to the singleton 66 * interaction client through which all interactions occur and the latter is 67 * responsible to execute the message before starting to wait for the 68 * asynchronous result delivered via the callback. In this case the expected 69 * result is already received so no waiting is performed. 70 * 71 * @hide 72 */ 73public final class AccessibilityInteractionClient 74 extends IAccessibilityInteractionConnectionCallback.Stub { 75 76 public static final int NO_ID = -1; 77 78 private static final String LOG_TAG = "AccessibilityInteractionClient"; 79 80 private static final boolean DEBUG = false; 81 82 private static final boolean CHECK_INTEGRITY = true; 83 84 private static final long TIMEOUT_INTERACTION_MILLIS = 5000; 85 86 private static final Object sStaticLock = new Object(); 87 88 private static final LongSparseArray<AccessibilityInteractionClient> sClients = 89 new LongSparseArray<AccessibilityInteractionClient>(); 90 91 private final AtomicInteger mInteractionIdCounter = new AtomicInteger(); 92 93 private final Object mInstanceLock = new Object(); 94 95 private volatile int mInteractionId = -1; 96 97 private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult; 98 99 private List<AccessibilityNodeInfo> mFindAccessibilityNodeInfosResult; 100 101 private boolean mPerformAccessibilityActionResult; 102 103 private Message mSameThreadMessage; 104 105 private final Rect mTempBounds = new Rect(); 106 107 // The connection cache is shared between all interrogating threads. 108 private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache = 109 new SparseArray<IAccessibilityServiceConnection>(); 110 111 // The connection cache is shared between all interrogating threads since 112 // at any given time there is only one window allowing querying. 113 private static final AccessibilityNodeInfoCache sAccessibilityNodeInfoCache = 114 new AccessibilityNodeInfoCache(); 115 116 /** 117 * @return The client for the current thread. 118 */ 119 public static AccessibilityInteractionClient getInstance() { 120 final long threadId = Thread.currentThread().getId(); 121 return getInstanceForThread(threadId); 122 } 123 124 /** 125 * <strong>Note:</strong> We keep one instance per interrogating thread since 126 * the instance contains state which can lead to undesired thread interleavings. 127 * We do not have a thread local variable since other threads should be able to 128 * look up the correct client knowing a thread id. See ViewRootImpl for details. 129 * 130 * @return The client for a given <code>threadId</code>. 131 */ 132 public static AccessibilityInteractionClient getInstanceForThread(long threadId) { 133 synchronized (sStaticLock) { 134 AccessibilityInteractionClient client = sClients.get(threadId); 135 if (client == null) { 136 client = new AccessibilityInteractionClient(); 137 sClients.put(threadId, client); 138 } 139 return client; 140 } 141 } 142 143 private AccessibilityInteractionClient() { 144 /* reducing constructor visibility */ 145 } 146 147 /** 148 * Sets the message to be processed if the interacted view hierarchy 149 * and the interacting client are running in the same thread. 150 * 151 * @param message The message. 152 */ 153 public void setSameThreadMessage(Message message) { 154 synchronized (mInstanceLock) { 155 mSameThreadMessage = message; 156 mInstanceLock.notifyAll(); 157 } 158 } 159 160 /** 161 * Gets the root {@link AccessibilityNodeInfo} in the currently active window. 162 * 163 * @param connectionId The id of a connection for interacting with the system. 164 * @return The root {@link AccessibilityNodeInfo} if found, null otherwise. 165 */ 166 public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) { 167 return findAccessibilityNodeInfoByAccessibilityId(connectionId, 168 AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, 169 AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); 170 } 171 172 /** 173 * Finds an {@link AccessibilityNodeInfo} by accessibility id. 174 * 175 * @param connectionId The id of a connection for interacting with the system. 176 * @param accessibilityWindowId A unique window id. Use 177 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 178 * to query the currently active window. 179 * @param accessibilityNodeId A unique view id or virtual descendant id from 180 * where to start the search. Use 181 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 182 * to start from the root. 183 * @param prefetchFlags flags to guide prefetching. 184 * @return An {@link AccessibilityNodeInfo} if found, null otherwise. 185 */ 186 public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, 187 int accessibilityWindowId, long accessibilityNodeId, int prefetchFlags) { 188 try { 189 IAccessibilityServiceConnection connection = getConnection(connectionId); 190 if (connection != null) { 191 AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get( 192 accessibilityNodeId); 193 if (cachedInfo != null) { 194 return cachedInfo; 195 } 196 final int interactionId = mInteractionIdCounter.getAndIncrement(); 197 final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId( 198 accessibilityWindowId, accessibilityNodeId, interactionId, this, 199 prefetchFlags, Thread.currentThread().getId()); 200 // If the scale is zero the call has failed. 201 if (windowScale > 0) { 202 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 203 interactionId); 204 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, windowScale); 205 if (infos != null && !infos.isEmpty()) { 206 return infos.get(0); 207 } 208 } 209 } else { 210 if (DEBUG) { 211 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 212 } 213 } 214 } catch (RemoteException re) { 215 if (DEBUG) { 216 Log.w(LOG_TAG, "Error while calling remote" 217 + " findAccessibilityNodeInfoByAccessibilityId", re); 218 } 219 } 220 return null; 221 } 222 223 /** 224 * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in 225 * the window whose id is specified and starts from the node whose accessibility 226 * id is specified. 227 * 228 * @param connectionId The id of a connection for interacting with the system. 229 * @param accessibilityWindowId A unique window id. Use 230 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 231 * to query the currently active window. 232 * @param accessibilityNodeId A unique view id or virtual descendant id from 233 * where to start the search. Use 234 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 235 * to start from the root. 236 * @param viewId The id of the view. 237 * @return An {@link AccessibilityNodeInfo} if found, null otherwise. 238 */ 239 public AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int connectionId, 240 int accessibilityWindowId, long accessibilityNodeId, int viewId) { 241 try { 242 IAccessibilityServiceConnection connection = getConnection(connectionId); 243 if (connection != null) { 244 final int interactionId = mInteractionIdCounter.getAndIncrement(); 245 final float windowScale = 246 connection.findAccessibilityNodeInfoByViewId(accessibilityWindowId, 247 accessibilityNodeId, viewId, interactionId, this, 248 Thread.currentThread().getId()); 249 // If the scale is zero the call has failed. 250 if (windowScale > 0) { 251 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 252 interactionId); 253 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); 254 return info; 255 } 256 } else { 257 if (DEBUG) { 258 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 259 } 260 } 261 } catch (RemoteException re) { 262 if (DEBUG) { 263 Log.w(LOG_TAG, "Error while calling remote" 264 + " findAccessibilityNodeInfoByViewIdInActiveWindow", re); 265 } 266 } 267 return null; 268 } 269 270 /** 271 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case 272 * insensitive containment. The search is performed in the window whose 273 * id is specified and starts from the node whose accessibility id is 274 * specified. 275 * 276 * @param connectionId The id of a connection for interacting with the system. 277 * @param accessibilityWindowId A unique window id. Use 278 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 279 * to query the currently active window. 280 * @param accessibilityNodeId A unique view id or virtual descendant id from 281 * where to start the search. Use 282 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 283 * to start from the root. 284 * @param text The searched text. 285 * @return A list of found {@link AccessibilityNodeInfo}s. 286 */ 287 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId, 288 int accessibilityWindowId, long accessibilityNodeId, String text) { 289 try { 290 IAccessibilityServiceConnection connection = getConnection(connectionId); 291 if (connection != null) { 292 final int interactionId = mInteractionIdCounter.getAndIncrement(); 293 final float windowScale = connection.findAccessibilityNodeInfosByText( 294 accessibilityWindowId, accessibilityNodeId, text, interactionId, this, 295 Thread.currentThread().getId()); 296 // If the scale is zero the call has failed. 297 if (windowScale > 0) { 298 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 299 interactionId); 300 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, windowScale); 301 return infos; 302 } 303 } else { 304 if (DEBUG) { 305 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 306 } 307 } 308 } catch (RemoteException re) { 309 if (DEBUG) { 310 Log.w(LOG_TAG, "Error while calling remote" 311 + " findAccessibilityNodeInfosByViewText", re); 312 } 313 } 314 return Collections.emptyList(); 315 } 316 317 /** 318 * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the 319 * specified focus type. The search is performed in the window whose id is specified 320 * and starts from the node whose accessibility id is specified. 321 * 322 * @param connectionId The id of a connection for interacting with the system. 323 * @param accessibilityWindowId A unique window id. Use 324 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 325 * to query the currently active window. 326 * @param accessibilityNodeId A unique view id or virtual descendant id from 327 * where to start the search. Use 328 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 329 * to start from the root. 330 * @param focusType The focus type. 331 * @return The accessibility focused {@link AccessibilityNodeInfo}. 332 */ 333 public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId, 334 long accessibilityNodeId, int focusType) { 335 try { 336 IAccessibilityServiceConnection connection = getConnection(connectionId); 337 if (connection != null) { 338 final int interactionId = mInteractionIdCounter.getAndIncrement(); 339 final float windowScale = connection.findFocus(accessibilityWindowId, 340 accessibilityNodeId, focusType, interactionId, this, 341 Thread.currentThread().getId()); 342 // If the scale is zero the call has failed. 343 if (windowScale > 0) { 344 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 345 interactionId); 346 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); 347 return info; 348 } 349 } else { 350 if (DEBUG) { 351 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 352 } 353 } 354 } catch (RemoteException re) { 355 if (DEBUG) { 356 Log.w(LOG_TAG, "Error while calling remote findAccessibilityFocus", re); 357 } 358 } 359 return null; 360 } 361 362 /** 363 * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}. 364 * The search is performed in the window whose id is specified and starts from the 365 * node whose accessibility id is specified. 366 * 367 * @param connectionId The id of a connection for interacting with the system. 368 * @param accessibilityWindowId A unique window id. Use 369 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 370 * to query the currently active window. 371 * @param accessibilityNodeId A unique view id or virtual descendant id from 372 * where to start the search. Use 373 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 374 * to start from the root. 375 * @param direction The direction in which to search for focusable. 376 * @return The accessibility focused {@link AccessibilityNodeInfo}. 377 */ 378 public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId, 379 long accessibilityNodeId, int direction) { 380 try { 381 IAccessibilityServiceConnection connection = getConnection(connectionId); 382 if (connection != null) { 383 final int interactionId = mInteractionIdCounter.getAndIncrement(); 384 final float windowScale = connection.focusSearch(accessibilityWindowId, 385 accessibilityNodeId, direction, interactionId, this, 386 Thread.currentThread().getId()); 387 // If the scale is zero the call has failed. 388 if (windowScale > 0) { 389 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 390 interactionId); 391 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); 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 * Applies compatibility scale to the info bounds if it is not equal to one. 608 * 609 * @param info The info whose bounds to scale. 610 * @param scale The scale to apply. 611 */ 612 private void applyCompatibilityScaleIfNeeded(AccessibilityNodeInfo info, float scale) { 613 if (scale == 1.0f) { 614 return; 615 } 616 Rect bounds = mTempBounds; 617 info.getBoundsInParent(bounds); 618 bounds.scale(scale); 619 info.setBoundsInParent(bounds); 620 621 info.getBoundsInScreen(bounds); 622 bounds.scale(scale); 623 info.setBoundsInScreen(bounds); 624 } 625 626 /** 627 * Finalize an {@link AccessibilityNodeInfo} before passing it to the client. 628 * 629 * @param info The info. 630 * @param connectionId The id of the connection to the system. 631 * @param windowScale The source window compatibility scale. 632 */ 633 private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId, 634 float windowScale) { 635 if (info != null) { 636 applyCompatibilityScaleIfNeeded(info, windowScale); 637 info.setConnectionId(connectionId); 638 info.setSealed(true); 639 sAccessibilityNodeInfoCache.add(info); 640 } 641 } 642 643 /** 644 * Finalize {@link AccessibilityNodeInfo}s before passing them to the client. 645 * 646 * @param infos The {@link AccessibilityNodeInfo}s. 647 * @param connectionId The id of the connection to the system. 648 * @param windowScale The source window compatibility scale. 649 */ 650 private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, 651 int connectionId, float windowScale) { 652 if (infos != null) { 653 final int infosCount = infos.size(); 654 for (int i = 0; i < infosCount; i++) { 655 AccessibilityNodeInfo info = infos.get(i); 656 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); 657 } 658 } 659 } 660 661 /** 662 * Gets the message stored if the interacted and interacting 663 * threads are the same. 664 * 665 * @return The message. 666 */ 667 private Message getSameProcessMessageAndClear() { 668 synchronized (mInstanceLock) { 669 Message result = mSameThreadMessage; 670 mSameThreadMessage = null; 671 return result; 672 } 673 } 674 675 /** 676 * Gets a cached accessibility service connection. 677 * 678 * @param connectionId The connection id. 679 * @return The cached connection if such. 680 */ 681 public IAccessibilityServiceConnection getConnection(int connectionId) { 682 synchronized (sConnectionCache) { 683 return sConnectionCache.get(connectionId); 684 } 685 } 686 687 /** 688 * Adds a cached accessibility service connection. 689 * 690 * @param connectionId The connection id. 691 * @param connection The connection. 692 */ 693 public void addConnection(int connectionId, IAccessibilityServiceConnection connection) { 694 synchronized (sConnectionCache) { 695 sConnectionCache.put(connectionId, connection); 696 } 697 } 698 699 /** 700 * Removes a cached accessibility service connection. 701 * 702 * @param connectionId The connection id. 703 */ 704 public void removeConnection(int connectionId) { 705 synchronized (sConnectionCache) { 706 sConnectionCache.remove(connectionId); 707 } 708 } 709 710 /** 711 * Checks whether the infos are a fully connected tree with no duplicates. 712 * 713 * @param infos The result list to check. 714 */ 715 private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) { 716 if (infos.size() == 0) { 717 return; 718 } 719 // Find the root node. 720 AccessibilityNodeInfo root = infos.get(0); 721 final int infoCount = infos.size(); 722 for (int i = 1; i < infoCount; i++) { 723 for (int j = i; j < infoCount; j++) { 724 AccessibilityNodeInfo candidate = infos.get(j); 725 if (root.getParentNodeId() == candidate.getSourceNodeId()) { 726 root = candidate; 727 break; 728 } 729 } 730 } 731 if (root == null) { 732 Log.e(LOG_TAG, "No root."); 733 } 734 // Check for duplicates. 735 HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>(); 736 Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>(); 737 fringe.add(root); 738 while (!fringe.isEmpty()) { 739 AccessibilityNodeInfo current = fringe.poll(); 740 if (!seen.add(current)) { 741 Log.e(LOG_TAG, "Duplicate node."); 742 return; 743 } 744 SparseLongArray childIds = current.getChildNodeIds(); 745 final int childCount = childIds.size(); 746 for (int i = 0; i < childCount; i++) { 747 final long childId = childIds.valueAt(i); 748 for (int j = 0; j < infoCount; j++) { 749 AccessibilityNodeInfo child = infos.get(j); 750 if (child.getSourceNodeId() == childId) { 751 fringe.add(child); 752 } 753 } 754 } 755 } 756 final int disconnectedCount = infos.size() - seen.size(); 757 if (disconnectedCount > 0) { 758 Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes."); 759 } 760 } 761} 762