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 17 package android.view.accessibility; 18 19 import android.accessibilityservice.IAccessibilityServiceConnection; 20 import android.graphics.Rect; 21 import android.os.Message; 22 import android.os.RemoteException; 23 import android.os.SystemClock; 24 import android.util.Log; 25 import android.util.SparseArray; 26 27 import java.util.Collections; 28 import java.util.List; 29 import 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 */ 63 public 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 // The connection cache is shared between all interrogating threads. 95 private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache = 96 new SparseArray<IAccessibilityServiceConnection>(); 97 98 /** 99 * @return The singleton of this class. 100 */ 101 public static AccessibilityInteractionClient getInstance() { 102 synchronized (sStaticLock) { 103 if (sInstance == null) { 104 sInstance = new AccessibilityInteractionClient(); 105 } 106 return sInstance; 107 } 108 } 109 110 /** 111 * Sets the message to be processed if the interacted view hierarchy 112 * and the interacting client are running in the same thread. 113 * 114 * @param message The message. 115 */ 116 public void setSameThreadMessage(Message message) { 117 synchronized (mInstanceLock) { 118 mSameThreadMessage = message; 119 mInstanceLock.notifyAll(); 120 } 121 } 122 123 /** 124 * Finds an {@link AccessibilityNodeInfo} by accessibility id. 125 * 126 * @param connectionId The id of a connection for interacting with the system. 127 * @param accessibilityWindowId A unique window id. 128 * @param accessibilityViewId A unique View accessibility id. 129 * @return An {@link AccessibilityNodeInfo} if found, null otherwise. 130 */ 131 public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, 132 int accessibilityWindowId, int accessibilityViewId) { 133 try { 134 IAccessibilityServiceConnection connection = getConnection(connectionId); 135 if (connection != null) { 136 final int interactionId = mInteractionIdCounter.getAndIncrement(); 137 final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId( 138 accessibilityWindowId, accessibilityViewId, interactionId, this, 139 Thread.currentThread().getId()); 140 // If the scale is zero the call has failed. 141 if (windowScale > 0) { 142 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 143 interactionId); 144 finalizeAccessibilityNodeInfo(info, connectionId, windowScale); 145 return info; 146 } 147 } else { 148 if (DEBUG) { 149 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 150 } 151 } 152 } catch (RemoteException re) { 153 if (DEBUG) { 154 Log.w(LOG_TAG, "Error while calling remote" 155 + " findAccessibilityNodeInfoByAccessibilityId", re); 156 } 157 } 158 return null; 159 } 160 161 /** 162 * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed 163 * in the currently active window and starts from the root View in the window. 164 * 165 * @param connectionId The id of a connection for interacting with the system. 166 * @param viewId The id of the view. 167 * @return An {@link AccessibilityNodeInfo} if found, null otherwise. 168 */ 169 public AccessibilityNodeInfo findAccessibilityNodeInfoByViewIdInActiveWindow(int connectionId, 170 int viewId) { 171 try { 172 IAccessibilityServiceConnection connection = getConnection(connectionId); 173 if (connection != null) { 174 final int interactionId = mInteractionIdCounter.getAndIncrement(); 175 final float windowScale = 176 connection.findAccessibilityNodeInfoByViewIdInActiveWindow(viewId, 177 interactionId, this, Thread.currentThread().getId()); 178 // If the scale is zero the call has failed. 179 if (windowScale > 0) { 180 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 181 interactionId); 182 finalizeAccessibilityNodeInfo(info, connectionId, windowScale); 183 return info; 184 } 185 } else { 186 if (DEBUG) { 187 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 188 } 189 } 190 } catch (RemoteException re) { 191 if (DEBUG) { 192 Log.w(LOG_TAG, "Error while calling remote" 193 + " findAccessibilityNodeInfoByViewIdInActiveWindow", re); 194 } 195 } 196 return null; 197 } 198 199 /** 200 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case 201 * insensitive containment. The search is performed in the currently 202 * active window and starts from the root View in the window. 203 * 204 * @param connectionId The id of a connection for interacting with the system. 205 * @param text The searched text. 206 * @return A list of found {@link AccessibilityNodeInfo}s. 207 */ 208 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewTextInActiveWindow( 209 int connectionId, String text) { 210 try { 211 IAccessibilityServiceConnection connection = getConnection(connectionId); 212 if (connection != null) { 213 final int interactionId = mInteractionIdCounter.getAndIncrement(); 214 final float windowScale = 215 connection.findAccessibilityNodeInfosByViewTextInActiveWindow(text, 216 interactionId, this, Thread.currentThread().getId()); 217 // If the scale is zero the call has failed. 218 if (windowScale > 0) { 219 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 220 interactionId); 221 finalizeAccessibilityNodeInfos(infos, connectionId, windowScale); 222 return infos; 223 } 224 } else { 225 if (DEBUG) { 226 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 227 } 228 } 229 } catch (RemoteException re) { 230 if (DEBUG) { 231 Log.w(LOG_TAG, "Error while calling remote" 232 + " findAccessibilityNodeInfosByViewTextInActiveWindow", re); 233 } 234 } 235 return null; 236 } 237 238 /** 239 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case 240 * insensitive containment. The search is performed in the window whose 241 * id is specified and starts from the View whose accessibility id is 242 * specified. 243 * 244 * @param connectionId The id of a connection for interacting with the system. 245 * @param text The searched text. 246 * @param accessibilityWindowId A unique window id. 247 * @param accessibilityViewId A unique View accessibility id from where to start the search. 248 * Use {@link android.view.View#NO_ID} to start from the root. 249 * @return A list of found {@link AccessibilityNodeInfo}s. 250 */ 251 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewText(int connectionId, 252 String text, int accessibilityWindowId, int accessibilityViewId) { 253 try { 254 IAccessibilityServiceConnection connection = getConnection(connectionId); 255 if (connection != null) { 256 final int interactionId = mInteractionIdCounter.getAndIncrement(); 257 final float windowScale = connection.findAccessibilityNodeInfosByViewText(text, 258 accessibilityWindowId, accessibilityViewId, interactionId, this, 259 Thread.currentThread().getId()); 260 // If the scale is zero the call has failed. 261 if (windowScale > 0) { 262 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 263 interactionId); 264 finalizeAccessibilityNodeInfos(infos, connectionId, windowScale); 265 return infos; 266 } 267 } else { 268 if (DEBUG) { 269 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 270 } 271 } 272 } catch (RemoteException re) { 273 if (DEBUG) { 274 Log.w(LOG_TAG, "Error while calling remote" 275 + " findAccessibilityNodeInfosByViewText", re); 276 } 277 } 278 return Collections.emptyList(); 279 } 280 281 /** 282 * Performs an accessibility action on an {@link AccessibilityNodeInfo}. 283 * 284 * @param connectionId The id of a connection for interacting with the system. 285 * @param accessibilityWindowId The id of the window. 286 * @param accessibilityViewId A unique View accessibility id. 287 * @param action The action to perform. 288 * @return Whether the action was performed. 289 */ 290 public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId, 291 int accessibilityViewId, int action) { 292 try { 293 IAccessibilityServiceConnection connection = getConnection(connectionId); 294 if (connection != null) { 295 final int interactionId = mInteractionIdCounter.getAndIncrement(); 296 final boolean success = connection.performAccessibilityAction( 297 accessibilityWindowId, accessibilityViewId, action, interactionId, this, 298 Thread.currentThread().getId()); 299 if (success) { 300 return getPerformAccessibilityActionResult(interactionId); 301 } 302 } else { 303 if (DEBUG) { 304 Log.w(LOG_TAG, "No connection for connection id: " + connectionId); 305 } 306 } 307 } catch (RemoteException re) { 308 if (DEBUG) { 309 Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re); 310 } 311 } 312 return false; 313 } 314 315 /** 316 * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}. 317 * 318 * @param interactionId The interaction id to match the result with the request. 319 * @return The result {@link AccessibilityNodeInfo}. 320 */ 321 private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) { 322 synchronized (mInstanceLock) { 323 final boolean success = waitForResultTimedLocked(interactionId); 324 AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null; 325 clearResultLocked(); 326 return result; 327 } 328 } 329 330 /** 331 * {@inheritDoc} 332 */ 333 public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, 334 int interactionId) { 335 synchronized (mInstanceLock) { 336 if (interactionId > mInteractionId) { 337 mFindAccessibilityNodeInfoResult = info; 338 mInteractionId = interactionId; 339 } 340 mInstanceLock.notifyAll(); 341 } 342 } 343 344 /** 345 * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s. 346 * 347 * @param interactionId The interaction id to match the result with the request. 348 * @return The result {@link AccessibilityNodeInfo}s. 349 */ 350 private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear( 351 int interactionId) { 352 synchronized (mInstanceLock) { 353 final boolean success = waitForResultTimedLocked(interactionId); 354 List<AccessibilityNodeInfo> result = success ? mFindAccessibilityNodeInfosResult : null; 355 clearResultLocked(); 356 return result; 357 } 358 } 359 360 /** 361 * {@inheritDoc} 362 */ 363 public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, 364 int interactionId) { 365 synchronized (mInstanceLock) { 366 if (interactionId > mInteractionId) { 367 mFindAccessibilityNodeInfosResult = infos; 368 mInteractionId = interactionId; 369 } 370 mInstanceLock.notifyAll(); 371 } 372 } 373 374 /** 375 * Gets the result of a request to perform an accessibility action. 376 * 377 * @param interactionId The interaction id to match the result with the request. 378 * @return Whether the action was performed. 379 */ 380 private boolean getPerformAccessibilityActionResult(int interactionId) { 381 synchronized (mInstanceLock) { 382 final boolean success = waitForResultTimedLocked(interactionId); 383 final boolean result = success ? mPerformAccessibilityActionResult : false; 384 clearResultLocked(); 385 return result; 386 } 387 } 388 389 /** 390 * {@inheritDoc} 391 */ 392 public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) { 393 synchronized (mInstanceLock) { 394 if (interactionId > mInteractionId) { 395 mPerformAccessibilityActionResult = succeeded; 396 mInteractionId = interactionId; 397 } 398 mInstanceLock.notifyAll(); 399 } 400 } 401 402 /** 403 * Clears the result state. 404 */ 405 private void clearResultLocked() { 406 mInteractionId = -1; 407 mFindAccessibilityNodeInfoResult = null; 408 mFindAccessibilityNodeInfosResult = null; 409 mPerformAccessibilityActionResult = false; 410 } 411 412 /** 413 * Waits up to a given bound for a result of a request and returns it. 414 * 415 * @param interactionId The interaction id to match the result with the request. 416 * @return Whether the result was received. 417 */ 418 private boolean waitForResultTimedLocked(int interactionId) { 419 long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS; 420 final long startTimeMillis = SystemClock.uptimeMillis(); 421 while (true) { 422 try { 423 Message sameProcessMessage = getSameProcessMessageAndClear(); 424 if (sameProcessMessage != null) { 425 sameProcessMessage.getTarget().handleMessage(sameProcessMessage); 426 } 427 428 if (mInteractionId == interactionId) { 429 return true; 430 } 431 if (mInteractionId > interactionId) { 432 return false; 433 } 434 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 435 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis; 436 if (waitTimeMillis <= 0) { 437 return false; 438 } 439 mInstanceLock.wait(waitTimeMillis); 440 } catch (InterruptedException ie) { 441 /* ignore */ 442 } 443 } 444 } 445 446 /** 447 * Applies compatibility scale to the info bounds if it is not equal to one. 448 * 449 * @param info The info whose bounds to scale. 450 * @param scale The scale to apply. 451 */ 452 private void applyCompatibilityScaleIfNeeded(AccessibilityNodeInfo info, float scale) { 453 if (scale == 1.0f) { 454 return; 455 } 456 Rect bounds = mTempBounds; 457 info.getBoundsInParent(bounds); 458 bounds.scale(scale); 459 info.setBoundsInParent(bounds); 460 461 info.getBoundsInScreen(bounds); 462 bounds.scale(scale); 463 info.setBoundsInScreen(bounds); 464 } 465 466 /** 467 * Finalize an {@link AccessibilityNodeInfo} before passing it to the client. 468 * 469 * @param info The info. 470 * @param connectionId The id of the connection to the system. 471 * @param windowScale The source window compatibility scale. 472 */ 473 private void finalizeAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId, 474 float windowScale) { 475 if (info != null) { 476 applyCompatibilityScaleIfNeeded(info, windowScale); 477 info.setConnectionId(connectionId); 478 info.setSealed(true); 479 } 480 } 481 482 /** 483 * Finalize {@link AccessibilityNodeInfo}s before passing them to the client. 484 * 485 * @param infos The {@link AccessibilityNodeInfo}s. 486 * @param connectionId The id of the connection to the system. 487 * @param windowScale The source window compatibility scale. 488 */ 489 private void finalizeAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, 490 int connectionId, float windowScale) { 491 if (infos != null) { 492 final int infosCount = infos.size(); 493 for (int i = 0; i < infosCount; i++) { 494 AccessibilityNodeInfo info = infos.get(i); 495 finalizeAccessibilityNodeInfo(info, connectionId, windowScale); 496 } 497 } 498 } 499 500 /** 501 * Gets the message stored if the interacted and interacting 502 * threads are the same. 503 * 504 * @return The message. 505 */ 506 private Message getSameProcessMessageAndClear() { 507 synchronized (mInstanceLock) { 508 Message result = mSameThreadMessage; 509 mSameThreadMessage = null; 510 return result; 511 } 512 } 513 514 /** 515 * Gets a cached accessibility service connection. 516 * 517 * @param connectionId The connection id. 518 * @return The cached connection if such. 519 */ 520 public IAccessibilityServiceConnection getConnection(int connectionId) { 521 synchronized (sConnectionCache) { 522 return sConnectionCache.get(connectionId); 523 } 524 } 525 526 /** 527 * Adds a cached accessibility service connection. 528 * 529 * @param connectionId The connection id. 530 * @param connection The connection. 531 */ 532 public void addConnection(int connectionId, IAccessibilityServiceConnection connection) { 533 synchronized (sConnectionCache) { 534 sConnectionCache.put(connectionId, connection); 535 } 536 } 537 538 /** 539 * Removes a cached accessibility service connection. 540 * 541 * @param connectionId The connection id. 542 */ 543 public void removeConnection(int connectionId) { 544 synchronized (sConnectionCache) { 545 sConnectionCache.remove(connectionId); 546 } 547 } 548 } 549