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