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