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