AccessibilityInteractionClient.java revision 57c7fd5a43237afc5e8ef31a076e862c0c16c328
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.graphics.Rect; 21import android.os.Message; 22import android.os.RemoteException; 23import android.os.SystemClock; 24import android.util.Log; 25import android.util.LongSparseArray; 26import android.util.SparseArray; 27 28import java.util.ArrayList; 29import java.util.Collections; 30import java.util.List; 31import java.util.concurrent.atomic.AtomicInteger; 32 33/** 34 * This class is a singleton that performs accessibility interaction 35 * which is it queries remote view hierarchies about snapshots of their 36 * views as well requests from these hierarchies to perform certain 37 * actions on their views. 38 * 39 * Rationale: The content retrieval APIs are synchronous from a client's 40 * perspective but internally they are asynchronous. The client thread 41 * calls into the system requesting an action and providing a callback 42 * to receive the result after which it waits up to a timeout for that 43 * result. The system enforces security and the delegates the request 44 * to a given view hierarchy where a message is posted (from a binder 45 * thread) describing what to be performed by the main UI thread the 46 * result of which it delivered via the mentioned callback. However, 47 * the blocked client thread and the main UI thread of the target view 48 * hierarchy can be the same thread, for example an accessibility service 49 * and an activity run in the same process, thus they are executed on the 50 * same main thread. In such a case the retrieval will fail since the UI 51 * thread that has to process the message describing the work to be done 52 * is blocked waiting for a result is has to compute! To avoid this scenario 53 * when making a call the client also passes its process and thread ids so 54 * the accessed view hierarchy can detect if the client making the request 55 * is running in its main UI thread. In such a case the view hierarchy, 56 * specifically the binder thread performing the IPC to it, does not post a 57 * message to be run on the UI thread but passes it to the singleton 58 * interaction client through which all interactions occur and the latter is 59 * responsible to execute the message before starting to wait for the 60 * asynchronous result delivered via the callback. In this case the expected 61 * result is already received so no waiting is performed. 62 * 63 * @hide 64 */ 65public final class AccessibilityInteractionClient 66 extends IAccessibilityInteractionConnectionCallback.Stub { 67 68 public static final int NO_ID = -1; 69 70 private static final String LOG_TAG = "AccessibilityInteractionClient"; 71 72 private static final boolean DEBUG = false; 73 74 private static final long TIMEOUT_INTERACTION_MILLIS = 5000; 75 76 private static final Object sStaticLock = new Object(); 77 78 private static final LongSparseArray<AccessibilityInteractionClient> sClients = 79 new LongSparseArray<AccessibilityInteractionClient>(); 80 81 private final AtomicInteger mInteractionIdCounter = new AtomicInteger(); 82 83 private final Object mInstanceLock = new Object(); 84 85 private int mInteractionId = -1; 86 87 private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult; 88 89 private List<AccessibilityNodeInfo> mFindAccessibilityNodeInfosResult; 90 91 private boolean mPerformAccessibilityActionResult; 92 93 private Message mSameThreadMessage; 94 95 private final Rect mTempBounds = new Rect(); 96 97 // The connection cache is shared between all interrogating threads. 98 private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache = 99 new SparseArray<IAccessibilityServiceConnection>(); 100 101 // The connection cache is shared between all interrogating threads since 102 // at any given time there is only one window allowing querying. 103 private static final AccessibilityNodeInfoCache sAccessibilityNodeInfoCache = 104 new AccessibilityNodeInfoCache(); 105 106 /** 107 * @return The client for the current thread. 108 */ 109 public static AccessibilityInteractionClient getInstance() { 110 final long threadId = Thread.currentThread().getId(); 111 return getInstanceForThread(threadId); 112 } 113 114 /** 115 * <strong>Note:</strong> We keep one instance per interrogating thread since 116 * the instance contains state which can lead to undesired thread interleavings. 117 * We do not have a thread local variable since other threads should be able to 118 * look up the correct client knowing a thread id. See ViewRootImpl for details. 119 * 120 * @return The client for a given <code>threadId</code>. 121 */ 122 public static AccessibilityInteractionClient getInstanceForThread(long threadId) { 123 synchronized (sStaticLock) { 124 AccessibilityInteractionClient client = sClients.get(threadId); 125 if (client == null) { 126 client = new AccessibilityInteractionClient(); 127 sClients.put(threadId, client); 128 } 129 return client; 130 } 131 } 132 133 private AccessibilityInteractionClient() { 134 /* reducing constructor visibility */ 135 } 136 137 /** 138 * Sets the message to be processed if the interacted view hierarchy 139 * and the interacting client are running in the same thread. 140 * 141 * @param message The message. 142 */ 143 public void setSameThreadMessage(Message message) { 144 synchronized (mInstanceLock) { 145 mSameThreadMessage = message; 146 mInstanceLock.notifyAll(); 147 } 148 } 149 150 /** 151 * Finds an {@link AccessibilityNodeInfo} by accessibility id. 152 * 153 * @param connectionId The id of a connection for interacting with the system. 154 * @param accessibilityWindowId A unique window id. Use 155 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 156 * to query the currently active window. 157 * @param accessibilityNodeId A unique view id or virtual descendant id from 158 * where to start the search. Use 159 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 160 * to start from the root. 161 * @param prefetchFlags flags to guide prefetching. 162 * @return An {@link AccessibilityNodeInfo} if found, null otherwise. 163 */ 164 public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, 165 int accessibilityWindowId, long accessibilityNodeId, int prefetchFlags) { 166 try { 167 IAccessibilityServiceConnection connection = getConnection(connectionId); 168 if (connection != null) { 169 AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get( 170 accessibilityNodeId); 171 if (cachedInfo != null) { 172 return cachedInfo; 173 } 174 final int interactionId = mInteractionIdCounter.getAndIncrement(); 175 final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId( 176 accessibilityWindowId, accessibilityNodeId, interactionId, this, 177 Thread.currentThread().getId(), prefetchFlags); 178 // If the scale is zero the call has failed. 179 if (windowScale > 0) { 180 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 181 interactionId); 182 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, windowScale); 183 if (infos != null && !infos.isEmpty()) { 184 return infos.get(0); 185 } 186 } 187 } else { 188 if (DEBUG) { 189 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 190 } 191 } 192 } catch (RemoteException re) { 193 if (DEBUG) { 194 Log.w(LOG_TAG, "Error while calling remote" 195 + " findAccessibilityNodeInfoByAccessibilityId", re); 196 } 197 } 198 return null; 199 } 200 201 /** 202 * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in 203 * the window whose id is specified and starts from the node whose accessibility 204 * id is specified. 205 * 206 * @param connectionId The id of a connection for interacting with the system. 207 * @param accessibilityWindowId A unique window id. Use 208 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 209 * to query the currently active window. 210 * @param accessibilityNodeId A unique view id or virtual descendant id from 211 * where to start the search. Use 212 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 213 * to start from the root. 214 * @param viewId The id of the view. 215 * @return An {@link AccessibilityNodeInfo} if found, null otherwise. 216 */ 217 public AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int connectionId, 218 int accessibilityWindowId, long accessibilityNodeId, int viewId) { 219 try { 220 IAccessibilityServiceConnection connection = getConnection(connectionId); 221 if (connection != null) { 222 final int interactionId = mInteractionIdCounter.getAndIncrement(); 223 final float windowScale = 224 connection.findAccessibilityNodeInfoByViewId(accessibilityWindowId, 225 accessibilityNodeId, viewId, interactionId, this, 226 Thread.currentThread().getId()); 227 // If the scale is zero the call has failed. 228 if (windowScale > 0) { 229 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 230 interactionId); 231 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); 232 return info; 233 } 234 } else { 235 if (DEBUG) { 236 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 237 } 238 } 239 } catch (RemoteException re) { 240 if (DEBUG) { 241 Log.w(LOG_TAG, "Error while calling remote" 242 + " findAccessibilityNodeInfoByViewIdInActiveWindow", re); 243 } 244 } 245 return null; 246 } 247 248 /** 249 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case 250 * insensitive containment. The search is performed in the window whose 251 * id is specified and starts from the node whose accessibility id is 252 * specified. 253 * 254 * @param connectionId The id of a connection for interacting with the system. 255 * @param accessibilityWindowId A unique window id. Use 256 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 257 * to query the currently active window. 258 * @param accessibilityNodeId A unique view id or virtual descendant id from 259 * where to start the search. Use 260 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 261 * to start from the root. 262 * @param text The searched text. 263 * @return A list of found {@link AccessibilityNodeInfo}s. 264 */ 265 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId, 266 int accessibilityWindowId, long accessibilityNodeId, String text) { 267 try { 268 IAccessibilityServiceConnection connection = getConnection(connectionId); 269 if (connection != null) { 270 final int interactionId = mInteractionIdCounter.getAndIncrement(); 271 final float windowScale = connection.findAccessibilityNodeInfosByText( 272 accessibilityWindowId, accessibilityNodeId, text, interactionId, this, 273 Thread.currentThread().getId()); 274 // If the scale is zero the call has failed. 275 if (windowScale > 0) { 276 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 277 interactionId); 278 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, windowScale); 279 return infos; 280 } 281 } else { 282 if (DEBUG) { 283 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 284 } 285 } 286 } catch (RemoteException re) { 287 if (DEBUG) { 288 Log.w(LOG_TAG, "Error while calling remote" 289 + " findAccessibilityNodeInfosByViewText", re); 290 } 291 } 292 return Collections.emptyList(); 293 } 294 295 /** 296 * Performs an accessibility action on an {@link AccessibilityNodeInfo}. 297 * 298 * @param connectionId The id of a connection for interacting with the system. 299 * @param accessibilityWindowId A unique window id. Use 300 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} 301 * to query the currently active window. 302 * @param accessibilityNodeId A unique view id or virtual descendant id from 303 * where to start the search. Use 304 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} 305 * to start from the root. 306 * @param action The action to perform. 307 * @return Whether the action was performed. 308 */ 309 public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId, 310 long accessibilityNodeId, int action) { 311 try { 312 IAccessibilityServiceConnection connection = getConnection(connectionId); 313 if (connection != null) { 314 final int interactionId = mInteractionIdCounter.getAndIncrement(); 315 final boolean success = connection.performAccessibilityAction( 316 accessibilityWindowId, accessibilityNodeId, action, interactionId, this, 317 Thread.currentThread().getId()); 318 if (success) { 319 return getPerformAccessibilityActionResultAndClear(interactionId); 320 } 321 } else { 322 if (DEBUG) { 323 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 324 } 325 } 326 } catch (RemoteException re) { 327 if (DEBUG) { 328 Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re); 329 } 330 } 331 return false; 332 } 333 334 public void clearCache() { 335 sAccessibilityNodeInfoCache.clear(); 336 } 337 338 public void removeCachedNode(long accessibilityNodeId) { 339 sAccessibilityNodeInfoCache.remove(accessibilityNodeId); 340 } 341 342 public void onAccessibilityEvent(AccessibilityEvent event) { 343 sAccessibilityNodeInfoCache.onAccessibilityEvent(event); 344 } 345 346 /** 347 * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}. 348 * 349 * @param interactionId The interaction id to match the result with the request. 350 * @return The result {@link AccessibilityNodeInfo}. 351 */ 352 private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) { 353 synchronized (mInstanceLock) { 354 final boolean success = waitForResultTimedLocked(interactionId); 355 AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null; 356 clearResultLocked(); 357 return result; 358 } 359 } 360 361 /** 362 * {@inheritDoc} 363 */ 364 public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, 365 int interactionId) { 366 synchronized (mInstanceLock) { 367 if (interactionId > mInteractionId) { 368 mFindAccessibilityNodeInfoResult = info; 369 mInteractionId = interactionId; 370 } 371 mInstanceLock.notifyAll(); 372 } 373 } 374 375 /** 376 * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s. 377 * 378 * @param interactionId The interaction id to match the result with the request. 379 * @return The result {@link AccessibilityNodeInfo}s. 380 */ 381 private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear( 382 int interactionId) { 383 synchronized (mInstanceLock) { 384 final boolean success = waitForResultTimedLocked(interactionId); 385 List<AccessibilityNodeInfo> result = success ? mFindAccessibilityNodeInfosResult : null; 386 clearResultLocked(); 387 return result; 388 } 389 } 390 391 /** 392 * {@inheritDoc} 393 */ 394 public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, 395 int interactionId) { 396 synchronized (mInstanceLock) { 397 if (interactionId > mInteractionId) { 398 // If the call is not an IPC, i.e. it is made from the same process, we need to 399 // instantiate new result list to avoid passing internal instances to clients. 400 final boolean isIpcCall = (queryLocalInterface(getInterfaceDescriptor()) == null); 401 if (!isIpcCall) { 402 mFindAccessibilityNodeInfosResult = new ArrayList<AccessibilityNodeInfo>(infos); 403 } else { 404 mFindAccessibilityNodeInfosResult = infos; 405 } 406 mInteractionId = interactionId; 407 } 408 mInstanceLock.notifyAll(); 409 } 410 } 411 412 /** 413 * Gets the result of a request to perform an accessibility action. 414 * 415 * @param interactionId The interaction id to match the result with the request. 416 * @return Whether the action was performed. 417 */ 418 private boolean getPerformAccessibilityActionResultAndClear(int interactionId) { 419 synchronized (mInstanceLock) { 420 final boolean success = waitForResultTimedLocked(interactionId); 421 final boolean result = success ? mPerformAccessibilityActionResult : false; 422 clearResultLocked(); 423 return result; 424 } 425 } 426 427 /** 428 * {@inheritDoc} 429 */ 430 public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) { 431 synchronized (mInstanceLock) { 432 if (interactionId > mInteractionId) { 433 mPerformAccessibilityActionResult = succeeded; 434 mInteractionId = interactionId; 435 } 436 mInstanceLock.notifyAll(); 437 } 438 } 439 440 /** 441 * Clears the result state. 442 */ 443 private void clearResultLocked() { 444 mInteractionId = -1; 445 mFindAccessibilityNodeInfoResult = null; 446 mFindAccessibilityNodeInfosResult = null; 447 mPerformAccessibilityActionResult = false; 448 } 449 450 /** 451 * Waits up to a given bound for a result of a request and returns it. 452 * 453 * @param interactionId The interaction id to match the result with the request. 454 * @return Whether the result was received. 455 */ 456 private boolean waitForResultTimedLocked(int interactionId) { 457 long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS; 458 final long startTimeMillis = SystemClock.uptimeMillis(); 459 while (true) { 460 try { 461 Message sameProcessMessage = getSameProcessMessageAndClear(); 462 if (sameProcessMessage != null) { 463 sameProcessMessage.getTarget().handleMessage(sameProcessMessage); 464 } 465 466 if (mInteractionId == interactionId) { 467 return true; 468 } 469 if (mInteractionId > interactionId) { 470 return false; 471 } 472 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 473 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis; 474 if (waitTimeMillis <= 0) { 475 return false; 476 } 477 mInstanceLock.wait(waitTimeMillis); 478 } catch (InterruptedException ie) { 479 /* ignore */ 480 } 481 } 482 } 483 484 /** 485 * Applies compatibility scale to the info bounds if it is not equal to one. 486 * 487 * @param info The info whose bounds to scale. 488 * @param scale The scale to apply. 489 */ 490 private void applyCompatibilityScaleIfNeeded(AccessibilityNodeInfo info, float scale) { 491 if (scale == 1.0f) { 492 return; 493 } 494 Rect bounds = mTempBounds; 495 info.getBoundsInParent(bounds); 496 bounds.scale(scale); 497 info.setBoundsInParent(bounds); 498 499 info.getBoundsInScreen(bounds); 500 bounds.scale(scale); 501 info.setBoundsInScreen(bounds); 502 } 503 504 /** 505 * Finalize an {@link AccessibilityNodeInfo} before passing it to the client. 506 * 507 * @param info The info. 508 * @param connectionId The id of the connection to the system. 509 * @param windowScale The source window compatibility scale. 510 */ 511 private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId, 512 float windowScale) { 513 if (info != null) { 514 applyCompatibilityScaleIfNeeded(info, windowScale); 515 info.setConnectionId(connectionId); 516 info.setSealed(true); 517 sAccessibilityNodeInfoCache.put(info.getSourceNodeId(), info); 518 } 519 } 520 521 /** 522 * Finalize {@link AccessibilityNodeInfo}s before passing them to the client. 523 * 524 * @param infos The {@link AccessibilityNodeInfo}s. 525 * @param connectionId The id of the connection to the system. 526 * @param windowScale The source window compatibility scale. 527 */ 528 private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, 529 int connectionId, float windowScale) { 530 if (infos != null) { 531 final int infosCount = infos.size(); 532 for (int i = 0; i < infosCount; i++) { 533 AccessibilityNodeInfo info = infos.get(i); 534 finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); 535 } 536 } 537 } 538 539 /** 540 * Gets the message stored if the interacted and interacting 541 * threads are the same. 542 * 543 * @return The message. 544 */ 545 private Message getSameProcessMessageAndClear() { 546 synchronized (mInstanceLock) { 547 Message result = mSameThreadMessage; 548 mSameThreadMessage = null; 549 return result; 550 } 551 } 552 553 /** 554 * Gets a cached accessibility service connection. 555 * 556 * @param connectionId The connection id. 557 * @return The cached connection if such. 558 */ 559 public IAccessibilityServiceConnection getConnection(int connectionId) { 560 synchronized (sConnectionCache) { 561 return sConnectionCache.get(connectionId); 562 } 563 } 564 565 /** 566 * Adds a cached accessibility service connection. 567 * 568 * @param connectionId The connection id. 569 * @param connection The connection. 570 */ 571 public void addConnection(int connectionId, IAccessibilityServiceConnection connection) { 572 synchronized (sConnectionCache) { 573 sConnectionCache.put(connectionId, connection); 574 } 575 } 576 577 /** 578 * Removes a cached accessibility service connection. 579 * 580 * @param connectionId The connection id. 581 */ 582 public void removeConnection(int connectionId) { 583 synchronized (sConnectionCache) { 584 sConnectionCache.remove(connectionId); 585 } 586 } 587} 588