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