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