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