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