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