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