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