AccessibilityInteractionClient.java revision 8bd69610aafc6995126965d1d23b771fe02a9084
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        }
108    }
109
110    /**
111     * Finds an {@link AccessibilityNodeInfo} by accessibility id.
112     *
113     * @param connection A connection for interacting with the system.
114     * @param accessibilityWindowId A unique window id.
115     * @param accessibilityViewId A unique View accessibility id.
116     * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
117     */
118    public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(
119            IAccessibilityServiceConnection connection, int accessibilityWindowId,
120            int accessibilityViewId) {
121        try {
122            final int interactionId = mInteractionIdCounter.getAndIncrement();
123            final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId(
124                    accessibilityWindowId, accessibilityViewId, interactionId, this,
125                    Thread.currentThread().getId());
126            // If the scale is zero the call has failed.
127            if (windowScale > 0) {
128                handleSameThreadMessageIfNeeded();
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 id The id of the node.
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                handleSameThreadMessageIfNeeded();
157                AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
158                        interactionId);
159                finalizeAccessibilityNodeInfo(info, connection, windowScale);
160                return info;
161            }
162        } catch (RemoteException re) {
163            /* ignore */
164        }
165        return null;
166    }
167
168    /**
169     * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
170     * insensitive containment. The search is performed in the currently
171     * active window and starts from the root View in the window.
172     *
173     * @param connection A connection for interacting with the system.
174     * @param text The searched text.
175     * @return A list of found {@link AccessibilityNodeInfo}s.
176     */
177    public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewTextInActiveWindow(
178            IAccessibilityServiceConnection connection, String text) {
179        try {
180            final int interactionId = mInteractionIdCounter.getAndIncrement();
181            final float windowScale = connection.findAccessibilityNodeInfosByViewTextInActiveWindow(
182                    text, interactionId, this, Thread.currentThread().getId());
183            // If the scale is zero the call has failed.
184            if (windowScale > 0) {
185                handleSameThreadMessageIfNeeded();
186                List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
187                        interactionId);
188                finalizeAccessibilityNodeInfos(infos, connection, windowScale);
189                return infos;
190            }
191        } catch (RemoteException re) {
192            /* ignore */
193        }
194        return null;
195    }
196
197    /**
198     * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
199     * insensitive containment. The search is performed in the window whose
200     * id is specified and starts from the View whose accessibility id is
201     * specified.
202     *
203     * @param connection A connection for interacting with the system.
204     * @param text The searched text.
205     * @param accessibilityWindowId A unique window id.
206     * @param accessibilityViewId A unique View accessibility id from where to start the search.
207     *        Use {@link android.view.View#NO_ID} to start from the root.
208     * @return A list of found {@link AccessibilityNodeInfo}s.
209     */
210    public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewText(
211            IAccessibilityServiceConnection connection, String text, int accessibilityWindowId,
212            int accessibilityViewId) {
213        try {
214            final int interactionId = mInteractionIdCounter.getAndIncrement();
215            final float windowScale = connection.findAccessibilityNodeInfosByViewText(text,
216                    accessibilityWindowId, accessibilityViewId, interactionId, this,
217                    Thread.currentThread().getId());
218            // If the scale is zero the call has failed.
219            if (windowScale > 0) {
220                handleSameThreadMessageIfNeeded();
221                List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
222                        interactionId);
223                finalizeAccessibilityNodeInfos(infos, connection, windowScale);
224                return infos;
225            }
226        } catch (RemoteException re) {
227            /* ignore */
228        }
229        return Collections.emptyList();
230    }
231
232    /**
233     * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
234     *
235     * @param connection A connection for interacting with the system.
236     * @param accessibilityWindowId The id of the window.
237     * @param accessibilityViewId A unique View accessibility id.
238     * @param action The action to perform.
239     * @return Whether the action was performed.
240     */
241    public boolean performAccessibilityAction(IAccessibilityServiceConnection connection,
242            int accessibilityWindowId, int accessibilityViewId, int action) {
243        try {
244            final int interactionId = mInteractionIdCounter.getAndIncrement();
245            final boolean success = connection.performAccessibilityAction(
246                    accessibilityWindowId, accessibilityViewId, action, interactionId, this,
247                    Thread.currentThread().getId());
248            if (success) {
249                handleSameThreadMessageIfNeeded();
250                return getPerformAccessibilityActionResult(interactionId);
251            }
252        } catch (RemoteException re) {
253            /* ignore */
254        }
255        return false;
256    }
257
258    /**
259     * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
260     *
261     * @param interactionId The interaction id to match the result with the request.
262     * @return The result {@link AccessibilityNodeInfo}.
263     */
264    private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) {
265        synchronized (mInstanceLock) {
266            final boolean success = waitForResultTimedLocked(interactionId);
267            AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null;
268            clearResultLocked();
269            return result;
270        }
271    }
272
273    /**
274     * {@inheritDoc}
275     */
276    public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info,
277                int interactionId) {
278        synchronized (mInstanceLock) {
279            if (interactionId > mInteractionId) {
280                mFindAccessibilityNodeInfoResult = info;
281                mInteractionId = interactionId;
282            }
283            mInstanceLock.notifyAll();
284        }
285    }
286
287    /**
288     * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s.
289     *
290     * @param interactionId The interaction id to match the result with the request.
291     * @return The result {@link AccessibilityNodeInfo}s.
292     */
293    private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear(
294                int interactionId) {
295        synchronized (mInstanceLock) {
296            final boolean success = waitForResultTimedLocked(interactionId);
297            List<AccessibilityNodeInfo> result = success ? mFindAccessibilityNodeInfosResult : null;
298            clearResultLocked();
299            return result;
300        }
301    }
302
303    /**
304     * {@inheritDoc}
305     */
306    public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
307                int interactionId) {
308        synchronized (mInstanceLock) {
309            if (interactionId > mInteractionId) {
310                mFindAccessibilityNodeInfosResult = infos;
311                mInteractionId = interactionId;
312            }
313            mInstanceLock.notifyAll();
314        }
315    }
316
317    /**
318     * Gets the result of a request to perform an accessibility action.
319     *
320     * @param interactionId The interaction id to match the result with the request.
321     * @return Whether the action was performed.
322     */
323    private boolean getPerformAccessibilityActionResult(int interactionId) {
324        synchronized (mInstanceLock) {
325            final boolean success = waitForResultTimedLocked(interactionId);
326            final boolean result = success ? mPerformAccessibilityActionResult : false;
327            clearResultLocked();
328            return result;
329        }
330    }
331
332    /**
333     * {@inheritDoc}
334     */
335    public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) {
336        synchronized (mInstanceLock) {
337            if (interactionId > mInteractionId) {
338                mPerformAccessibilityActionResult = succeeded;
339                mInteractionId = interactionId;
340            }
341            mInstanceLock.notifyAll();
342        }
343    }
344
345    /**
346     * Clears the result state.
347     */
348    private void clearResultLocked() {
349        mInteractionId = -1;
350        mFindAccessibilityNodeInfoResult = null;
351        mFindAccessibilityNodeInfosResult = null;
352        mPerformAccessibilityActionResult = false;
353    }
354
355    /**
356     * Waits up to a given bound for a result of a request and returns it.
357     *
358     * @param interactionId The interaction id to match the result with the request.
359     * @return Whether the result was received.
360     */
361    private boolean waitForResultTimedLocked(int interactionId) {
362        long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS;
363        final long startTimeMillis = SystemClock.uptimeMillis();
364        while (true) {
365            try {
366                if (mInteractionId == interactionId) {
367                    return true;
368                }
369                if (mInteractionId > interactionId) {
370                    return false;
371                }
372                final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
373                waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
374                if (waitTimeMillis <= 0) {
375                    return false;
376                }
377                mInstanceLock.wait(waitTimeMillis);
378            } catch (InterruptedException ie) {
379                /* ignore */
380            }
381        }
382    }
383
384    /**
385     * Applies compatibility scale to the info bounds if it is not equal to one.
386     *
387     * @param info The info whose bounds to scale.
388     * @param scale The scale to apply.
389     */
390    private void applyCompatibilityScaleIfNeeded(AccessibilityNodeInfo info, float scale) {
391        if (scale == 1.0f) {
392            return;
393        }
394        Rect bounds = mTempBounds;
395        info.getBoundsInParent(bounds);
396        bounds.scale(scale);
397        info.setBoundsInParent(bounds);
398
399        info.getBoundsInScreen(bounds);
400        bounds.scale(scale);
401        info.setBoundsInScreen(bounds);
402    }
403
404    /**
405     * Handles the message stored if the interacted and interacting
406     * threads are the same otherwise this is a NOP.
407     */
408    private void handleSameThreadMessageIfNeeded() {
409        Message sameProcessMessage = getSameProcessMessageAndClear();
410        if (sameProcessMessage != null) {
411            sameProcessMessage.getTarget().handleMessage(sameProcessMessage);
412        }
413    }
414
415    /**
416     * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
417     *
418     * @param info The info.
419     * @param connection The current connection to the system.
420     * @param windowScale The source window compatibility scale.
421     */
422    private void finalizeAccessibilityNodeInfo(AccessibilityNodeInfo info,
423            IAccessibilityServiceConnection connection, float windowScale) {
424        if (info != null) {
425            applyCompatibilityScaleIfNeeded(info, windowScale);
426            info.setConnection(connection);
427            info.setSealed(true);
428        }
429    }
430
431    /**
432     * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
433     *
434     * @param infos The {@link AccessibilityNodeInfo}s.
435     * @param connection The current connection to the system.
436     * @param windowScale The source window compatibility scale.
437     */
438    private void finalizeAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
439            IAccessibilityServiceConnection connection, float windowScale) {
440        if (infos != null) {
441            final int infosCount = infos.size();
442            for (int i = 0; i < infosCount; i++) {
443                AccessibilityNodeInfo info = infos.get(i);
444                finalizeAccessibilityNodeInfo(info, connection, windowScale);
445            }
446        }
447    }
448
449    /**
450     * Gets the message stored if the interacted and interacting
451     * threads are the same.
452     *
453     * @return The message.
454     */
455    private Message getSameProcessMessageAndClear() {
456        synchronized (mInstanceLock) {
457            Message result = mSameThreadMessage;
458            mSameThreadMessage = null;
459            return result;
460        }
461    }
462}
463