blob: 139df3e57af01bb1e44324c5f87dd58736c9dc4b [file] [log] [blame]
Svetoslav Ganov8bd69612011-08-23 13:40:30 -07001/*
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
Svetoslav Ganov8b5a8142011-11-30 19:10:26 -080019import android.accessibilityservice.IAccessibilityServiceConnection;
Svetoslav Ganov42138042012-03-20 11:51:39 -070020import android.os.Binder;
Svetoslav Ganov4528b4e2012-05-15 18:24:10 -070021import android.os.Build;
Svetoslav Ganovaa780c12012-04-19 23:01:39 -070022import android.os.Bundle;
Svetoslav Ganov8bd69612011-08-23 13:40:30 -070023import android.os.Message;
Svetoslav Ganov42138042012-03-20 11:51:39 -070024import android.os.Process;
Svetoslav Ganov8bd69612011-08-23 13:40:30 -070025import android.os.RemoteException;
26import android.os.SystemClock;
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -080027import android.util.Log;
Svetoslav Ganov8b5a8142011-11-30 19:10:26 -080028import android.util.LongSparseArray;
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -080029import android.util.SparseArray;
Svetoslav Ganov4528b4e2012-05-15 18:24:10 -070030import android.util.SparseLongArray;
Svetoslav Ganov8bd69612011-08-23 13:40:30 -070031
Svetoslav Ganov79311c42012-01-17 20:24:26 -080032import java.util.ArrayList;
Svetoslav Ganov8bd69612011-08-23 13:40:30 -070033import java.util.Collections;
Svetoslav Ganov4528b4e2012-05-15 18:24:10 -070034import java.util.HashSet;
35import java.util.LinkedList;
Svetoslav Ganov8bd69612011-08-23 13:40:30 -070036import java.util.List;
Svetoslav Ganov4528b4e2012-05-15 18:24:10 -070037import java.util.Queue;
Svetoslav Ganov8bd69612011-08-23 13:40:30 -070038import java.util.concurrent.atomic.AtomicInteger;
39
40/**
41 * This class is a singleton that performs accessibility interaction
42 * which is it queries remote view hierarchies about snapshots of their
43 * views as well requests from these hierarchies to perform certain
44 * actions on their views.
45 *
46 * Rationale: The content retrieval APIs are synchronous from a client's
47 * perspective but internally they are asynchronous. The client thread
48 * calls into the system requesting an action and providing a callback
49 * to receive the result after which it waits up to a timeout for that
50 * result. The system enforces security and the delegates the request
51 * to a given view hierarchy where a message is posted (from a binder
52 * thread) describing what to be performed by the main UI thread the
53 * result of which it delivered via the mentioned callback. However,
54 * the blocked client thread and the main UI thread of the target view
55 * hierarchy can be the same thread, for example an accessibility service
56 * and an activity run in the same process, thus they are executed on the
57 * same main thread. In such a case the retrieval will fail since the UI
58 * thread that has to process the message describing the work to be done
59 * is blocked waiting for a result is has to compute! To avoid this scenario
60 * when making a call the client also passes its process and thread ids so
61 * the accessed view hierarchy can detect if the client making the request
62 * is running in its main UI thread. In such a case the view hierarchy,
63 * specifically the binder thread performing the IPC to it, does not post a
64 * message to be run on the UI thread but passes it to the singleton
65 * interaction client through which all interactions occur and the latter is
66 * responsible to execute the message before starting to wait for the
67 * asynchronous result delivered via the callback. In this case the expected
68 * result is already received so no waiting is performed.
69 *
70 * @hide
71 */
72public final class AccessibilityInteractionClient
73 extends IAccessibilityInteractionConnectionCallback.Stub {
74
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -080075 public static final int NO_ID = -1;
76
77 private static final String LOG_TAG = "AccessibilityInteractionClient";
78
79 private static final boolean DEBUG = false;
80
Svetoslav Ganov4528b4e2012-05-15 18:24:10 -070081 private static final boolean CHECK_INTEGRITY = true;
82
Svetoslav Ganov8bd69612011-08-23 13:40:30 -070083 private static final long TIMEOUT_INTERACTION_MILLIS = 5000;
84
85 private static final Object sStaticLock = new Object();
86
Svetoslav Ganov02107852011-10-03 17:06:56 -070087 private static final LongSparseArray<AccessibilityInteractionClient> sClients =
88 new LongSparseArray<AccessibilityInteractionClient>();
Svetoslav Ganov8bd69612011-08-23 13:40:30 -070089
90 private final AtomicInteger mInteractionIdCounter = new AtomicInteger();
91
92 private final Object mInstanceLock = new Object();
93
Svetoslav Ganovfefd20e2012-04-19 21:44:35 -070094 private volatile int mInteractionId = -1;
Svetoslav Ganov8bd69612011-08-23 13:40:30 -070095
96 private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult;
97
98 private List<AccessibilityNodeInfo> mFindAccessibilityNodeInfosResult;
99
100 private boolean mPerformAccessibilityActionResult;
101
102 private Message mSameThreadMessage;
103
Svetoslav Ganov36bcdb52011-12-01 11:48:21 -0800104 // The connection cache is shared between all interrogating threads.
105 private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800106 new SparseArray<IAccessibilityServiceConnection>();
107
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800108 // The connection cache is shared between all interrogating threads since
109 // at any given time there is only one window allowing querying.
110 private static final AccessibilityNodeInfoCache sAccessibilityNodeInfoCache =
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800111 new AccessibilityNodeInfoCache();
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800112
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700113 /**
Svetoslav Ganov02107852011-10-03 17:06:56 -0700114 * @return The client for the current thread.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700115 */
116 public static AccessibilityInteractionClient getInstance() {
Svetoslav Ganov02107852011-10-03 17:06:56 -0700117 final long threadId = Thread.currentThread().getId();
118 return getInstanceForThread(threadId);
119 }
120
121 /**
122 * <strong>Note:</strong> We keep one instance per interrogating thread since
123 * the instance contains state which can lead to undesired thread interleavings.
124 * We do not have a thread local variable since other threads should be able to
125 * look up the correct client knowing a thread id. See ViewRootImpl for details.
126 *
127 * @return The client for a given <code>threadId</code>.
128 */
129 public static AccessibilityInteractionClient getInstanceForThread(long threadId) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700130 synchronized (sStaticLock) {
Svetoslav Ganov02107852011-10-03 17:06:56 -0700131 AccessibilityInteractionClient client = sClients.get(threadId);
132 if (client == null) {
133 client = new AccessibilityInteractionClient();
134 sClients.put(threadId, client);
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700135 }
Svetoslav Ganov02107852011-10-03 17:06:56 -0700136 return client;
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700137 }
138 }
139
Svetoslav Ganov02107852011-10-03 17:06:56 -0700140 private AccessibilityInteractionClient() {
141 /* reducing constructor visibility */
142 }
143
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700144 /**
145 * Sets the message to be processed if the interacted view hierarchy
146 * and the interacting client are running in the same thread.
147 *
148 * @param message The message.
149 */
150 public void setSameThreadMessage(Message message) {
151 synchronized (mInstanceLock) {
152 mSameThreadMessage = message;
Svetoslav Ganov6bc5e532011-09-09 18:43:15 -0700153 mInstanceLock.notifyAll();
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700154 }
155 }
156
157 /**
Svetoslav Ganovfefd20e2012-04-19 21:44:35 -0700158 * Gets the root {@link AccessibilityNodeInfo} in the currently active window.
159 *
160 * @param connectionId The id of a connection for interacting with the system.
161 * @return The root {@link AccessibilityNodeInfo} if found, null otherwise.
162 */
163 public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) {
164 return findAccessibilityNodeInfoByAccessibilityId(connectionId,
165 AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,
Svetoslav6254f482013-06-04 17:22:14 -0700166 false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS);
Svetoslav Ganovfefd20e2012-04-19 21:44:35 -0700167 }
168
169 /**
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700170 * Finds an {@link AccessibilityNodeInfo} by accessibility id.
171 *
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800172 * @param connectionId The id of a connection for interacting with the system.
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800173 * @param accessibilityWindowId A unique window id. Use
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800174 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800175 * to query the currently active window.
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800176 * @param accessibilityNodeId A unique view id or virtual descendant id from
177 * where to start the search. Use
178 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
179 * to start from the root.
Svetoslav6254f482013-06-04 17:22:14 -0700180 * @param bypassCache Whether to bypass the cache while looking for the node.
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800181 * @param prefetchFlags flags to guide prefetching.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700182 * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
183 */
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800184 public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
Svetoslav6254f482013-06-04 17:22:14 -0700185 int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
186 int prefetchFlags) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700187 try {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800188 IAccessibilityServiceConnection connection = getConnection(connectionId);
189 if (connection != null) {
Svetoslav6254f482013-06-04 17:22:14 -0700190 if (!bypassCache) {
191 AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
192 accessibilityNodeId);
193 if (cachedInfo != null) {
194 return cachedInfo;
195 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800196 }
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800197 final int interactionId = mInteractionIdCounter.getAndIncrement();
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700198 final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
Svetoslav Ganovf3b4f312011-11-30 18:15:01 -0800199 accessibilityWindowId, accessibilityNodeId, interactionId, this,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700200 prefetchFlags, Thread.currentThread().getId());
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800201 // If the scale is zero the call has failed.
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700202 if (success) {
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800203 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800204 interactionId);
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700205 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800206 if (infos != null && !infos.isEmpty()) {
207 return infos.get(0);
208 }
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800209 }
210 } else {
211 if (DEBUG) {
212 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
213 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700214 }
215 } catch (RemoteException re) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800216 if (DEBUG) {
217 Log.w(LOG_TAG, "Error while calling remote"
218 + " findAccessibilityNodeInfoByAccessibilityId", re);
219 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700220 }
221 return null;
222 }
223
224 /**
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800225 * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in
226 * the window whose id is specified and starts from the node whose accessibility
227 * id is specified.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700228 *
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800229 * @param connectionId The id of a connection for interacting with the system.
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800230 * @param accessibilityWindowId A unique window id. Use
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800231 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800232 * to query the currently active window.
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800233 * @param accessibilityNodeId A unique view id or virtual descendant id from
234 * where to start the search. Use
235 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800236 * to start from the root.
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800237 * @param viewId The fully qualified resource name of the view id to find.
238 * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700239 */
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800240 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId,
241 int accessibilityWindowId, long accessibilityNodeId, String viewId) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700242 try {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800243 IAccessibilityServiceConnection connection = getConnection(connectionId);
244 if (connection != null) {
245 final int interactionId = mInteractionIdCounter.getAndIncrement();
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800246 final boolean success = connection.findAccessibilityNodeInfosByViewId(
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700247 accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this,
248 Thread.currentThread().getId());
249 if (success) {
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800250 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800251 interactionId);
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800252 if (infos != null) {
253 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
254 return infos;
255 }
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800256 }
257 } else {
258 if (DEBUG) {
259 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
260 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700261 }
262 } catch (RemoteException re) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800263 if (DEBUG) {
264 Log.w(LOG_TAG, "Error while calling remote"
265 + " findAccessibilityNodeInfoByViewIdInActiveWindow", re);
266 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700267 }
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800268 return Collections.emptyList();
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700269 }
270
271 /**
272 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700273 * insensitive containment. The search is performed in the window whose
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800274 * id is specified and starts from the node whose accessibility id is
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700275 * specified.
276 *
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800277 * @param connectionId The id of a connection for interacting with the system.
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800278 * @param accessibilityWindowId A unique window id. Use
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800279 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800280 * to query the currently active window.
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800281 * @param accessibilityNodeId A unique view id or virtual descendant id from
282 * where to start the search. Use
283 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
284 * to start from the root.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700285 * @param text The searched text.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700286 * @return A list of found {@link AccessibilityNodeInfo}s.
287 */
Svetoslav Ganov66922db2011-11-30 19:18:51 -0800288 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId,
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800289 int accessibilityWindowId, long accessibilityNodeId, String text) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700290 try {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800291 IAccessibilityServiceConnection connection = getConnection(connectionId);
292 if (connection != null) {
293 final int interactionId = mInteractionIdCounter.getAndIncrement();
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700294 final boolean success = connection.findAccessibilityNodeInfosByText(
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800295 accessibilityWindowId, accessibilityNodeId, text, interactionId, this,
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800296 Thread.currentThread().getId());
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700297 if (success) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800298 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
299 interactionId);
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800300 if (infos != null) {
301 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
302 return infos;
303 }
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800304 }
305 } else {
306 if (DEBUG) {
307 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
308 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700309 }
310 } catch (RemoteException re) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800311 if (DEBUG) {
312 Log.w(LOG_TAG, "Error while calling remote"
313 + " findAccessibilityNodeInfosByViewText", re);
314 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700315 }
316 return Collections.emptyList();
317 }
318
319 /**
Svetoslav Ganov42138042012-03-20 11:51:39 -0700320 * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the
321 * specified focus type. The search is performed in the window whose id is specified
322 * and starts from the node whose accessibility id is specified.
323 *
324 * @param connectionId The id of a connection for interacting with the system.
325 * @param accessibilityWindowId A unique window id. Use
326 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
327 * to query the currently active window.
328 * @param accessibilityNodeId A unique view id or virtual descendant id from
329 * where to start the search. Use
330 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
331 * to start from the root.
332 * @param focusType The focus type.
333 * @return The accessibility focused {@link AccessibilityNodeInfo}.
334 */
335 public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId,
336 long accessibilityNodeId, int focusType) {
337 try {
338 IAccessibilityServiceConnection connection = getConnection(connectionId);
339 if (connection != null) {
340 final int interactionId = mInteractionIdCounter.getAndIncrement();
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700341 final boolean success = connection.findFocus(accessibilityWindowId,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700342 accessibilityNodeId, focusType, interactionId, this,
343 Thread.currentThread().getId());
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700344 if (success) {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700345 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
346 interactionId);
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700347 finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700348 return info;
349 }
350 } else {
351 if (DEBUG) {
352 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
353 }
354 }
355 } catch (RemoteException re) {
356 if (DEBUG) {
Svetoslav6254f482013-06-04 17:22:14 -0700357 Log.w(LOG_TAG, "Error while calling remote findFocus", re);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700358 }
359 }
360 return null;
361 }
362
363 /**
364 * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}.
365 * The search is performed in the window whose id is specified and starts from the
366 * node whose accessibility id is specified.
367 *
368 * @param connectionId The id of a connection for interacting with the system.
369 * @param accessibilityWindowId A unique window id. Use
370 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
371 * to query the currently active window.
372 * @param accessibilityNodeId A unique view id or virtual descendant id from
373 * where to start the search. Use
374 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
375 * to start from the root.
376 * @param direction The direction in which to search for focusable.
377 * @return The accessibility focused {@link AccessibilityNodeInfo}.
378 */
379 public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId,
380 long accessibilityNodeId, int direction) {
381 try {
382 IAccessibilityServiceConnection connection = getConnection(connectionId);
383 if (connection != null) {
384 final int interactionId = mInteractionIdCounter.getAndIncrement();
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700385 final boolean success = connection.focusSearch(accessibilityWindowId,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700386 accessibilityNodeId, direction, interactionId, this,
387 Thread.currentThread().getId());
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700388 if (success) {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700389 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
390 interactionId);
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700391 finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700392 return info;
393 }
394 } else {
395 if (DEBUG) {
396 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
397 }
398 }
399 } catch (RemoteException re) {
400 if (DEBUG) {
401 Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re);
402 }
403 }
404 return null;
405 }
406
407 /**
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700408 * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
409 *
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800410 * @param connectionId The id of a connection for interacting with the system.
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800411 * @param accessibilityWindowId A unique window id. Use
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800412 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800413 * to query the currently active window.
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800414 * @param accessibilityNodeId A unique view id or virtual descendant id from
415 * where to start the search. Use
416 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
417 * to start from the root.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700418 * @param action The action to perform.
Svetoslav Ganovaa780c12012-04-19 23:01:39 -0700419 * @param arguments Optional action arguments.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700420 * @return Whether the action was performed.
421 */
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800422 public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
Svetoslav Ganovaa780c12012-04-19 23:01:39 -0700423 long accessibilityNodeId, int action, Bundle arguments) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700424 try {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800425 IAccessibilityServiceConnection connection = getConnection(connectionId);
426 if (connection != null) {
427 final int interactionId = mInteractionIdCounter.getAndIncrement();
428 final boolean success = connection.performAccessibilityAction(
Svetoslav Ganovaa780c12012-04-19 23:01:39 -0700429 accessibilityWindowId, accessibilityNodeId, action, arguments,
430 interactionId, this, Thread.currentThread().getId());
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800431 if (success) {
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800432 return getPerformAccessibilityActionResultAndClear(interactionId);
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800433 }
434 } else {
435 if (DEBUG) {
436 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
437 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700438 }
439 } catch (RemoteException re) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800440 if (DEBUG) {
441 Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re);
442 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700443 }
444 return false;
445 }
446
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800447 public void clearCache() {
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800448 sAccessibilityNodeInfoCache.clear();
449 }
450
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800451 public void onAccessibilityEvent(AccessibilityEvent event) {
452 sAccessibilityNodeInfoCache.onAccessibilityEvent(event);
453 }
454
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700455 /**
456 * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
457 *
458 * @param interactionId The interaction id to match the result with the request.
459 * @return The result {@link AccessibilityNodeInfo}.
460 */
461 private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) {
462 synchronized (mInstanceLock) {
463 final boolean success = waitForResultTimedLocked(interactionId);
464 AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null;
465 clearResultLocked();
466 return result;
467 }
468 }
469
470 /**
471 * {@inheritDoc}
472 */
473 public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info,
474 int interactionId) {
475 synchronized (mInstanceLock) {
476 if (interactionId > mInteractionId) {
477 mFindAccessibilityNodeInfoResult = info;
478 mInteractionId = interactionId;
479 }
480 mInstanceLock.notifyAll();
481 }
482 }
483
484 /**
485 * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s.
486 *
487 * @param interactionId The interaction id to match the result with the request.
488 * @return The result {@link AccessibilityNodeInfo}s.
489 */
490 private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear(
491 int interactionId) {
492 synchronized (mInstanceLock) {
493 final boolean success = waitForResultTimedLocked(interactionId);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700494 List<AccessibilityNodeInfo> result = null;
495 if (success) {
496 result = mFindAccessibilityNodeInfosResult;
497 } else {
498 result = Collections.emptyList();
499 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700500 clearResultLocked();
Svetoslav Ganov4528b4e2012-05-15 18:24:10 -0700501 if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) {
502 checkFindAccessibilityNodeInfoResultIntegrity(result);
503 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700504 return result;
505 }
506 }
507
508 /**
509 * {@inheritDoc}
510 */
511 public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
512 int interactionId) {
513 synchronized (mInstanceLock) {
514 if (interactionId > mInteractionId) {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700515 if (infos != null) {
516 // If the call is not an IPC, i.e. it is made from the same process, we need to
517 // instantiate new result list to avoid passing internal instances to clients.
518 final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid());
519 if (!isIpcCall) {
520 mFindAccessibilityNodeInfosResult =
521 new ArrayList<AccessibilityNodeInfo>(infos);
522 } else {
523 mFindAccessibilityNodeInfosResult = infos;
524 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800525 } else {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700526 mFindAccessibilityNodeInfosResult = Collections.emptyList();
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800527 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700528 mInteractionId = interactionId;
529 }
530 mInstanceLock.notifyAll();
531 }
532 }
533
534 /**
535 * Gets the result of a request to perform an accessibility action.
536 *
537 * @param interactionId The interaction id to match the result with the request.
538 * @return Whether the action was performed.
539 */
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800540 private boolean getPerformAccessibilityActionResultAndClear(int interactionId) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700541 synchronized (mInstanceLock) {
542 final boolean success = waitForResultTimedLocked(interactionId);
543 final boolean result = success ? mPerformAccessibilityActionResult : false;
544 clearResultLocked();
545 return result;
546 }
547 }
548
549 /**
550 * {@inheritDoc}
551 */
552 public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) {
553 synchronized (mInstanceLock) {
554 if (interactionId > mInteractionId) {
555 mPerformAccessibilityActionResult = succeeded;
556 mInteractionId = interactionId;
557 }
558 mInstanceLock.notifyAll();
559 }
560 }
561
562 /**
563 * Clears the result state.
564 */
565 private void clearResultLocked() {
566 mInteractionId = -1;
567 mFindAccessibilityNodeInfoResult = null;
568 mFindAccessibilityNodeInfosResult = null;
569 mPerformAccessibilityActionResult = false;
570 }
571
572 /**
573 * Waits up to a given bound for a result of a request and returns it.
574 *
575 * @param interactionId The interaction id to match the result with the request.
576 * @return Whether the result was received.
577 */
578 private boolean waitForResultTimedLocked(int interactionId) {
579 long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS;
580 final long startTimeMillis = SystemClock.uptimeMillis();
581 while (true) {
582 try {
Svetoslav Ganov6bc5e532011-09-09 18:43:15 -0700583 Message sameProcessMessage = getSameProcessMessageAndClear();
584 if (sameProcessMessage != null) {
585 sameProcessMessage.getTarget().handleMessage(sameProcessMessage);
586 }
587
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700588 if (mInteractionId == interactionId) {
589 return true;
590 }
591 if (mInteractionId > interactionId) {
592 return false;
593 }
594 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
595 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
596 if (waitTimeMillis <= 0) {
597 return false;
598 }
599 mInstanceLock.wait(waitTimeMillis);
600 } catch (InterruptedException ie) {
601 /* ignore */
602 }
603 }
604 }
605
606 /**
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700607 * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
608 *
609 * @param info The info.
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800610 * @param connectionId The id of the connection to the system.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700611 */
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700612 private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info,
613 int connectionId) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700614 if (info != null) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800615 info.setConnectionId(connectionId);
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700616 info.setSealed(true);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700617 sAccessibilityNodeInfoCache.add(info);
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700618 }
619 }
620
621 /**
622 * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
623 *
624 * @param infos The {@link AccessibilityNodeInfo}s.
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800625 * @param connectionId The id of the connection to the system.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700626 */
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800627 private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700628 int connectionId) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700629 if (infos != null) {
630 final int infosCount = infos.size();
631 for (int i = 0; i < infosCount; i++) {
632 AccessibilityNodeInfo info = infos.get(i);
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700633 finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700634 }
635 }
636 }
637
638 /**
639 * Gets the message stored if the interacted and interacting
640 * threads are the same.
641 *
642 * @return The message.
643 */
644 private Message getSameProcessMessageAndClear() {
645 synchronized (mInstanceLock) {
646 Message result = mSameThreadMessage;
647 mSameThreadMessage = null;
648 return result;
649 }
650 }
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800651
652 /**
653 * Gets a cached accessibility service connection.
654 *
655 * @param connectionId The connection id.
656 * @return The cached connection if such.
657 */
658 public IAccessibilityServiceConnection getConnection(int connectionId) {
Svetoslav Ganov36bcdb52011-12-01 11:48:21 -0800659 synchronized (sConnectionCache) {
660 return sConnectionCache.get(connectionId);
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800661 }
662 }
663
664 /**
665 * Adds a cached accessibility service connection.
666 *
667 * @param connectionId The connection id.
668 * @param connection The connection.
669 */
670 public void addConnection(int connectionId, IAccessibilityServiceConnection connection) {
Svetoslav Ganov36bcdb52011-12-01 11:48:21 -0800671 synchronized (sConnectionCache) {
672 sConnectionCache.put(connectionId, connection);
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800673 }
674 }
675
676 /**
677 * Removes a cached accessibility service connection.
678 *
679 * @param connectionId The connection id.
680 */
681 public void removeConnection(int connectionId) {
Svetoslav Ganov36bcdb52011-12-01 11:48:21 -0800682 synchronized (sConnectionCache) {
683 sConnectionCache.remove(connectionId);
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800684 }
685 }
Svetoslav Ganov4528b4e2012-05-15 18:24:10 -0700686
687 /**
688 * Checks whether the infos are a fully connected tree with no duplicates.
689 *
690 * @param infos The result list to check.
691 */
692 private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) {
693 if (infos.size() == 0) {
694 return;
695 }
696 // Find the root node.
697 AccessibilityNodeInfo root = infos.get(0);
698 final int infoCount = infos.size();
699 for (int i = 1; i < infoCount; i++) {
700 for (int j = i; j < infoCount; j++) {
701 AccessibilityNodeInfo candidate = infos.get(j);
702 if (root.getParentNodeId() == candidate.getSourceNodeId()) {
703 root = candidate;
704 break;
705 }
706 }
707 }
708 if (root == null) {
709 Log.e(LOG_TAG, "No root.");
710 }
711 // Check for duplicates.
712 HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>();
713 Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>();
714 fringe.add(root);
715 while (!fringe.isEmpty()) {
716 AccessibilityNodeInfo current = fringe.poll();
717 if (!seen.add(current)) {
718 Log.e(LOG_TAG, "Duplicate node.");
719 return;
720 }
721 SparseLongArray childIds = current.getChildNodeIds();
722 final int childCount = childIds.size();
723 for (int i = 0; i < childCount; i++) {
724 final long childId = childIds.valueAt(i);
725 for (int j = 0; j < infoCount; j++) {
726 AccessibilityNodeInfo child = infos.get(j);
727 if (child.getSourceNodeId() == childId) {
728 fringe.add(child);
729 }
730 }
731 }
732 }
733 final int disconnectedCount = infos.size() - seen.size();
734 if (disconnectedCount > 0) {
735 Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes.");
736 }
737 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700738}