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    // The connection cache is shared between all interrogating threads.
95    private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
96        new SparseArray<IAccessibilityServiceConnection>();
97
98    /**
99     * @return The singleton of this class.
100     */
101    public static AccessibilityInteractionClient getInstance() {
102        synchronized (sStaticLock) {
103            if (sInstance == null) {
104                sInstance = new AccessibilityInteractionClient();
105            }
106            return sInstance;
107        }
108    }
109
110    /**
111     * Sets the message to be processed if the interacted view hierarchy
112     * and the interacting client are running in the same thread.
113     *
114     * @param message The message.
115     */
116    public void setSameThreadMessage(Message message) {
117        synchronized (mInstanceLock) {
118            mSameThreadMessage = message;
119            mInstanceLock.notifyAll();
120        }
121    }
122
123    /**
124     * Finds an {@link AccessibilityNodeInfo} by accessibility id.
125     *
126     * @param connectionId The id of a connection for interacting with the system.
127     * @param accessibilityWindowId A unique window id.
128     * @param accessibilityViewId A unique View accessibility id.
129     * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
130     */
131    public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
132            int accessibilityWindowId, int accessibilityViewId) {
133        try {
134            IAccessibilityServiceConnection connection = getConnection(connectionId);
135            if (connection != null) {
136                final int interactionId = mInteractionIdCounter.getAndIncrement();
137                final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId(
138                        accessibilityWindowId, accessibilityViewId, interactionId, this,
139                        Thread.currentThread().getId());
140                // If the scale is zero the call has failed.
141                if (windowScale > 0) {
142                    AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
143                            interactionId);
144                    finalizeAccessibilityNodeInfo(info, connectionId, windowScale);
145                    return info;
146                }
147            } else {
148                if (DEBUG) {
149                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
150                }
151            }
152        } catch (RemoteException re) {
153            if (DEBUG) {
154                Log.w(LOG_TAG, "Error while calling remote"
155                        + " findAccessibilityNodeInfoByAccessibilityId", re);
156            }
157        }
158        return null;
159    }
160
161    /**
162     * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed
163     * in the currently active window and starts from the root View in the window.
164     *
165     * @param connectionId The id of a connection for interacting with the system.
166     * @param viewId The id of the view.
167     * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
168     */
169    public AccessibilityNodeInfo findAccessibilityNodeInfoByViewIdInActiveWindow(int connectionId,
170            int viewId) {
171        try {
172            IAccessibilityServiceConnection connection = getConnection(connectionId);
173            if (connection != null) {
174                final int interactionId = mInteractionIdCounter.getAndIncrement();
175                final float windowScale =
176                    connection.findAccessibilityNodeInfoByViewIdInActiveWindow(viewId,
177                            interactionId, this, Thread.currentThread().getId());
178                // If the scale is zero the call has failed.
179                if (windowScale > 0) {
180                    AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
181                            interactionId);
182                    finalizeAccessibilityNodeInfo(info, connectionId, windowScale);
183                    return info;
184                }
185            } else {
186                if (DEBUG) {
187                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
188                }
189            }
190        } catch (RemoteException re) {
191            if (DEBUG) {
192                Log.w(LOG_TAG, "Error while calling remote"
193                        + " findAccessibilityNodeInfoByViewIdInActiveWindow", re);
194            }
195        }
196        return null;
197    }
198
199    /**
200     * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
201     * insensitive containment. The search is performed in the currently
202     * active window and starts from the root View in the window.
203     *
204     * @param connectionId The id of a connection for interacting with the system.
205     * @param text The searched text.
206     * @return A list of found {@link AccessibilityNodeInfo}s.
207     */
208    public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewTextInActiveWindow(
209            int connectionId, String text) {
210        try {
211            IAccessibilityServiceConnection connection = getConnection(connectionId);
212            if (connection != null) {
213                final int interactionId = mInteractionIdCounter.getAndIncrement();
214                final float windowScale =
215                    connection.findAccessibilityNodeInfosByViewTextInActiveWindow(text,
216                            interactionId, this, Thread.currentThread().getId());
217                // If the scale is zero the call has failed.
218                if (windowScale > 0) {
219                    List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
220                            interactionId);
221                    finalizeAccessibilityNodeInfos(infos, connectionId, windowScale);
222                    return infos;
223                }
224            } else {
225                if (DEBUG) {
226                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
227                }
228            }
229        } catch (RemoteException re) {
230            if (DEBUG) {
231                Log.w(LOG_TAG, "Error while calling remote"
232                        + " findAccessibilityNodeInfosByViewTextInActiveWindow", re);
233            }
234        }
235        return null;
236    }
237
238    /**
239     * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
240     * insensitive containment. The search is performed in the window whose
241     * id is specified and starts from the View whose accessibility id is
242     * specified.
243     *
244     * @param connectionId The id of a connection for interacting with the system.
245     * @param text The searched text.
246     * @param accessibilityWindowId A unique window id.
247     * @param accessibilityViewId A unique View accessibility id from where to start the search.
248     *        Use {@link android.view.View#NO_ID} to start from the root.
249     * @return A list of found {@link AccessibilityNodeInfo}s.
250     */
251    public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewText(int connectionId,
252            String text, int accessibilityWindowId, int accessibilityViewId) {
253        try {
254            IAccessibilityServiceConnection connection = getConnection(connectionId);
255            if (connection != null) {
256                final int interactionId = mInteractionIdCounter.getAndIncrement();
257                final float windowScale = connection.findAccessibilityNodeInfosByViewText(text,
258                        accessibilityWindowId, accessibilityViewId, interactionId, this,
259                        Thread.currentThread().getId());
260                // If the scale is zero the call has failed.
261                if (windowScale > 0) {
262                    List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
263                            interactionId);
264                    finalizeAccessibilityNodeInfos(infos, connectionId, windowScale);
265                    return infos;
266                }
267            } else {
268                if (DEBUG) {
269                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
270                }
271            }
272        } catch (RemoteException re) {
273            if (DEBUG) {
274                Log.w(LOG_TAG, "Error while calling remote"
275                        + " findAccessibilityNodeInfosByViewText", re);
276            }
277        }
278        return Collections.emptyList();
279    }
280
281    /**
282     * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
283     *
284     * @param connectionId The id of a connection for interacting with the system.
285     * @param accessibilityWindowId The id of the window.
286     * @param accessibilityViewId A unique View accessibility id.
287     * @param action The action to perform.
288     * @return Whether the action was performed.
289     */
290    public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
291            int accessibilityViewId, int action) {
292        try {
293            IAccessibilityServiceConnection connection = getConnection(connectionId);
294            if (connection != null) {
295                final int interactionId = mInteractionIdCounter.getAndIncrement();
296                final boolean success = connection.performAccessibilityAction(
297                        accessibilityWindowId, accessibilityViewId, action, interactionId, this,
298                        Thread.currentThread().getId());
299                if (success) {
300                    return getPerformAccessibilityActionResult(interactionId);
301                }
302            } else {
303                if (DEBUG) {
304                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
305                }
306            }
307        } catch (RemoteException re) {
308            if (DEBUG) {
309                Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re);
310            }
311        }
312        return false;
313    }
314
315    /**
316     * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
317     *
318     * @param interactionId The interaction id to match the result with the request.
319     * @return The result {@link AccessibilityNodeInfo}.
320     */
321    private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) {
322        synchronized (mInstanceLock) {
323            final boolean success = waitForResultTimedLocked(interactionId);
324            AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null;
325            clearResultLocked();
326            return result;
327        }
328    }
329
330    /**
331     * {@inheritDoc}
332     */
333    public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info,
334                int interactionId) {
335        synchronized (mInstanceLock) {
336            if (interactionId > mInteractionId) {
337                mFindAccessibilityNodeInfoResult = info;
338                mInteractionId = interactionId;
339            }
340            mInstanceLock.notifyAll();
341        }
342    }
343
344    /**
345     * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s.
346     *
347     * @param interactionId The interaction id to match the result with the request.
348     * @return The result {@link AccessibilityNodeInfo}s.
349     */
350    private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear(
351                int interactionId) {
352        synchronized (mInstanceLock) {
353            final boolean success = waitForResultTimedLocked(interactionId);
354            List<AccessibilityNodeInfo> result = success ? mFindAccessibilityNodeInfosResult : null;
355            clearResultLocked();
356            return result;
357        }
358    }
359
360    /**
361     * {@inheritDoc}
362     */
363    public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
364                int interactionId) {
365        synchronized (mInstanceLock) {
366            if (interactionId > mInteractionId) {
367                mFindAccessibilityNodeInfosResult = infos;
368                mInteractionId = interactionId;
369            }
370            mInstanceLock.notifyAll();
371        }
372    }
373
374    /**
375     * Gets the result of a request to perform an accessibility action.
376     *
377     * @param interactionId The interaction id to match the result with the request.
378     * @return Whether the action was performed.
379     */
380    private boolean getPerformAccessibilityActionResult(int interactionId) {
381        synchronized (mInstanceLock) {
382            final boolean success = waitForResultTimedLocked(interactionId);
383            final boolean result = success ? mPerformAccessibilityActionResult : false;
384            clearResultLocked();
385            return result;
386        }
387    }
388
389    /**
390     * {@inheritDoc}
391     */
392    public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) {
393        synchronized (mInstanceLock) {
394            if (interactionId > mInteractionId) {
395                mPerformAccessibilityActionResult = succeeded;
396                mInteractionId = interactionId;
397            }
398            mInstanceLock.notifyAll();
399        }
400    }
401
402    /**
403     * Clears the result state.
404     */
405    private void clearResultLocked() {
406        mInteractionId = -1;
407        mFindAccessibilityNodeInfoResult = null;
408        mFindAccessibilityNodeInfosResult = null;
409        mPerformAccessibilityActionResult = false;
410    }
411
412    /**
413     * Waits up to a given bound for a result of a request and returns it.
414     *
415     * @param interactionId The interaction id to match the result with the request.
416     * @return Whether the result was received.
417     */
418    private boolean waitForResultTimedLocked(int interactionId) {
419        long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS;
420        final long startTimeMillis = SystemClock.uptimeMillis();
421        while (true) {
422            try {
423                Message sameProcessMessage = getSameProcessMessageAndClear();
424                if (sameProcessMessage != null) {
425                    sameProcessMessage.getTarget().handleMessage(sameProcessMessage);
426                }
427
428                if (mInteractionId == interactionId) {
429                    return true;
430                }
431                if (mInteractionId > interactionId) {
432                    return false;
433                }
434                final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
435                waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
436                if (waitTimeMillis <= 0) {
437                    return false;
438                }
439                mInstanceLock.wait(waitTimeMillis);
440            } catch (InterruptedException ie) {
441                /* ignore */
442            }
443        }
444    }
445
446    /**
447     * Applies compatibility scale to the info bounds if it is not equal to one.
448     *
449     * @param info The info whose bounds to scale.
450     * @param scale The scale to apply.
451     */
452    private void applyCompatibilityScaleIfNeeded(AccessibilityNodeInfo info, float scale) {
453        if (scale == 1.0f) {
454            return;
455        }
456        Rect bounds = mTempBounds;
457        info.getBoundsInParent(bounds);
458        bounds.scale(scale);
459        info.setBoundsInParent(bounds);
460
461        info.getBoundsInScreen(bounds);
462        bounds.scale(scale);
463        info.setBoundsInScreen(bounds);
464    }
465
466    /**
467     * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
468     *
469     * @param info The info.
470     * @param connectionId The id of the connection to the system.
471     * @param windowScale The source window compatibility scale.
472     */
473    private void finalizeAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId,
474            float windowScale) {
475        if (info != null) {
476            applyCompatibilityScaleIfNeeded(info, windowScale);
477            info.setConnectionId(connectionId);
478            info.setSealed(true);
479        }
480    }
481
482    /**
483     * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
484     *
485     * @param infos The {@link AccessibilityNodeInfo}s.
486     * @param connectionId The id of the connection to the system.
487     * @param windowScale The source window compatibility scale.
488     */
489    private void finalizeAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
490            int connectionId, float windowScale) {
491        if (infos != null) {
492            final int infosCount = infos.size();
493            for (int i = 0; i < infosCount; i++) {
494                AccessibilityNodeInfo info = infos.get(i);
495                finalizeAccessibilityNodeInfo(info, connectionId, windowScale);
496            }
497        }
498    }
499
500    /**
501     * Gets the message stored if the interacted and interacting
502     * threads are the same.
503     *
504     * @return The message.
505     */
506    private Message getSameProcessMessageAndClear() {
507        synchronized (mInstanceLock) {
508            Message result = mSameThreadMessage;
509            mSameThreadMessage = null;
510            return result;
511        }
512    }
513
514    /**
515     * Gets a cached accessibility service connection.
516     *
517     * @param connectionId The connection id.
518     * @return The cached connection if such.
519     */
520    public IAccessibilityServiceConnection getConnection(int connectionId) {
521        synchronized (sConnectionCache) {
522            return sConnectionCache.get(connectionId);
523        }
524    }
525
526    /**
527     * Adds a cached accessibility service connection.
528     *
529     * @param connectionId The connection id.
530     * @param connection The connection.
531     */
532    public void addConnection(int connectionId, IAccessibilityServiceConnection connection) {
533        synchronized (sConnectionCache) {
534            sConnectionCache.put(connectionId, connection);
535        }
536    }
537
538    /**
539     * Removes a cached accessibility service connection.
540     *
541     * @param connectionId The connection id.
542     */
543    public void removeConnection(int connectionId) {
544        synchronized (sConnectionCache) {
545            sConnectionCache.remove(connectionId);
546        }
547    }
548}
549