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