AccessibilityInteractionClient.java revision 6bc5e530016928027c7b390a8368ecdd5bff072f
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 mInstanceLock.notifyAll(); 108 } 109 } 110 111 /** 112 * Finds an {@link AccessibilityNodeInfo} by accessibility id. 113 * 114 * @param connection A connection for interacting with the system. 115 * @param accessibilityWindowId A unique window id. 116 * @param accessibilityViewId A unique View accessibility id. 117 * @return An {@link AccessibilityNodeInfo} if found, null otherwise. 118 */ 119 public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId( 120 IAccessibilityServiceConnection connection, int accessibilityWindowId, 121 int accessibilityViewId) { 122 try { 123 final int interactionId = mInteractionIdCounter.getAndIncrement(); 124 final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId( 125 accessibilityWindowId, accessibilityViewId, interactionId, this, 126 Thread.currentThread().getId()); 127 // If the scale is zero the call has failed. 128 if (windowScale > 0) { 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 viewId The id of the view. 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 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( 157 interactionId); 158 finalizeAccessibilityNodeInfo(info, connection, windowScale); 159 return info; 160 } 161 } catch (RemoteException re) { 162 /* ignore */ 163 } 164 return null; 165 } 166 167 /** 168 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case 169 * insensitive containment. The search is performed in the currently 170 * active window and starts from the root View in the window. 171 * 172 * @param connection A connection for interacting with the system. 173 * @param text The searched text. 174 * @return A list of found {@link AccessibilityNodeInfo}s. 175 */ 176 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewTextInActiveWindow( 177 IAccessibilityServiceConnection connection, String text) { 178 try { 179 final int interactionId = mInteractionIdCounter.getAndIncrement(); 180 final float windowScale = connection.findAccessibilityNodeInfosByViewTextInActiveWindow( 181 text, interactionId, this, Thread.currentThread().getId()); 182 // If the scale is zero the call has failed. 183 if (windowScale > 0) { 184 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( 185 interactionId); 186 finalizeAccessibilityNodeInfos(infos, connection, windowScale); 187 return infos; 188 } 189 } catch (RemoteException re) { 190 /* ignore */ 191 } 192 return null; 193 } 194 195 /** 196 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case 197 * insensitive containment. The search is performed in the window whose 198 * id is specified and starts from the View whose accessibility id is 199 * specified. 200 * 201 * @param connection A connection for interacting with the system. 202 * @param text The searched text. 203 * @param accessibilityWindowId A unique window id. 204 * @param accessibilityViewId A unique View accessibility id from where to start the search. 205 * Use {@link android.view.View#NO_ID} to start from the root. 206 * @return A list of found {@link AccessibilityNodeInfo}s. 207 */ 208 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewText( 209 IAccessibilityServiceConnection connection, String text, int accessibilityWindowId, 210 int accessibilityViewId) { 211 try { 212 final int interactionId = mInteractionIdCounter.getAndIncrement(); 213 final float windowScale = connection.findAccessibilityNodeInfosByViewText(text, 214 accessibilityWindowId, accessibilityViewId, interactionId, this, 215 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, connection, windowScale); 221 return infos; 222 } 223 } catch (RemoteException re) { 224 /* ignore */ 225 } 226 return Collections.emptyList(); 227 } 228 229 /** 230 * Performs an accessibility action on an {@link AccessibilityNodeInfo}. 231 * 232 * @param connection A connection for interacting with the system. 233 * @param accessibilityWindowId The id of the window. 234 * @param accessibilityViewId A unique View accessibility id. 235 * @param action The action to perform. 236 * @return Whether the action was performed. 237 */ 238 public boolean performAccessibilityAction(IAccessibilityServiceConnection connection, 239 int accessibilityWindowId, int accessibilityViewId, int action) { 240 try { 241 final int interactionId = mInteractionIdCounter.getAndIncrement(); 242 final boolean success = connection.performAccessibilityAction( 243 accessibilityWindowId, accessibilityViewId, action, interactionId, this, 244 Thread.currentThread().getId()); 245 if (success) { 246 return getPerformAccessibilityActionResult(interactionId); 247 } 248 } catch (RemoteException re) { 249 /* ignore */ 250 } 251 return false; 252 } 253 254 /** 255 * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}. 256 * 257 * @param interactionId The interaction id to match the result with the request. 258 * @return The result {@link AccessibilityNodeInfo}. 259 */ 260 private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) { 261 synchronized (mInstanceLock) { 262 final boolean success = waitForResultTimedLocked(interactionId); 263 AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null; 264 clearResultLocked(); 265 return result; 266 } 267 } 268 269 /** 270 * {@inheritDoc} 271 */ 272 public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, 273 int interactionId) { 274 synchronized (mInstanceLock) { 275 if (interactionId > mInteractionId) { 276 mFindAccessibilityNodeInfoResult = info; 277 mInteractionId = interactionId; 278 } 279 mInstanceLock.notifyAll(); 280 } 281 } 282 283 /** 284 * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s. 285 * 286 * @param interactionId The interaction id to match the result with the request. 287 * @return The result {@link AccessibilityNodeInfo}s. 288 */ 289 private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear( 290 int interactionId) { 291 synchronized (mInstanceLock) { 292 final boolean success = waitForResultTimedLocked(interactionId); 293 List<AccessibilityNodeInfo> result = success ? mFindAccessibilityNodeInfosResult : null; 294 clearResultLocked(); 295 return result; 296 } 297 } 298 299 /** 300 * {@inheritDoc} 301 */ 302 public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, 303 int interactionId) { 304 synchronized (mInstanceLock) { 305 if (interactionId > mInteractionId) { 306 mFindAccessibilityNodeInfosResult = infos; 307 mInteractionId = interactionId; 308 } 309 mInstanceLock.notifyAll(); 310 } 311 } 312 313 /** 314 * Gets the result of a request to perform an accessibility action. 315 * 316 * @param interactionId The interaction id to match the result with the request. 317 * @return Whether the action was performed. 318 */ 319 private boolean getPerformAccessibilityActionResult(int interactionId) { 320 synchronized (mInstanceLock) { 321 final boolean success = waitForResultTimedLocked(interactionId); 322 final boolean result = success ? mPerformAccessibilityActionResult : false; 323 clearResultLocked(); 324 return result; 325 } 326 } 327 328 /** 329 * {@inheritDoc} 330 */ 331 public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) { 332 synchronized (mInstanceLock) { 333 if (interactionId > mInteractionId) { 334 mPerformAccessibilityActionResult = succeeded; 335 mInteractionId = interactionId; 336 } 337 mInstanceLock.notifyAll(); 338 } 339 } 340 341 /** 342 * Clears the result state. 343 */ 344 private void clearResultLocked() { 345 mInteractionId = -1; 346 mFindAccessibilityNodeInfoResult = null; 347 mFindAccessibilityNodeInfosResult = null; 348 mPerformAccessibilityActionResult = false; 349 } 350 351 /** 352 * Waits up to a given bound for a result of a request and returns it. 353 * 354 * @param interactionId The interaction id to match the result with the request. 355 * @return Whether the result was received. 356 */ 357 private boolean waitForResultTimedLocked(int interactionId) { 358 long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS; 359 final long startTimeMillis = SystemClock.uptimeMillis(); 360 while (true) { 361 try { 362 Message sameProcessMessage = getSameProcessMessageAndClear(); 363 if (sameProcessMessage != null) { 364 sameProcessMessage.getTarget().handleMessage(sameProcessMessage); 365 } 366 367 if (mInteractionId == interactionId) { 368 return true; 369 } 370 if (mInteractionId > interactionId) { 371 return false; 372 } 373 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 374 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis; 375 if (waitTimeMillis <= 0) { 376 return false; 377 } 378 mInstanceLock.wait(waitTimeMillis); 379 } catch (InterruptedException ie) { 380 /* ignore */ 381 } 382 } 383 } 384 385 /** 386 * Applies compatibility scale to the info bounds if it is not equal to one. 387 * 388 * @param info The info whose bounds to scale. 389 * @param scale The scale to apply. 390 */ 391 private void applyCompatibilityScaleIfNeeded(AccessibilityNodeInfo info, float scale) { 392 if (scale == 1.0f) { 393 return; 394 } 395 Rect bounds = mTempBounds; 396 info.getBoundsInParent(bounds); 397 bounds.scale(scale); 398 info.setBoundsInParent(bounds); 399 400 info.getBoundsInScreen(bounds); 401 bounds.scale(scale); 402 info.setBoundsInScreen(bounds); 403 } 404 405 /** 406 * Finalize an {@link AccessibilityNodeInfo} before passing it to the client. 407 * 408 * @param info The info. 409 * @param connection The current connection to the system. 410 * @param windowScale The source window compatibility scale. 411 */ 412 private void finalizeAccessibilityNodeInfo(AccessibilityNodeInfo info, 413 IAccessibilityServiceConnection connection, float windowScale) { 414 if (info != null) { 415 applyCompatibilityScaleIfNeeded(info, windowScale); 416 info.setConnection(connection); 417 info.setSealed(true); 418 } 419 } 420 421 /** 422 * Finalize {@link AccessibilityNodeInfo}s before passing them to the client. 423 * 424 * @param infos The {@link AccessibilityNodeInfo}s. 425 * @param connection The current connection to the system. 426 * @param windowScale The source window compatibility scale. 427 */ 428 private void finalizeAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, 429 IAccessibilityServiceConnection connection, float windowScale) { 430 if (infos != null) { 431 final int infosCount = infos.size(); 432 for (int i = 0; i < infosCount; i++) { 433 AccessibilityNodeInfo info = infos.get(i); 434 finalizeAccessibilityNodeInfo(info, connection, windowScale); 435 } 436 } 437 } 438 439 /** 440 * Gets the message stored if the interacted and interacting 441 * threads are the same. 442 * 443 * @return The message. 444 */ 445 private Message getSameProcessMessageAndClear() { 446 synchronized (mInstanceLock) { 447 Message result = mSameThreadMessage; 448 mSameThreadMessage = null; 449 return result; 450 } 451 } 452} 453