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