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