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