1 /* 2 * Copyright (C) 2012 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.accessibilityservice; 18 19 import android.accessibilityservice.AccessibilityService.Callbacks; 20 import android.accessibilityservice.AccessibilityService.IAccessibilityServiceClientWrapper; 21 import android.content.Context; 22 import android.os.Bundle; 23 import android.os.HandlerThread; 24 import android.os.Looper; 25 import android.os.RemoteException; 26 import android.os.ServiceManager; 27 import android.os.SystemClock; 28 import android.util.Log; 29 import android.view.accessibility.AccessibilityEvent; 30 import android.view.accessibility.AccessibilityInteractionClient; 31 import android.view.accessibility.AccessibilityNodeInfo; 32 import android.view.accessibility.IAccessibilityManager; 33 34 import com.android.internal.util.Predicate; 35 36 import java.util.List; 37 import java.util.concurrent.TimeoutException; 38 39 /** 40 * This class represents a bridge that can be used for UI test 41 * automation. It is responsible for connecting to the system, 42 * keeping track of the last accessibility event, and exposing 43 * window content querying APIs. This class is designed to be 44 * used from both an Android application and a Java program 45 * run from the shell. 46 * 47 * @hide 48 */ 49 public class UiTestAutomationBridge { 50 51 private static final String LOG_TAG = UiTestAutomationBridge.class.getSimpleName(); 52 53 private static final int TIMEOUT_REGISTER_SERVICE = 5000; 54 55 public static final int ACTIVE_WINDOW_ID = AccessibilityNodeInfo.ACTIVE_WINDOW_ID; 56 57 public static final long ROOT_NODE_ID = AccessibilityNodeInfo.ROOT_NODE_ID; 58 59 public static final int UNDEFINED = -1; 60 61 private static final int FIND_ACCESSIBILITY_NODE_INFO_PREFETCH_FLAGS = 62 AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS 63 | AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS 64 | AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS; 65 66 private final Object mLock = new Object(); 67 68 private volatile int mConnectionId = AccessibilityInteractionClient.NO_ID; 69 70 private IAccessibilityServiceClientWrapper mListener; 71 72 private AccessibilityEvent mLastEvent; 73 74 private volatile boolean mWaitingForEventDelivery; 75 76 private volatile boolean mUnprocessedEventAvailable; 77 78 private HandlerThread mHandlerThread; 79 80 /** 81 * Gets the last received {@link AccessibilityEvent}. 82 * 83 * @return The event. 84 */ 85 public AccessibilityEvent getLastAccessibilityEvent() { 86 return mLastEvent; 87 } 88 89 /** 90 * Callback for receiving an {@link AccessibilityEvent}. 91 * 92 * <strong>Note:</strong> This method is <strong>NOT</strong> 93 * executed on the application main thread. The client are 94 * responsible for proper synchronization. 95 * 96 * @param event The received event. 97 */ 98 public void onAccessibilityEvent(AccessibilityEvent event) { 99 /* hook - do nothing */ 100 } 101 102 /** 103 * Callback for requests to stop feedback. 104 * 105 * <strong>Note:</strong> This method is <strong>NOT</strong> 106 * executed on the application main thread. The client are 107 * responsible for proper synchronization. 108 */ 109 public void onInterrupt() { 110 /* hook - do nothing */ 111 } 112 113 /** 114 * Connects this service. 115 * 116 * @throws IllegalStateException If already connected. 117 */ 118 public void connect() { 119 if (isConnected()) { 120 throw new IllegalStateException("Already connected."); 121 } 122 123 // Serialize binder calls to a handler on a dedicated thread 124 // different from the main since we expose APIs that block 125 // the main thread waiting for a result the deliver of which 126 // on the main thread will prevent that thread from waking up. 127 // The serialization is needed also to ensure that events are 128 // examined in delivery order. Otherwise, a fair locking 129 // is needed for making sure the binder calls are interleaved 130 // with check for the expected event and also to make sure the 131 // binder threads are allowed to proceed in the received order. 132 mHandlerThread = new HandlerThread("UiTestAutomationBridge"); 133 mHandlerThread.setDaemon(true); 134 mHandlerThread.start(); 135 Looper looper = mHandlerThread.getLooper(); 136 137 mListener = new IAccessibilityServiceClientWrapper(null, looper, new Callbacks() { 138 @Override 139 public void onServiceConnected() { 140 /* do nothing */ 141 } 142 143 @Override 144 public void onInterrupt() { 145 UiTestAutomationBridge.this.onInterrupt(); 146 } 147 148 @Override 149 public void onAccessibilityEvent(AccessibilityEvent event) { 150 synchronized (mLock) { 151 while (true) { 152 mLastEvent = AccessibilityEvent.obtain(event); 153 if (!mWaitingForEventDelivery) { 154 mLock.notifyAll(); 155 break; 156 } 157 if (!mUnprocessedEventAvailable) { 158 mUnprocessedEventAvailable = true; 159 mLock.notifyAll(); 160 break; 161 } 162 try { 163 mLock.wait(); 164 } catch (InterruptedException ie) { 165 /* ignore */ 166 } 167 } 168 } 169 UiTestAutomationBridge.this.onAccessibilityEvent(event); 170 } 171 172 @Override 173 public void onSetConnectionId(int connectionId) { 174 synchronized (mLock) { 175 mConnectionId = connectionId; 176 mLock.notifyAll(); 177 } 178 } 179 180 @Override 181 public boolean onGesture(int gestureId) { 182 return false; 183 } 184 }); 185 186 final IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( 187 ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); 188 189 final AccessibilityServiceInfo info = new AccessibilityServiceInfo(); 190 info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; 191 info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; 192 info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; 193 194 try { 195 manager.registerUiTestAutomationService(mListener, info); 196 } catch (RemoteException re) { 197 throw new IllegalStateException("Cound not register UiAutomationService.", re); 198 } 199 200 synchronized (mLock) { 201 final long startTimeMillis = SystemClock.uptimeMillis(); 202 while (true) { 203 if (isConnected()) { 204 return; 205 } 206 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 207 final long remainingTimeMillis = TIMEOUT_REGISTER_SERVICE - elapsedTimeMillis; 208 if (remainingTimeMillis <= 0) { 209 throw new IllegalStateException("Cound not register UiAutomationService."); 210 } 211 try { 212 mLock.wait(remainingTimeMillis); 213 } catch (InterruptedException ie) { 214 /* ignore */ 215 } 216 } 217 } 218 } 219 220 /** 221 * Disconnects this service. 222 * 223 * @throws IllegalStateException If already disconnected. 224 */ 225 public void disconnect() { 226 if (!isConnected()) { 227 throw new IllegalStateException("Already disconnected."); 228 } 229 230 mHandlerThread.quit(); 231 232 IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( 233 ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); 234 235 try { 236 manager.unregisterUiTestAutomationService(mListener); 237 } catch (RemoteException re) { 238 Log.e(LOG_TAG, "Error while unregistering UiTestAutomationService", re); 239 } 240 } 241 242 /** 243 * Gets whether this service is connected. 244 * 245 * @return True if connected. 246 */ 247 public boolean isConnected() { 248 return (mConnectionId != AccessibilityInteractionClient.NO_ID); 249 } 250 251 /** 252 * Executes a command and waits for a specific accessibility event type up 253 * to a given timeout. 254 * 255 * @param command The command to execute before starting to wait for the event. 256 * @param predicate Predicate for recognizing the awaited event. 257 * @param timeoutMillis The max wait time in milliseconds. 258 */ 259 public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, 260 Predicate<AccessibilityEvent> predicate, long timeoutMillis) 261 throws TimeoutException, Exception { 262 // TODO: This is broken - remove from here when finalizing this as public APIs. 263 synchronized (mLock) { 264 // Prepare to wait for an event. 265 mWaitingForEventDelivery = true; 266 mUnprocessedEventAvailable = false; 267 if (mLastEvent != null) { 268 mLastEvent.recycle(); 269 mLastEvent = null; 270 } 271 // Execute the command. 272 command.run(); 273 // Wait for the event. 274 final long startTimeMillis = SystemClock.uptimeMillis(); 275 while (true) { 276 // If the expected event is received, that's it. 277 if ((mUnprocessedEventAvailable && predicate.apply(mLastEvent))) { 278 mWaitingForEventDelivery = false; 279 mUnprocessedEventAvailable = false; 280 mLock.notifyAll(); 281 return mLastEvent; 282 } 283 // Ask for another event. 284 mWaitingForEventDelivery = true; 285 mUnprocessedEventAvailable = false; 286 mLock.notifyAll(); 287 // Check if timed out and if not wait. 288 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 289 final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis; 290 if (remainingTimeMillis <= 0) { 291 mWaitingForEventDelivery = false; 292 mUnprocessedEventAvailable = false; 293 mLock.notifyAll(); 294 throw new TimeoutException("Expacted event not received within: " 295 + timeoutMillis + " ms."); 296 } 297 try { 298 mLock.wait(remainingTimeMillis); 299 } catch (InterruptedException ie) { 300 /* ignore */ 301 } 302 } 303 } 304 } 305 306 /** 307 * Waits for the accessibility event stream to become idle, which is not to 308 * have received a new accessibility event within <code>idleTimeout</code>, 309 * and do so within a maximal global timeout as specified by 310 * <code>globalTimeout</code>. 311 * 312 * @param idleTimeout The timeout between two event to consider the device idle. 313 * @param globalTimeout The maximal global timeout in which to wait for idle. 314 */ 315 public void waitForIdle(long idleTimeout, long globalTimeout) { 316 final long startTimeMillis = SystemClock.uptimeMillis(); 317 long lastEventTime = (mLastEvent != null) 318 ? mLastEvent.getEventTime() : SystemClock.uptimeMillis(); 319 synchronized (mLock) { 320 while (true) { 321 final long currentTimeMillis = SystemClock.uptimeMillis(); 322 final long sinceLastEventTimeMillis = currentTimeMillis - lastEventTime; 323 if (sinceLastEventTimeMillis > idleTimeout) { 324 return; 325 } 326 if (mLastEvent != null) { 327 lastEventTime = mLastEvent.getEventTime(); 328 } 329 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 330 final long remainingTimeMillis = globalTimeout - elapsedTimeMillis; 331 if (remainingTimeMillis <= 0) { 332 return; 333 } 334 try { 335 mLock.wait(idleTimeout); 336 } catch (InterruptedException e) { 337 /* ignore */ 338 } 339 } 340 } 341 } 342 343 /** 344 * Finds an {@link AccessibilityNodeInfo} by accessibility id in the active 345 * window. The search is performed from the root node. 346 * 347 * @param accessibilityNodeId A unique view id or virtual descendant id for 348 * which to search. 349 * @return The current window scale, where zero means a failure. 350 */ 351 public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityIdInActiveWindow( 352 long accessibilityNodeId) { 353 return findAccessibilityNodeInfoByAccessibilityId(ACTIVE_WINDOW_ID, accessibilityNodeId); 354 } 355 356 /** 357 * Finds an {@link AccessibilityNodeInfo} by accessibility id. 358 * 359 * @param accessibilityWindowId A unique window id. Use {@link #ACTIVE_WINDOW_ID} to query 360 * the currently active window. 361 * @param accessibilityNodeId A unique view id or virtual descendant id for 362 * which to search. 363 * @return The current window scale, where zero means a failure. 364 */ 365 public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId( 366 int accessibilityWindowId, long accessibilityNodeId) { 367 // Cache the id to avoid locking 368 final int connectionId = mConnectionId; 369 ensureValidConnection(connectionId); 370 return AccessibilityInteractionClient.getInstance() 371 .findAccessibilityNodeInfoByAccessibilityId(mConnectionId, 372 accessibilityWindowId, accessibilityNodeId, 373 FIND_ACCESSIBILITY_NODE_INFO_PREFETCH_FLAGS); 374 } 375 376 /** 377 * Finds an {@link AccessibilityNodeInfo} by View id in the active 378 * window. The search is performed from the root node. 379 * 380 * @param viewId The id of a View. 381 * @return The current window scale, where zero means a failure. 382 */ 383 public AccessibilityNodeInfo findAccessibilityNodeInfoByViewIdInActiveWindow(int viewId) { 384 return findAccessibilityNodeInfoByViewId(ACTIVE_WINDOW_ID, ROOT_NODE_ID, viewId); 385 } 386 387 /** 388 * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in 389 * the window whose id is specified and starts from the node whose accessibility 390 * id is specified. 391 * 392 * @param accessibilityWindowId A unique window id. Use 393 * {@link #ACTIVE_WINDOW_ID} to query the currently active window. 394 * @param accessibilityNodeId A unique view id or virtual descendant id from 395 * where to start the search. Use {@link #ROOT_NODE_ID} to start from the root. 396 * @param viewId The id of a View. 397 * @return The current window scale, where zero means a failure. 398 */ 399 public AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int accessibilityWindowId, 400 long accessibilityNodeId, int viewId) { 401 // Cache the id to avoid locking 402 final int connectionId = mConnectionId; 403 ensureValidConnection(connectionId); 404 return AccessibilityInteractionClient.getInstance() 405 .findAccessibilityNodeInfoByViewId(connectionId, accessibilityWindowId, 406 accessibilityNodeId, viewId); 407 } 408 409 /** 410 * Finds {@link AccessibilityNodeInfo}s by View text in the active 411 * window. The search is performed from the root node. 412 * 413 * @param text The searched text. 414 * @return The current window scale, where zero means a failure. 415 */ 416 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByTextInActiveWindow(String text) { 417 return findAccessibilityNodeInfosByText(ACTIVE_WINDOW_ID, ROOT_NODE_ID, text); 418 } 419 420 /** 421 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case 422 * insensitive containment. The search is performed in the window whose 423 * id is specified and starts from the node whose accessibility id is 424 * specified. 425 * 426 * @param accessibilityWindowId A unique window id. Use 427 * {@link #ACTIVE_WINDOW_ID} to query the currently active window. 428 * @param accessibilityNodeId A unique view id or virtual descendant id from 429 * where to start the search. Use {@link #ROOT_NODE_ID} to start from the root. 430 * @param text The searched text. 431 * @return The current window scale, where zero means a failure. 432 */ 433 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int accessibilityWindowId, 434 long accessibilityNodeId, String text) { 435 // Cache the id to avoid locking 436 final int connectionId = mConnectionId; 437 ensureValidConnection(connectionId); 438 return AccessibilityInteractionClient.getInstance() 439 .findAccessibilityNodeInfosByText(connectionId, accessibilityWindowId, 440 accessibilityNodeId, text); 441 } 442 443 /** 444 * Performs an accessibility action on an {@link AccessibilityNodeInfo} 445 * in the active window. 446 * 447 * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id). 448 * @param action The action to perform. 449 * @param arguments Optional action arguments. 450 * @return Whether the action was performed. 451 */ 452 public boolean performAccessibilityActionInActiveWindow(long accessibilityNodeId, int action, 453 Bundle arguments) { 454 return performAccessibilityAction(ACTIVE_WINDOW_ID, accessibilityNodeId, action, arguments); 455 } 456 457 /** 458 * Performs an accessibility action on an {@link AccessibilityNodeInfo}. 459 * 460 * @param accessibilityWindowId A unique window id. Use 461 * {@link #ACTIVE_WINDOW_ID} to query the currently active window. 462 * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id). 463 * @param action The action to perform. 464 * @param arguments Optional action arguments. 465 * @return Whether the action was performed. 466 */ 467 public boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, 468 int action, Bundle arguments) { 469 // Cache the id to avoid locking 470 final int connectionId = mConnectionId; 471 ensureValidConnection(connectionId); 472 return AccessibilityInteractionClient.getInstance().performAccessibilityAction(connectionId, 473 accessibilityWindowId, accessibilityNodeId, action, arguments); 474 } 475 476 /** 477 * Gets the root {@link AccessibilityNodeInfo} in the active window. 478 * 479 * @return The root info. 480 */ 481 public AccessibilityNodeInfo getRootAccessibilityNodeInfoInActiveWindow() { 482 // Cache the id to avoid locking 483 final int connectionId = mConnectionId; 484 ensureValidConnection(connectionId); 485 return AccessibilityInteractionClient.getInstance() 486 .findAccessibilityNodeInfoByAccessibilityId(connectionId, ACTIVE_WINDOW_ID, 487 ROOT_NODE_ID, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); 488 } 489 490 private void ensureValidConnection(int connectionId) { 491 if (connectionId == UNDEFINED) { 492 throw new IllegalStateException("UiAutomationService not connected." 493 + " Did you call #register()?"); 494 } 495 } 496 } 497