1/*
2 * Copyright (C) 2013 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.app;
18
19import android.accessibilityservice.AccessibilityService.Callbacks;
20import android.accessibilityservice.AccessibilityService.IAccessibilityServiceClientWrapper;
21import android.accessibilityservice.AccessibilityServiceInfo;
22import android.accessibilityservice.IAccessibilityServiceClient;
23import android.accessibilityservice.IAccessibilityServiceConnection;
24import android.graphics.Bitmap;
25import android.graphics.Canvas;
26import android.graphics.Point;
27import android.hardware.display.DisplayManagerGlobal;
28import android.os.IBinder;
29import android.os.Looper;
30import android.os.ParcelFileDescriptor;
31import android.os.RemoteException;
32import android.os.SystemClock;
33import android.util.Log;
34import android.view.Display;
35import android.view.InputEvent;
36import android.view.KeyEvent;
37import android.view.Surface;
38import android.view.WindowAnimationFrameStats;
39import android.view.WindowContentFrameStats;
40import android.view.accessibility.AccessibilityEvent;
41import android.view.accessibility.AccessibilityInteractionClient;
42import android.view.accessibility.AccessibilityNodeInfo;
43import android.view.accessibility.AccessibilityWindowInfo;
44import android.view.accessibility.IAccessibilityInteractionConnection;
45import libcore.io.IoUtils;
46
47import java.io.IOException;
48import java.util.ArrayList;
49import java.util.List;
50import java.util.concurrent.TimeoutException;
51
52/**
53 * Class for interacting with the device's UI by simulation user actions and
54 * introspection of the screen content. It relies on the platform accessibility
55 * APIs to introspect the screen and to perform some actions on the remote view
56 * tree. It also allows injecting of arbitrary raw input events simulating user
57 * interaction with keyboards and touch devices. One can think of a UiAutomation
58 * as a special type of {@link android.accessibilityservice.AccessibilityService}
59 * which does not provide hooks for the service life cycle and exposes other
60 * APIs that are useful for UI test automation.
61 * <p>
62 * The APIs exposed by this class are low-level to maximize flexibility when
63 * developing UI test automation tools and libraries. Generally, a UiAutomation
64 * client should be using a higher-level library or implement high-level functions.
65 * For example, performing a tap on the screen requires construction and injecting
66 * of a touch down and up events which have to be delivered to the system by a
67 * call to {@link #injectInputEvent(InputEvent, boolean)}.
68 * </p>
69 * <p>
70 * The APIs exposed by this class operate across applications enabling a client
71 * to write tests that cover use cases spanning over multiple applications. For
72 * example, going to the settings application to change a setting and then
73 * interacting with another application whose behavior depends on that setting.
74 * </p>
75 */
76public final class UiAutomation {
77
78    private static final String LOG_TAG = UiAutomation.class.getSimpleName();
79
80    private static final boolean DEBUG = false;
81
82    private static final int CONNECTION_ID_UNDEFINED = -1;
83
84    private static final long CONNECT_TIMEOUT_MILLIS = 5000;
85
86    /** Rotation constant: Unfreeze rotation (rotating the device changes its rotation state). */
87    public static final int ROTATION_UNFREEZE = -2;
88
89    /** Rotation constant: Freeze rotation to its current state. */
90    public static final int ROTATION_FREEZE_CURRENT = -1;
91
92    /** Rotation constant: Freeze rotation to 0 degrees (natural orientation) */
93    public static final int ROTATION_FREEZE_0 = Surface.ROTATION_0;
94
95    /** Rotation constant: Freeze rotation to 90 degrees . */
96    public static final int ROTATION_FREEZE_90 = Surface.ROTATION_90;
97
98    /** Rotation constant: Freeze rotation to 180 degrees . */
99    public static final int ROTATION_FREEZE_180 = Surface.ROTATION_180;
100
101    /** Rotation constant: Freeze rotation to 270 degrees . */
102    public static final int ROTATION_FREEZE_270 = Surface.ROTATION_270;
103
104    private final Object mLock = new Object();
105
106    private final ArrayList<AccessibilityEvent> mEventQueue = new ArrayList<AccessibilityEvent>();
107
108    private final IAccessibilityServiceClient mClient;
109
110    private final IUiAutomationConnection mUiAutomationConnection;
111
112    private int mConnectionId = CONNECTION_ID_UNDEFINED;
113
114    private OnAccessibilityEventListener mOnAccessibilityEventListener;
115
116    private boolean mWaitingForEventDelivery;
117
118    private long mLastEventTimeMillis;
119
120    private boolean mIsConnecting;
121
122    /**
123     * Listener for observing the {@link AccessibilityEvent} stream.
124     */
125    public static interface OnAccessibilityEventListener {
126
127        /**
128         * Callback for receiving an {@link AccessibilityEvent}.
129         * <p>
130         * <strong>Note:</strong> This method is <strong>NOT</strong> executed
131         * on the main test thread. The client is responsible for proper
132         * synchronization.
133         * </p>
134         * <p>
135         * <strong>Note:</strong> It is responsibility of the client
136         * to recycle the received events to minimize object creation.
137         * </p>
138         *
139         * @param event The received event.
140         */
141        public void onAccessibilityEvent(AccessibilityEvent event);
142    }
143
144    /**
145     * Listener for filtering accessibility events.
146     */
147    public static interface AccessibilityEventFilter {
148
149        /**
150         * Callback for determining whether an event is accepted or
151         * it is filtered out.
152         *
153         * @param event The event to process.
154         * @return True if the event is accepted, false to filter it out.
155         */
156        public boolean accept(AccessibilityEvent event);
157    }
158
159    /**
160     * Creates a new instance that will handle callbacks from the accessibility
161     * layer on the thread of the provided looper and perform requests for privileged
162     * operations on the provided connection.
163     *
164     * @param looper The looper on which to execute accessibility callbacks.
165     * @param connection The connection for performing privileged operations.
166     *
167     * @hide
168     */
169    public UiAutomation(Looper looper, IUiAutomationConnection connection) {
170        if (looper == null) {
171            throw new IllegalArgumentException("Looper cannot be null!");
172        }
173        if (connection == null) {
174            throw new IllegalArgumentException("Connection cannot be null!");
175        }
176        mUiAutomationConnection = connection;
177        mClient = new IAccessibilityServiceClientImpl(looper);
178    }
179
180    /**
181     * Connects this UiAutomation to the accessibility introspection APIs.
182     *
183     * @hide
184     */
185    public void connect() {
186        synchronized (mLock) {
187            throwIfConnectedLocked();
188            if (mIsConnecting) {
189                return;
190            }
191            mIsConnecting = true;
192        }
193
194        try {
195            // Calling out without a lock held.
196            mUiAutomationConnection.connect(mClient);
197        } catch (RemoteException re) {
198            throw new RuntimeException("Error while connecting UiAutomation", re);
199        }
200
201        synchronized (mLock) {
202            final long startTimeMillis = SystemClock.uptimeMillis();
203            try {
204                while (true) {
205                    if (isConnectedLocked()) {
206                        break;
207                    }
208                    final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
209                    final long remainingTimeMillis = CONNECT_TIMEOUT_MILLIS - elapsedTimeMillis;
210                    if (remainingTimeMillis <= 0) {
211                        throw new RuntimeException("Error while connecting UiAutomation");
212                    }
213                    try {
214                        mLock.wait(remainingTimeMillis);
215                    } catch (InterruptedException ie) {
216                        /* ignore */
217                    }
218                }
219            } finally {
220                mIsConnecting = false;
221            }
222        }
223    }
224
225    /**
226     * Disconnects this UiAutomation from the accessibility introspection APIs.
227     *
228     * @hide
229     */
230    public void disconnect() {
231        synchronized (mLock) {
232            if (mIsConnecting) {
233                throw new IllegalStateException(
234                        "Cannot call disconnect() while connecting!");
235            }
236            throwIfNotConnectedLocked();
237            mConnectionId = CONNECTION_ID_UNDEFINED;
238        }
239        try {
240            // Calling out without a lock held.
241            mUiAutomationConnection.disconnect();
242        } catch (RemoteException re) {
243            throw new RuntimeException("Error while disconnecting UiAutomation", re);
244        }
245    }
246
247    /**
248     * The id of the {@link IAccessibilityInteractionConnection} for querying
249     * the screen content. This is here for legacy purposes since some tools use
250     * hidden APIs to introspect the screen.
251     *
252     * @hide
253     */
254    public int getConnectionId() {
255        synchronized (mLock) {
256            throwIfNotConnectedLocked();
257            return mConnectionId;
258        }
259    }
260
261    /**
262     * Sets a callback for observing the stream of {@link AccessibilityEvent}s.
263     *
264     * @param listener The callback.
265     */
266    public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) {
267        synchronized (mLock) {
268            mOnAccessibilityEventListener = listener;
269        }
270    }
271
272    /**
273     * Performs a global action. Such an action can be performed at any moment
274     * regardless of the current application or user location in that application.
275     * For example going back, going home, opening recents, etc.
276     *
277     * @param action The action to perform.
278     * @return Whether the action was successfully performed.
279     *
280     * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_BACK
281     * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_HOME
282     * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_NOTIFICATIONS
283     * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_RECENTS
284     */
285    public final boolean performGlobalAction(int action) {
286        final IAccessibilityServiceConnection connection;
287        synchronized (mLock) {
288            throwIfNotConnectedLocked();
289            connection = AccessibilityInteractionClient.getInstance()
290                    .getConnection(mConnectionId);
291        }
292        // Calling out without a lock held.
293        if (connection != null) {
294            try {
295                return connection.performGlobalAction(action);
296            } catch (RemoteException re) {
297                Log.w(LOG_TAG, "Error while calling performGlobalAction", re);
298            }
299        }
300        return false;
301    }
302
303    /**
304     * Find the view that has the specified focus type. The search is performed
305     * across all windows.
306     * <p>
307     * <strong>Note:</strong> In order to access the windows you have to opt-in
308     * to retrieve the interactive windows by setting the
309     * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag.
310     * Otherwise, the search will be performed only in the active window.
311     * </p>
312     *
313     * @param focus The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or
314     *         {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}.
315     * @return The node info of the focused view or null.
316     *
317     * @see AccessibilityNodeInfo#FOCUS_INPUT
318     * @see AccessibilityNodeInfo#FOCUS_ACCESSIBILITY
319     */
320    public AccessibilityNodeInfo findFocus(int focus) {
321        return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId,
322                AccessibilityNodeInfo.ANY_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, focus);
323    }
324
325    /**
326     * Gets the an {@link AccessibilityServiceInfo} describing this UiAutomation.
327     * This method is useful if one wants to change some of the dynamically
328     * configurable properties at runtime.
329     *
330     * @return The accessibility service info.
331     *
332     * @see AccessibilityServiceInfo
333     */
334    public final AccessibilityServiceInfo getServiceInfo() {
335        final IAccessibilityServiceConnection connection;
336        synchronized (mLock) {
337            throwIfNotConnectedLocked();
338            connection = AccessibilityInteractionClient.getInstance()
339                    .getConnection(mConnectionId);
340        }
341        // Calling out without a lock held.
342        if (connection != null) {
343            try {
344                return connection.getServiceInfo();
345            } catch (RemoteException re) {
346                Log.w(LOG_TAG, "Error while getting AccessibilityServiceInfo", re);
347            }
348        }
349        return null;
350    }
351
352    /**
353     * Sets the {@link AccessibilityServiceInfo} that describes how this
354     * UiAutomation will be handled by the platform accessibility layer.
355     *
356     * @param info The info.
357     *
358     * @see AccessibilityServiceInfo
359     */
360    public final void setServiceInfo(AccessibilityServiceInfo info) {
361        final IAccessibilityServiceConnection connection;
362        synchronized (mLock) {
363            throwIfNotConnectedLocked();
364            AccessibilityInteractionClient.getInstance().clearCache();
365            connection = AccessibilityInteractionClient.getInstance()
366                    .getConnection(mConnectionId);
367        }
368        // Calling out without a lock held.
369        if (connection != null) {
370            try {
371                connection.setServiceInfo(info);
372            } catch (RemoteException re) {
373                Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re);
374            }
375        }
376    }
377
378    /**
379     * Gets the windows on the screen. This method returns only the windows
380     * that a sighted user can interact with, as opposed to all windows.
381     * For example, if there is a modal dialog shown and the user cannot touch
382     * anything behind it, then only the modal window will be reported
383     * (assuming it is the top one). For convenience the returned windows
384     * are ordered in a descending layer order, which is the windows that
385     * are higher in the Z-order are reported first.
386     * <p>
387     * <strong>Note:</strong> In order to access the windows you have to opt-in
388     * to retrieve the interactive windows by setting the
389     * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag.
390     * </p>
391     *
392     * @return The windows if there are windows such, otherwise an empty list.
393     */
394    public List<AccessibilityWindowInfo> getWindows() {
395        final int connectionId;
396        synchronized (mLock) {
397            throwIfNotConnectedLocked();
398            connectionId = mConnectionId;
399        }
400        // Calling out without a lock held.
401        return AccessibilityInteractionClient.getInstance()
402                .getWindows(connectionId);
403    }
404
405    /**
406     * Gets the root {@link AccessibilityNodeInfo} in the active window.
407     *
408     * @return The root info.
409     */
410    public AccessibilityNodeInfo getRootInActiveWindow() {
411        final int connectionId;
412        synchronized (mLock) {
413            throwIfNotConnectedLocked();
414            connectionId = mConnectionId;
415        }
416        // Calling out without a lock held.
417        return AccessibilityInteractionClient.getInstance()
418                .getRootInActiveWindow(connectionId);
419    }
420
421    /**
422     * A method for injecting an arbitrary input event.
423     * <p>
424     * <strong>Note:</strong> It is caller's responsibility to recycle the event.
425     * </p>
426     * @param event The event to inject.
427     * @param sync Whether to inject the event synchronously.
428     * @return Whether event injection succeeded.
429     */
430    public boolean injectInputEvent(InputEvent event, boolean sync) {
431        synchronized (mLock) {
432            throwIfNotConnectedLocked();
433        }
434        try {
435            if (DEBUG) {
436                Log.i(LOG_TAG, "Injecting: " + event + " sync: " + sync);
437            }
438            // Calling out without a lock held.
439            return mUiAutomationConnection.injectInputEvent(event, sync);
440        } catch (RemoteException re) {
441            Log.e(LOG_TAG, "Error while injecting input event!", re);
442        }
443        return false;
444    }
445
446    /**
447     * Sets the device rotation. A client can freeze the rotation in
448     * desired state or freeze the rotation to its current state or
449     * unfreeze the rotation (rotating the device changes its rotation
450     * state).
451     *
452     * @param rotation The desired rotation.
453     * @return Whether the rotation was set successfully.
454     *
455     * @see #ROTATION_FREEZE_0
456     * @see #ROTATION_FREEZE_90
457     * @see #ROTATION_FREEZE_180
458     * @see #ROTATION_FREEZE_270
459     * @see #ROTATION_FREEZE_CURRENT
460     * @see #ROTATION_UNFREEZE
461     */
462    public boolean setRotation(int rotation) {
463        synchronized (mLock) {
464            throwIfNotConnectedLocked();
465        }
466        switch (rotation) {
467            case ROTATION_FREEZE_0:
468            case ROTATION_FREEZE_90:
469            case ROTATION_FREEZE_180:
470            case ROTATION_FREEZE_270:
471            case ROTATION_UNFREEZE:
472            case ROTATION_FREEZE_CURRENT: {
473                try {
474                    // Calling out without a lock held.
475                    mUiAutomationConnection.setRotation(rotation);
476                    return true;
477                } catch (RemoteException re) {
478                    Log.e(LOG_TAG, "Error while setting rotation!", re);
479                }
480            } return false;
481            default: {
482                throw new IllegalArgumentException("Invalid rotation.");
483            }
484        }
485    }
486
487    /**
488     * Executes a command and waits for a specific accessibility event up to a
489     * given wait timeout. To detect a sequence of events one can implement a
490     * filter that keeps track of seen events of the expected sequence and
491     * returns true after the last event of that sequence is received.
492     * <p>
493     * <strong>Note:</strong> It is caller's responsibility to recycle the returned event.
494     * </p>
495     * @param command The command to execute.
496     * @param filter Filter that recognizes the expected event.
497     * @param timeoutMillis The wait timeout in milliseconds.
498     *
499     * @throws TimeoutException If the expected event is not received within the timeout.
500     */
501    public AccessibilityEvent executeAndWaitForEvent(Runnable command,
502            AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException {
503        // Acquire the lock and prepare for receiving events.
504        synchronized (mLock) {
505            throwIfNotConnectedLocked();
506            mEventQueue.clear();
507            // Prepare to wait for an event.
508            mWaitingForEventDelivery = true;
509        }
510
511        // Note: We have to release the lock since calling out with this lock held
512        // can bite. We will correctly filter out events from other interactions,
513        // so starting to collect events before running the action is just fine.
514
515        // We will ignore events from previous interactions.
516        final long executionStartTimeMillis = SystemClock.uptimeMillis();
517        // Execute the command *without* the lock being held.
518        command.run();
519
520        // Acquire the lock and wait for the event.
521        synchronized (mLock) {
522            try {
523                // Wait for the event.
524                final long startTimeMillis = SystemClock.uptimeMillis();
525                while (true) {
526                    // Drain the event queue
527                    while (!mEventQueue.isEmpty()) {
528                        AccessibilityEvent event = mEventQueue.remove(0);
529                        // Ignore events from previous interactions.
530                        if (event.getEventTime() < executionStartTimeMillis) {
531                            continue;
532                        }
533                        if (filter.accept(event)) {
534                            return event;
535                        }
536                        event.recycle();
537                    }
538                    // Check if timed out and if not wait.
539                    final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
540                    final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
541                    if (remainingTimeMillis <= 0) {
542                        throw new TimeoutException("Expected event not received within: "
543                                + timeoutMillis + " ms.");
544                    }
545                    try {
546                        mLock.wait(remainingTimeMillis);
547                    } catch (InterruptedException ie) {
548                        /* ignore */
549                    }
550                }
551            } finally {
552                mWaitingForEventDelivery = false;
553                mEventQueue.clear();
554                mLock.notifyAll();
555            }
556        }
557    }
558
559    /**
560     * Waits for the accessibility event stream to become idle, which is not to
561     * have received an accessibility event within <code>idleTimeoutMillis</code>.
562     * The total time spent to wait for an idle accessibility event stream is bounded
563     * by the <code>globalTimeoutMillis</code>.
564     *
565     * @param idleTimeoutMillis The timeout in milliseconds between two events
566     *            to consider the device idle.
567     * @param globalTimeoutMillis The maximal global timeout in milliseconds in
568     *            which to wait for an idle state.
569     *
570     * @throws TimeoutException If no idle state was detected within
571     *            <code>globalTimeoutMillis.</code>
572     */
573    public void waitForIdle(long idleTimeoutMillis, long globalTimeoutMillis)
574            throws TimeoutException {
575        synchronized (mLock) {
576            throwIfNotConnectedLocked();
577
578            final long startTimeMillis = SystemClock.uptimeMillis();
579            if (mLastEventTimeMillis <= 0) {
580                mLastEventTimeMillis = startTimeMillis;
581            }
582
583            while (true) {
584                final long currentTimeMillis = SystemClock.uptimeMillis();
585                // Did we get idle state within the global timeout?
586                final long elapsedGlobalTimeMillis = currentTimeMillis - startTimeMillis;
587                final long remainingGlobalTimeMillis =
588                        globalTimeoutMillis - elapsedGlobalTimeMillis;
589                if (remainingGlobalTimeMillis <= 0) {
590                    throw new TimeoutException("No idle state with idle timeout: "
591                            + idleTimeoutMillis + " within global timeout: "
592                            + globalTimeoutMillis);
593                }
594                // Did we get an idle state within the idle timeout?
595                final long elapsedIdleTimeMillis = currentTimeMillis - mLastEventTimeMillis;
596                final long remainingIdleTimeMillis = idleTimeoutMillis - elapsedIdleTimeMillis;
597                if (remainingIdleTimeMillis <= 0) {
598                    return;
599                }
600                try {
601                     mLock.wait(remainingIdleTimeMillis);
602                } catch (InterruptedException ie) {
603                     /* ignore */
604                }
605            }
606        }
607    }
608
609    /**
610     * Takes a screenshot.
611     *
612     * @return The screenshot bitmap on success, null otherwise.
613     */
614    public Bitmap takeScreenshot() {
615        synchronized (mLock) {
616            throwIfNotConnectedLocked();
617        }
618        Display display = DisplayManagerGlobal.getInstance()
619                .getRealDisplay(Display.DEFAULT_DISPLAY);
620        Point displaySize = new Point();
621        display.getRealSize(displaySize);
622        final int displayWidth = displaySize.x;
623        final int displayHeight = displaySize.y;
624
625        final float screenshotWidth;
626        final float screenshotHeight;
627
628        final int rotation = display.getRotation();
629        switch (rotation) {
630            case ROTATION_FREEZE_0: {
631                screenshotWidth = displayWidth;
632                screenshotHeight = displayHeight;
633            } break;
634            case ROTATION_FREEZE_90: {
635                screenshotWidth = displayHeight;
636                screenshotHeight = displayWidth;
637            } break;
638            case ROTATION_FREEZE_180: {
639                screenshotWidth = displayWidth;
640                screenshotHeight = displayHeight;
641            } break;
642            case ROTATION_FREEZE_270: {
643                screenshotWidth = displayHeight;
644                screenshotHeight = displayWidth;
645            } break;
646            default: {
647                throw new IllegalArgumentException("Invalid rotation: "
648                        + rotation);
649            }
650        }
651
652        // Take the screenshot
653        Bitmap screenShot = null;
654        try {
655            // Calling out without a lock held.
656            screenShot = mUiAutomationConnection.takeScreenshot((int) screenshotWidth,
657                    (int) screenshotHeight);
658            if (screenShot == null) {
659                return null;
660            }
661        } catch (RemoteException re) {
662            Log.e(LOG_TAG, "Error while taking screnshot!", re);
663            return null;
664        }
665
666        // Rotate the screenshot to the current orientation
667        if (rotation != ROTATION_FREEZE_0) {
668            Bitmap unrotatedScreenShot = Bitmap.createBitmap(displayWidth, displayHeight,
669                    Bitmap.Config.ARGB_8888);
670            Canvas canvas = new Canvas(unrotatedScreenShot);
671            canvas.translate(unrotatedScreenShot.getWidth() / 2,
672                    unrotatedScreenShot.getHeight() / 2);
673            canvas.rotate(getDegreesForRotation(rotation));
674            canvas.translate(- screenshotWidth / 2, - screenshotHeight / 2);
675            canvas.drawBitmap(screenShot, 0, 0, null);
676            canvas.setBitmap(null);
677            screenShot.recycle();
678            screenShot = unrotatedScreenShot;
679        }
680
681        // Optimization
682        screenShot.setHasAlpha(false);
683
684        return screenShot;
685    }
686
687    /**
688     * Sets whether this UiAutomation to run in a "monkey" mode. Applications can query whether
689     * they are executed in a "monkey" mode, i.e. run by a test framework, and avoid doing
690     * potentially undesirable actions such as calling 911 or posting on public forums etc.
691     *
692     * @param enable whether to run in a "monkey" mode or not. Default is not.
693     * @see {@link android.app.ActivityManager#isUserAMonkey()}
694     */
695    public void setRunAsMonkey(boolean enable) {
696        synchronized (mLock) {
697            throwIfNotConnectedLocked();
698        }
699        try {
700            ActivityManagerNative.getDefault().setUserIsMonkey(enable);
701        } catch (RemoteException re) {
702            Log.e(LOG_TAG, "Error while setting run as monkey!", re);
703        }
704    }
705
706    /**
707     * Clears the frame statistics for the content of a given window. These
708     * statistics contain information about the most recently rendered content
709     * frames.
710     *
711     * @param windowId The window id.
712     * @return Whether the window is present and its frame statistics
713     *         were cleared.
714     *
715     * @see android.view.WindowContentFrameStats
716     * @see #getWindowContentFrameStats(int)
717     * @see #getWindows()
718     * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId()
719     */
720    public boolean clearWindowContentFrameStats(int windowId) {
721        synchronized (mLock) {
722            throwIfNotConnectedLocked();
723        }
724        try {
725            if (DEBUG) {
726                Log.i(LOG_TAG, "Clearing content frame stats for window: " + windowId);
727            }
728            // Calling out without a lock held.
729            return mUiAutomationConnection.clearWindowContentFrameStats(windowId);
730        } catch (RemoteException re) {
731            Log.e(LOG_TAG, "Error clearing window content frame stats!", re);
732        }
733        return false;
734    }
735
736    /**
737     * Gets the frame statistics for a given window. These statistics contain
738     * information about the most recently rendered content frames.
739     * <p>
740     * A typical usage requires clearing the window frame statistics via {@link
741     * #clearWindowContentFrameStats(int)} followed by an interaction with the UI and
742     * finally getting the window frame statistics via calling this method.
743     * </p>
744     * <pre>
745     * // Assume we have at least one window.
746     * final int windowId = getWindows().get(0).getId();
747     *
748     * // Start with a clean slate.
749     * uiAutimation.clearWindowContentFrameStats(windowId);
750     *
751     * // Do stuff with the UI.
752     *
753     * // Get the frame statistics.
754     * WindowContentFrameStats stats = uiAutomation.getWindowContentFrameStats(windowId);
755     * </pre>
756     *
757     * @param windowId The window id.
758     * @return The window frame statistics, or null if the window is not present.
759     *
760     * @see android.view.WindowContentFrameStats
761     * @see #clearWindowContentFrameStats(int)
762     * @see #getWindows()
763     * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId()
764     */
765    public WindowContentFrameStats getWindowContentFrameStats(int windowId) {
766        synchronized (mLock) {
767            throwIfNotConnectedLocked();
768        }
769        try {
770            if (DEBUG) {
771                Log.i(LOG_TAG, "Getting content frame stats for window: " + windowId);
772            }
773            // Calling out without a lock held.
774            return mUiAutomationConnection.getWindowContentFrameStats(windowId);
775        } catch (RemoteException re) {
776            Log.e(LOG_TAG, "Error getting window content frame stats!", re);
777        }
778        return null;
779    }
780
781    /**
782     * Clears the window animation rendering statistics. These statistics contain
783     * information about the most recently rendered window animation frames, i.e.
784     * for window transition animations.
785     *
786     * @see android.view.WindowAnimationFrameStats
787     * @see #getWindowAnimationFrameStats()
788     * @see android.R.styleable#WindowAnimation
789     */
790    public void clearWindowAnimationFrameStats() {
791        synchronized (mLock) {
792            throwIfNotConnectedLocked();
793        }
794        try {
795            if (DEBUG) {
796                Log.i(LOG_TAG, "Clearing window animation frame stats");
797            }
798            // Calling out without a lock held.
799            mUiAutomationConnection.clearWindowAnimationFrameStats();
800        } catch (RemoteException re) {
801            Log.e(LOG_TAG, "Error clearing window animation frame stats!", re);
802        }
803    }
804
805    /**
806     * Gets the window animation frame statistics. These statistics contain
807     * information about the most recently rendered window animation frames, i.e.
808     * for window transition animations.
809     *
810     * <p>
811     * A typical usage requires clearing the window animation frame statistics via
812     * {@link #clearWindowAnimationFrameStats()} followed by an interaction that causes
813     * a window transition which uses a window animation and finally getting the window
814     * animation frame statistics by calling this method.
815     * </p>
816     * <pre>
817     * // Start with a clean slate.
818     * uiAutimation.clearWindowAnimationFrameStats();
819     *
820     * // Do stuff to trigger a window transition.
821     *
822     * // Get the frame statistics.
823     * WindowAnimationFrameStats stats = uiAutomation.getWindowAnimationFrameStats();
824     * </pre>
825     *
826     * @return The window animation frame statistics.
827     *
828     * @see android.view.WindowAnimationFrameStats
829     * @see #clearWindowAnimationFrameStats()
830     * @see android.R.styleable#WindowAnimation
831     */
832    public WindowAnimationFrameStats getWindowAnimationFrameStats() {
833        synchronized (mLock) {
834            throwIfNotConnectedLocked();
835        }
836        try {
837            if (DEBUG) {
838                Log.i(LOG_TAG, "Getting window animation frame stats");
839            }
840            // Calling out without a lock held.
841            return mUiAutomationConnection.getWindowAnimationFrameStats();
842        } catch (RemoteException re) {
843            Log.e(LOG_TAG, "Error getting window animation frame stats!", re);
844        }
845        return null;
846    }
847
848    /**
849     * Executes a shell command. This method returs a file descriptor that points
850     * to the standard output stream. The command execution is similar to running
851     * "adb shell <command>" from a host connected to the device.
852     * <p>
853     * <strong>Note:</strong> It is your responsibility to close the retunred file
854     * descriptor once you are done reading.
855     * </p>
856     *
857     * @param command The command to execute.
858     * @return A file descriptor to the standard output stream.
859     */
860    public ParcelFileDescriptor executeShellCommand(String command) {
861        synchronized (mLock) {
862            throwIfNotConnectedLocked();
863        }
864
865        ParcelFileDescriptor source = null;
866        ParcelFileDescriptor sink = null;
867
868        try {
869            ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
870            source = pipe[0];
871            sink = pipe[1];
872
873            // Calling out without a lock held.
874            mUiAutomationConnection.executeShellCommand(command, sink);
875        } catch (IOException ioe) {
876            Log.e(LOG_TAG, "Error executing shell command!", ioe);
877        } catch (RemoteException re) {
878            Log.e(LOG_TAG, "Error executing shell command!", re);
879        } finally {
880            IoUtils.closeQuietly(sink);
881        }
882
883        return source;
884    }
885
886    private static float getDegreesForRotation(int value) {
887        switch (value) {
888            case Surface.ROTATION_90: {
889                return 360f - 90f;
890            }
891            case Surface.ROTATION_180: {
892                return 360f - 180f;
893            }
894            case Surface.ROTATION_270: {
895                return 360f - 270f;
896            } default: {
897                return 0;
898            }
899        }
900    }
901
902    private boolean isConnectedLocked() {
903        return mConnectionId != CONNECTION_ID_UNDEFINED;
904    }
905
906    private void throwIfConnectedLocked() {
907        if (mConnectionId != CONNECTION_ID_UNDEFINED) {
908            throw new IllegalStateException("UiAutomation not connected!");
909        }
910    }
911
912    private void throwIfNotConnectedLocked() {
913        if (!isConnectedLocked()) {
914            throw new IllegalStateException("UiAutomation not connected!");
915        }
916    }
917
918    private class IAccessibilityServiceClientImpl extends IAccessibilityServiceClientWrapper {
919
920        public IAccessibilityServiceClientImpl(Looper looper) {
921            super(null, looper, new Callbacks() {
922                @Override
923                public void init(int connectionId, IBinder windowToken) {
924                    synchronized (mLock) {
925                        mConnectionId = connectionId;
926                        mLock.notifyAll();
927                    }
928                }
929
930                @Override
931                public void onServiceConnected() {
932                    /* do nothing */
933                }
934
935                @Override
936                public void onInterrupt() {
937                    /* do nothing */
938                }
939
940                @Override
941                public boolean onGesture(int gestureId) {
942                    /* do nothing */
943                    return false;
944                }
945
946                @Override
947                public void onAccessibilityEvent(AccessibilityEvent event) {
948                    synchronized (mLock) {
949                        mLastEventTimeMillis = event.getEventTime();
950                        if (mWaitingForEventDelivery) {
951                            mEventQueue.add(AccessibilityEvent.obtain(event));
952                        }
953                        mLock.notifyAll();
954                    }
955                    // Calling out only without a lock held.
956                    final OnAccessibilityEventListener listener = mOnAccessibilityEventListener;
957                    if (listener != null) {
958                        listener.onAccessibilityEvent(AccessibilityEvent.obtain(event));
959                    }
960                }
961
962                @Override
963                public boolean onKeyEvent(KeyEvent event) {
964                    return false;
965                }
966            });
967        }
968    }
969}
970