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