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