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