AccessibilityInteractionClient.java revision 55cdacb9ba22ce0e40918fd54acca7df62637ca6
1/*
2 ** Copyright 2011, 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.view.accessibility;
18
19import android.accessibilityservice.IAccessibilityServiceConnection;
20import android.graphics.Rect;
21import android.os.Message;
22import android.os.RemoteException;
23import android.os.SystemClock;
24import android.util.Log;
25import android.util.LongSparseArray;
26import android.util.SparseArray;
27
28import java.util.Collections;
29import java.util.List;
30import java.util.concurrent.atomic.AtomicInteger;
31
32/**
33 * This class is a singleton that performs accessibility interaction
34 * which is it queries remote view hierarchies about snapshots of their
35 * views as well requests from these hierarchies to perform certain
36 * actions on their views.
37 *
38 * Rationale: The content retrieval APIs are synchronous from a client's
39 *     perspective but internally they are asynchronous. The client thread
40 *     calls into the system requesting an action and providing a callback
41 *     to receive the result after which it waits up to a timeout for that
42 *     result. The system enforces security and the delegates the request
43 *     to a given view hierarchy where a message is posted (from a binder
44 *     thread) describing what to be performed by the main UI thread the
45 *     result of which it delivered via the mentioned callback. However,
46 *     the blocked client thread and the main UI thread of the target view
47 *     hierarchy can be the same thread, for example an accessibility service
48 *     and an activity run in the same process, thus they are executed on the
49 *     same main thread. In such a case the retrieval will fail since the UI
50 *     thread that has to process the message describing the work to be done
51 *     is blocked waiting for a result is has to compute! To avoid this scenario
52 *     when making a call the client also passes its process and thread ids so
53 *     the accessed view hierarchy can detect if the client making the request
54 *     is running in its main UI thread. In such a case the view hierarchy,
55 *     specifically the binder thread performing the IPC to it, does not post a
56 *     message to be run on the UI thread but passes it to the singleton
57 *     interaction client through which all interactions occur and the latter is
58 *     responsible to execute the message before starting to wait for the
59 *     asynchronous result delivered via the callback. In this case the expected
60 *     result is already received so no waiting is performed.
61 *
62 * @hide
63 */
64public final class AccessibilityInteractionClient
65        extends IAccessibilityInteractionConnectionCallback.Stub {
66
67    public static final int NO_ID = -1;
68
69    private static final String LOG_TAG = "AccessibilityInteractionClient";
70
71    private static final boolean DEBUG = false;
72
73    private static final long TIMEOUT_INTERACTION_MILLIS = 5000;
74
75    private static final Object sStaticLock = new Object();
76
77    private static final LongSparseArray<AccessibilityInteractionClient> sClients =
78        new LongSparseArray<AccessibilityInteractionClient>();
79
80    private final AtomicInteger mInteractionIdCounter = new AtomicInteger();
81
82    private final Object mInstanceLock = new Object();
83
84    private int mInteractionId = -1;
85
86    private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult;
87
88    private List<AccessibilityNodeInfo> mFindAccessibilityNodeInfosResult;
89
90    private boolean mPerformAccessibilityActionResult;
91
92    private Message mSameThreadMessage;
93
94    private final Rect mTempBounds = new Rect();
95
96    // The connection cache is shared between all interrogating threads.
97    private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
98        new SparseArray<IAccessibilityServiceConnection>();
99
100    /**
101     * @return The client for the current thread.
102     */
103    public static AccessibilityInteractionClient getInstance() {
104        final long threadId = Thread.currentThread().getId();
105        return getInstanceForThread(threadId);
106    }
107
108    /**
109     * <strong>Note:</strong> We keep one instance per interrogating thread since
110     * the instance contains state which can lead to undesired thread interleavings.
111     * We do not have a thread local variable since other threads should be able to
112     * look up the correct client knowing a thread id. See ViewRootImpl for details.
113     *
114     * @return The client for a given <code>threadId</code>.
115     */
116    public static AccessibilityInteractionClient getInstanceForThread(long threadId) {
117        synchronized (sStaticLock) {
118            AccessibilityInteractionClient client = sClients.get(threadId);
119            if (client == null) {
120                client = new AccessibilityInteractionClient();
121                sClients.put(threadId, client);
122            }
123            return client;
124        }
125    }
126
127    private AccessibilityInteractionClient() {
128        /* reducing constructor visibility */
129    }
130
131    /**
132     * Sets the message to be processed if the interacted view hierarchy
133     * and the interacting client are running in the same thread.
134     *
135     * @param message The message.
136     */
137    public void setSameThreadMessage(Message message) {
138        synchronized (mInstanceLock) {
139            mSameThreadMessage = message;
140            mInstanceLock.notifyAll();
141        }
142    }
143
144    /**
145     * Finds an {@link AccessibilityNodeInfo} by accessibility id.
146     *
147     * @param connectionId The id of a connection for interacting with the system.
148     * @param accessibilityWindowId A unique window id.
149     * @param accessibilityNodeId A unique node accessibility id
150     *     (accessibility view and virtual descendant id).
151     * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
152     */
153    public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
154            int accessibilityWindowId, long accessibilityNodeId) {
155        try {
156            IAccessibilityServiceConnection connection = getConnection(connectionId);
157            if (connection != null) {
158                final int interactionId = mInteractionIdCounter.getAndIncrement();
159                final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId(
160                        accessibilityWindowId, accessibilityNodeId, interactionId, this,
161                        Thread.currentThread().getId());
162                // If the scale is zero the call has failed.
163                if (windowScale > 0) {
164                    AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
165                            interactionId);
166                    finalizeAccessibilityNodeInfo(info, connectionId, windowScale);
167                    return info;
168                }
169            } else {
170                if (DEBUG) {
171                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
172                }
173            }
174        } catch (RemoteException re) {
175            if (DEBUG) {
176                Log.w(LOG_TAG, "Error while calling remote"
177                        + " findAccessibilityNodeInfoByAccessibilityId", re);
178            }
179        }
180        return null;
181    }
182
183    /**
184     * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed
185     * in the currently active window and starts from the root View in the window.
186     *
187     * @param connectionId The id of a connection for interacting with the system.
188     * @param viewId The id of the view.
189     * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
190     */
191    public AccessibilityNodeInfo findAccessibilityNodeInfoByViewIdInActiveWindow(int connectionId,
192            int viewId) {
193        try {
194            IAccessibilityServiceConnection connection = getConnection(connectionId);
195            if (connection != null) {
196                final int interactionId = mInteractionIdCounter.getAndIncrement();
197                final float windowScale =
198                    connection.findAccessibilityNodeInfoByViewIdInActiveWindow(viewId,
199                            interactionId, this, Thread.currentThread().getId());
200                // If the scale is zero the call has failed.
201                if (windowScale > 0) {
202                    AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
203                            interactionId);
204                    finalizeAccessibilityNodeInfo(info, connectionId, windowScale);
205                    return info;
206                }
207            } else {
208                if (DEBUG) {
209                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
210                }
211            }
212        } catch (RemoteException re) {
213            if (DEBUG) {
214                Log.w(LOG_TAG, "Error while calling remote"
215                        + " findAccessibilityNodeInfoByViewIdInActiveWindow", re);
216            }
217        }
218        return null;
219    }
220
221    /**
222     * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
223     * insensitive containment. The search is performed in the currently
224     * active window and starts from the root View in the window.
225     *
226     * @param connectionId The id of a connection for interacting with the system.
227     * @param text The searched text.
228     * @return A list of found {@link AccessibilityNodeInfo}s.
229     */
230    public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByTextInActiveWindow(
231            int connectionId, String text) {
232        try {
233            IAccessibilityServiceConnection connection = getConnection(connectionId);
234            if (connection != null) {
235                final int interactionId = mInteractionIdCounter.getAndIncrement();
236                final float windowScale =
237                    connection.findAccessibilityNodeInfosByTextInActiveWindow(text,
238                            interactionId, this, Thread.currentThread().getId());
239                // If the scale is zero the call has failed.
240                if (windowScale > 0) {
241                    List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
242                            interactionId);
243                    finalizeAccessibilityNodeInfos(infos, connectionId, windowScale);
244                    return infos;
245                }
246            } else {
247                if (DEBUG) {
248                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
249                }
250            }
251        } catch (RemoteException re) {
252            if (DEBUG) {
253                Log.w(LOG_TAG, "Error while calling remote"
254                        + " findAccessibilityNodeInfosByViewTextInActiveWindow", re);
255            }
256        }
257        return null;
258    }
259
260    /**
261     * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
262     * insensitive containment. The search is performed in the window whose
263     * id is specified and starts from the View whose accessibility id is
264     * specified.
265     *
266     * @param connectionId The id of a connection for interacting with the system.
267     * @param text The searched text.
268     * @param accessibilityWindowId A unique window id.
269     * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id) from
270     *        where to start the search. Use {@link android.view.View#NO_ID} to start from the root.
271     * @return A list of found {@link AccessibilityNodeInfo}s.
272     */
273    public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId,
274            String text, int accessibilityWindowId, long accessibilityNodeId) {
275        try {
276            IAccessibilityServiceConnection connection = getConnection(connectionId);
277            if (connection != null) {
278                final int interactionId = mInteractionIdCounter.getAndIncrement();
279                final float windowScale = connection.findAccessibilityNodeInfosByText(text,
280                        accessibilityWindowId, accessibilityNodeId, interactionId, this,
281                        Thread.currentThread().getId());
282                // If the scale is zero the call has failed.
283                if (windowScale > 0) {
284                    List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
285                            interactionId);
286                    finalizeAccessibilityNodeInfos(infos, connectionId, windowScale);
287                    return infos;
288                }
289            } else {
290                if (DEBUG) {
291                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
292                }
293            }
294        } catch (RemoteException re) {
295            if (DEBUG) {
296                Log.w(LOG_TAG, "Error while calling remote"
297                        + " findAccessibilityNodeInfosByViewText", re);
298            }
299        }
300        return Collections.emptyList();
301    }
302
303    /**
304     * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
305     *
306     * @param connectionId The id of a connection for interacting with the system.
307     * @param accessibilityWindowId The id of the window.
308     * @param accessibilityNodeId A unique node id (accessibility and virtual descendant id).
309     * @param action The action to perform.
310     * @return Whether the action was performed.
311     */
312    public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
313            long accessibilityNodeId, int action) {
314        try {
315            IAccessibilityServiceConnection connection = getConnection(connectionId);
316            if (connection != null) {
317                final int interactionId = mInteractionIdCounter.getAndIncrement();
318                final boolean success = connection.performAccessibilityAction(
319                        accessibilityWindowId, accessibilityNodeId, action, interactionId, this,
320                        Thread.currentThread().getId());
321                if (success) {
322                    return getPerformAccessibilityActionResult(interactionId);
323                }
324            } else {
325                if (DEBUG) {
326                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
327                }
328            }
329        } catch (RemoteException re) {
330            if (DEBUG) {
331                Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re);
332            }
333        }
334        return false;
335    }
336
337    /**
338     * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
339     *
340     * @param interactionId The interaction id to match the result with the request.
341     * @return The result {@link AccessibilityNodeInfo}.
342     */
343    private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) {
344        synchronized (mInstanceLock) {
345            final boolean success = waitForResultTimedLocked(interactionId);
346            AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null;
347            clearResultLocked();
348            return result;
349        }
350    }
351
352    /**
353     * {@inheritDoc}
354     */
355    public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info,
356                int interactionId) {
357        synchronized (mInstanceLock) {
358            if (interactionId > mInteractionId) {
359                mFindAccessibilityNodeInfoResult = info;
360                mInteractionId = interactionId;
361            }
362            mInstanceLock.notifyAll();
363        }
364    }
365
366    /**
367     * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s.
368     *
369     * @param interactionId The interaction id to match the result with the request.
370     * @return The result {@link AccessibilityNodeInfo}s.
371     */
372    private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear(
373                int interactionId) {
374        synchronized (mInstanceLock) {
375            final boolean success = waitForResultTimedLocked(interactionId);
376            List<AccessibilityNodeInfo> result = success ? mFindAccessibilityNodeInfosResult : null;
377            clearResultLocked();
378            return result;
379        }
380    }
381
382    /**
383     * {@inheritDoc}
384     */
385    public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
386                int interactionId) {
387        synchronized (mInstanceLock) {
388            if (interactionId > mInteractionId) {
389                mFindAccessibilityNodeInfosResult = infos;
390                mInteractionId = interactionId;
391            }
392            mInstanceLock.notifyAll();
393        }
394    }
395
396    /**
397     * Gets the result of a request to perform an accessibility action.
398     *
399     * @param interactionId The interaction id to match the result with the request.
400     * @return Whether the action was performed.
401     */
402    private boolean getPerformAccessibilityActionResult(int interactionId) {
403        synchronized (mInstanceLock) {
404            final boolean success = waitForResultTimedLocked(interactionId);
405            final boolean result = success ? mPerformAccessibilityActionResult : false;
406            clearResultLocked();
407            return result;
408        }
409    }
410
411    /**
412     * {@inheritDoc}
413     */
414    public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) {
415        synchronized (mInstanceLock) {
416            if (interactionId > mInteractionId) {
417                mPerformAccessibilityActionResult = succeeded;
418                mInteractionId = interactionId;
419            }
420            mInstanceLock.notifyAll();
421        }
422    }
423
424    /**
425     * Clears the result state.
426     */
427    private void clearResultLocked() {
428        mInteractionId = -1;
429        mFindAccessibilityNodeInfoResult = null;
430        mFindAccessibilityNodeInfosResult = null;
431        mPerformAccessibilityActionResult = false;
432    }
433
434    /**
435     * Waits up to a given bound for a result of a request and returns it.
436     *
437     * @param interactionId The interaction id to match the result with the request.
438     * @return Whether the result was received.
439     */
440    private boolean waitForResultTimedLocked(int interactionId) {
441        long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS;
442        final long startTimeMillis = SystemClock.uptimeMillis();
443        while (true) {
444            try {
445                Message sameProcessMessage = getSameProcessMessageAndClear();
446                if (sameProcessMessage != null) {
447                    sameProcessMessage.getTarget().handleMessage(sameProcessMessage);
448                }
449
450                if (mInteractionId == interactionId) {
451                    return true;
452                }
453                if (mInteractionId > interactionId) {
454                    return false;
455                }
456                final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
457                waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
458                if (waitTimeMillis <= 0) {
459                    return false;
460                }
461                mInstanceLock.wait(waitTimeMillis);
462            } catch (InterruptedException ie) {
463                /* ignore */
464            }
465        }
466    }
467
468    /**
469     * Applies compatibility scale to the info bounds if it is not equal to one.
470     *
471     * @param info The info whose bounds to scale.
472     * @param scale The scale to apply.
473     */
474    private void applyCompatibilityScaleIfNeeded(AccessibilityNodeInfo info, float scale) {
475        if (scale == 1.0f) {
476            return;
477        }
478        Rect bounds = mTempBounds;
479        info.getBoundsInParent(bounds);
480        bounds.scale(scale);
481        info.setBoundsInParent(bounds);
482
483        info.getBoundsInScreen(bounds);
484        bounds.scale(scale);
485        info.setBoundsInScreen(bounds);
486    }
487
488    /**
489     * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
490     *
491     * @param info The info.
492     * @param connectionId The id of the connection to the system.
493     * @param windowScale The source window compatibility scale.
494     */
495    private void finalizeAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId,
496            float windowScale) {
497        if (info != null) {
498            applyCompatibilityScaleIfNeeded(info, windowScale);
499            info.setConnectionId(connectionId);
500            info.setSealed(true);
501        }
502    }
503
504    /**
505     * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
506     *
507     * @param infos The {@link AccessibilityNodeInfo}s.
508     * @param connectionId The id of the connection to the system.
509     * @param windowScale The source window compatibility scale.
510     */
511    private void finalizeAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
512            int connectionId, float windowScale) {
513        if (infos != null) {
514            final int infosCount = infos.size();
515            for (int i = 0; i < infosCount; i++) {
516                AccessibilityNodeInfo info = infos.get(i);
517                finalizeAccessibilityNodeInfo(info, connectionId, windowScale);
518            }
519        }
520    }
521
522    /**
523     * Gets the message stored if the interacted and interacting
524     * threads are the same.
525     *
526     * @return The message.
527     */
528    private Message getSameProcessMessageAndClear() {
529        synchronized (mInstanceLock) {
530            Message result = mSameThreadMessage;
531            mSameThreadMessage = null;
532            return result;
533        }
534    }
535
536    /**
537     * Gets a cached accessibility service connection.
538     *
539     * @param connectionId The connection id.
540     * @return The cached connection if such.
541     */
542    public IAccessibilityServiceConnection getConnection(int connectionId) {
543        synchronized (sConnectionCache) {
544            return sConnectionCache.get(connectionId);
545        }
546    }
547
548    /**
549     * Adds a cached accessibility service connection.
550     *
551     * @param connectionId The connection id.
552     * @param connection The connection.
553     */
554    public void addConnection(int connectionId, IAccessibilityServiceConnection connection) {
555        synchronized (sConnectionCache) {
556            sConnectionCache.put(connectionId, connection);
557        }
558    }
559
560    /**
561     * Removes a cached accessibility service connection.
562     *
563     * @param connectionId The connection id.
564     */
565    public void removeConnection(int connectionId) {
566        synchronized (sConnectionCache) {
567            sConnectionCache.remove(connectionId);
568        }
569    }
570}
571