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