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