blob: 84d7e720b31f83ec1564e851d6abc47a1a919871 [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,
166 AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS);
167 }
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.
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800180 * @param prefetchFlags flags to guide prefetching.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700181 * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
182 */
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800183 public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
Svetoslav Ganov57c7fd52012-02-23 18:31:39 -0800184 int accessibilityWindowId, long accessibilityNodeId, int prefetchFlags) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700185 try {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800186 IAccessibilityServiceConnection connection = getConnection(connectionId);
187 if (connection != null) {
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800188 AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
189 accessibilityNodeId);
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800190 if (cachedInfo != null) {
191 return cachedInfo;
192 }
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800193 final int interactionId = mInteractionIdCounter.getAndIncrement();
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700194 final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
Svetoslav Ganovf3b4f312011-11-30 18:15:01 -0800195 accessibilityWindowId, accessibilityNodeId, interactionId, this,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700196 prefetchFlags, Thread.currentThread().getId());
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800197 // If the scale is zero the call has failed.
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700198 if (success) {
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800199 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800200 interactionId);
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700201 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800202 if (infos != null && !infos.isEmpty()) {
203 return infos.get(0);
204 }
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800205 }
206 } else {
207 if (DEBUG) {
208 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
209 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700210 }
211 } catch (RemoteException re) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800212 if (DEBUG) {
213 Log.w(LOG_TAG, "Error while calling remote"
214 + " findAccessibilityNodeInfoByAccessibilityId", re);
215 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700216 }
217 return null;
218 }
219
220 /**
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800221 * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in
222 * the window whose id is specified and starts from the node whose accessibility
223 * id is specified.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700224 *
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800225 * @param connectionId The id of a connection for interacting with the system.
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800226 * @param accessibilityWindowId A unique window id. Use
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800227 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800228 * to query the currently active window.
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800229 * @param accessibilityNodeId A unique view id or virtual descendant id from
230 * where to start the search. Use
231 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800232 * to start from the root.
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800233 * @param viewId The fully qualified resource name of the view id to find.
234 * @return An list of {@link AccessibilityNodeInfo} if found, empty list otherwise.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700235 */
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800236 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(int connectionId,
237 int accessibilityWindowId, long accessibilityNodeId, String viewId) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700238 try {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800239 IAccessibilityServiceConnection connection = getConnection(connectionId);
240 if (connection != null) {
241 final int interactionId = mInteractionIdCounter.getAndIncrement();
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800242 final boolean success = connection.findAccessibilityNodeInfosByViewId(
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700243 accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this,
244 Thread.currentThread().getId());
245 if (success) {
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800246 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800247 interactionId);
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800248 if (infos != null) {
249 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
250 return infos;
251 }
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800252 }
253 } else {
254 if (DEBUG) {
255 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
256 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700257 }
258 } catch (RemoteException re) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800259 if (DEBUG) {
260 Log.w(LOG_TAG, "Error while calling remote"
261 + " findAccessibilityNodeInfoByViewIdInActiveWindow", re);
262 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700263 }
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800264 return Collections.emptyList();
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700265 }
266
267 /**
268 * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700269 * insensitive containment. The search is performed in the window whose
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800270 * id is specified and starts from the node whose accessibility id is
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700271 * specified.
272 *
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800273 * @param connectionId The id of a connection for interacting with the system.
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800274 * @param accessibilityWindowId A unique window id. Use
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800275 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800276 * to query the currently active window.
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800277 * @param accessibilityNodeId A unique view id or virtual descendant id from
278 * where to start the search. Use
279 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
280 * to start from the root.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700281 * @param text The searched text.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700282 * @return A list of found {@link AccessibilityNodeInfo}s.
283 */
Svetoslav Ganov66922db2011-11-30 19:18:51 -0800284 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(int connectionId,
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800285 int accessibilityWindowId, long accessibilityNodeId, String text) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700286 try {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800287 IAccessibilityServiceConnection connection = getConnection(connectionId);
288 if (connection != null) {
289 final int interactionId = mInteractionIdCounter.getAndIncrement();
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700290 final boolean success = connection.findAccessibilityNodeInfosByText(
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800291 accessibilityWindowId, accessibilityNodeId, text, interactionId, this,
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800292 Thread.currentThread().getId());
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700293 if (success) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800294 List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
295 interactionId);
Svetoslav Ganov80943d82013-01-02 10:25:37 -0800296 if (infos != null) {
297 finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
298 return infos;
299 }
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800300 }
301 } else {
302 if (DEBUG) {
303 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
304 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700305 }
306 } catch (RemoteException re) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800307 if (DEBUG) {
308 Log.w(LOG_TAG, "Error while calling remote"
309 + " findAccessibilityNodeInfosByViewText", re);
310 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700311 }
312 return Collections.emptyList();
313 }
314
315 /**
Svetoslav Ganov42138042012-03-20 11:51:39 -0700316 * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the
317 * specified focus type. The search is performed in the window whose id is specified
318 * and starts from the node whose accessibility id is specified.
319 *
320 * @param connectionId The id of a connection for interacting with the system.
321 * @param accessibilityWindowId A unique window id. Use
322 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
323 * to query the currently active window.
324 * @param accessibilityNodeId A unique view id or virtual descendant id from
325 * where to start the search. Use
326 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
327 * to start from the root.
328 * @param focusType The focus type.
329 * @return The accessibility focused {@link AccessibilityNodeInfo}.
330 */
331 public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId,
332 long accessibilityNodeId, int focusType) {
333 try {
334 IAccessibilityServiceConnection connection = getConnection(connectionId);
335 if (connection != null) {
336 final int interactionId = mInteractionIdCounter.getAndIncrement();
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700337 final boolean success = connection.findFocus(accessibilityWindowId,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700338 accessibilityNodeId, focusType, interactionId, this,
339 Thread.currentThread().getId());
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700340 if (success) {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700341 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
342 interactionId);
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700343 finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700344 return info;
345 }
346 } else {
347 if (DEBUG) {
348 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
349 }
350 }
351 } catch (RemoteException re) {
352 if (DEBUG) {
353 Log.w(LOG_TAG, "Error while calling remote findAccessibilityFocus", re);
354 }
355 }
356 return null;
357 }
358
359 /**
360 * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}.
361 * The search is performed in the window whose id is specified and starts from the
362 * node whose accessibility id is specified.
363 *
364 * @param connectionId The id of a connection for interacting with the system.
365 * @param accessibilityWindowId A unique window id. Use
366 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
367 * to query the currently active window.
368 * @param accessibilityNodeId A unique view id or virtual descendant id from
369 * where to start the search. Use
370 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
371 * to start from the root.
372 * @param direction The direction in which to search for focusable.
373 * @return The accessibility focused {@link AccessibilityNodeInfo}.
374 */
375 public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId,
376 long accessibilityNodeId, int direction) {
377 try {
378 IAccessibilityServiceConnection connection = getConnection(connectionId);
379 if (connection != null) {
380 final int interactionId = mInteractionIdCounter.getAndIncrement();
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700381 final boolean success = connection.focusSearch(accessibilityWindowId,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700382 accessibilityNodeId, direction, interactionId, this,
383 Thread.currentThread().getId());
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700384 if (success) {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700385 AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
386 interactionId);
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700387 finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700388 return info;
389 }
390 } else {
391 if (DEBUG) {
392 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
393 }
394 }
395 } catch (RemoteException re) {
396 if (DEBUG) {
397 Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re);
398 }
399 }
400 return null;
401 }
402
403 /**
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700404 * Performs an accessibility action on an {@link AccessibilityNodeInfo}.
405 *
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800406 * @param connectionId The id of a connection for interacting with the system.
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800407 * @param accessibilityWindowId A unique window id. Use
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800408 * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800409 * to query the currently active window.
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800410 * @param accessibilityNodeId A unique view id or virtual descendant id from
411 * where to start the search. Use
412 * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
413 * to start from the root.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700414 * @param action The action to perform.
Svetoslav Ganovaa780c12012-04-19 23:01:39 -0700415 * @param arguments Optional action arguments.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700416 * @return Whether the action was performed.
417 */
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800418 public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
Svetoslav Ganovaa780c12012-04-19 23:01:39 -0700419 long accessibilityNodeId, int action, Bundle arguments) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700420 try {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800421 IAccessibilityServiceConnection connection = getConnection(connectionId);
422 if (connection != null) {
423 final int interactionId = mInteractionIdCounter.getAndIncrement();
424 final boolean success = connection.performAccessibilityAction(
Svetoslav Ganovaa780c12012-04-19 23:01:39 -0700425 accessibilityWindowId, accessibilityNodeId, action, arguments,
426 interactionId, this, Thread.currentThread().getId());
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800427 if (success) {
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800428 return getPerformAccessibilityActionResultAndClear(interactionId);
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800429 }
430 } else {
431 if (DEBUG) {
432 Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
433 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700434 }
435 } catch (RemoteException re) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800436 if (DEBUG) {
437 Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re);
438 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700439 }
440 return false;
441 }
442
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800443 public void clearCache() {
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800444 sAccessibilityNodeInfoCache.clear();
445 }
446
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800447 public void onAccessibilityEvent(AccessibilityEvent event) {
448 sAccessibilityNodeInfoCache.onAccessibilityEvent(event);
449 }
450
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700451 /**
452 * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}.
453 *
454 * @param interactionId The interaction id to match the result with the request.
455 * @return The result {@link AccessibilityNodeInfo}.
456 */
457 private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) {
458 synchronized (mInstanceLock) {
459 final boolean success = waitForResultTimedLocked(interactionId);
460 AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null;
461 clearResultLocked();
462 return result;
463 }
464 }
465
466 /**
467 * {@inheritDoc}
468 */
469 public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info,
470 int interactionId) {
471 synchronized (mInstanceLock) {
472 if (interactionId > mInteractionId) {
473 mFindAccessibilityNodeInfoResult = info;
474 mInteractionId = interactionId;
475 }
476 mInstanceLock.notifyAll();
477 }
478 }
479
480 /**
481 * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s.
482 *
483 * @param interactionId The interaction id to match the result with the request.
484 * @return The result {@link AccessibilityNodeInfo}s.
485 */
486 private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear(
487 int interactionId) {
488 synchronized (mInstanceLock) {
489 final boolean success = waitForResultTimedLocked(interactionId);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700490 List<AccessibilityNodeInfo> result = null;
491 if (success) {
492 result = mFindAccessibilityNodeInfosResult;
493 } else {
494 result = Collections.emptyList();
495 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700496 clearResultLocked();
Svetoslav Ganov4528b4e2012-05-15 18:24:10 -0700497 if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) {
498 checkFindAccessibilityNodeInfoResultIntegrity(result);
499 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700500 return result;
501 }
502 }
503
504 /**
505 * {@inheritDoc}
506 */
507 public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
508 int interactionId) {
509 synchronized (mInstanceLock) {
510 if (interactionId > mInteractionId) {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700511 if (infos != null) {
512 // If the call is not an IPC, i.e. it is made from the same process, we need to
513 // instantiate new result list to avoid passing internal instances to clients.
514 final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid());
515 if (!isIpcCall) {
516 mFindAccessibilityNodeInfosResult =
517 new ArrayList<AccessibilityNodeInfo>(infos);
518 } else {
519 mFindAccessibilityNodeInfosResult = infos;
520 }
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800521 } else {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700522 mFindAccessibilityNodeInfosResult = Collections.emptyList();
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800523 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700524 mInteractionId = interactionId;
525 }
526 mInstanceLock.notifyAll();
527 }
528 }
529
530 /**
531 * Gets the result of a request to perform an accessibility action.
532 *
533 * @param interactionId The interaction id to match the result with the request.
534 * @return Whether the action was performed.
535 */
Svetoslav Ganov79311c42012-01-17 20:24:26 -0800536 private boolean getPerformAccessibilityActionResultAndClear(int interactionId) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700537 synchronized (mInstanceLock) {
538 final boolean success = waitForResultTimedLocked(interactionId);
539 final boolean result = success ? mPerformAccessibilityActionResult : false;
540 clearResultLocked();
541 return result;
542 }
543 }
544
545 /**
546 * {@inheritDoc}
547 */
548 public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) {
549 synchronized (mInstanceLock) {
550 if (interactionId > mInteractionId) {
551 mPerformAccessibilityActionResult = succeeded;
552 mInteractionId = interactionId;
553 }
554 mInstanceLock.notifyAll();
555 }
556 }
557
558 /**
559 * Clears the result state.
560 */
561 private void clearResultLocked() {
562 mInteractionId = -1;
563 mFindAccessibilityNodeInfoResult = null;
564 mFindAccessibilityNodeInfosResult = null;
565 mPerformAccessibilityActionResult = false;
566 }
567
568 /**
569 * Waits up to a given bound for a result of a request and returns it.
570 *
571 * @param interactionId The interaction id to match the result with the request.
572 * @return Whether the result was received.
573 */
574 private boolean waitForResultTimedLocked(int interactionId) {
575 long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS;
576 final long startTimeMillis = SystemClock.uptimeMillis();
577 while (true) {
578 try {
Svetoslav Ganov6bc5e532011-09-09 18:43:15 -0700579 Message sameProcessMessage = getSameProcessMessageAndClear();
580 if (sameProcessMessage != null) {
581 sameProcessMessage.getTarget().handleMessage(sameProcessMessage);
582 }
583
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700584 if (mInteractionId == interactionId) {
585 return true;
586 }
587 if (mInteractionId > interactionId) {
588 return false;
589 }
590 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
591 waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis;
592 if (waitTimeMillis <= 0) {
593 return false;
594 }
595 mInstanceLock.wait(waitTimeMillis);
596 } catch (InterruptedException ie) {
597 /* ignore */
598 }
599 }
600 }
601
602 /**
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700603 * Finalize an {@link AccessibilityNodeInfo} before passing it to the client.
604 *
605 * @param info The info.
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800606 * @param connectionId The id of the connection to the system.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700607 */
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700608 private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info,
609 int connectionId) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700610 if (info != null) {
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800611 info.setConnectionId(connectionId);
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700612 info.setSealed(true);
Svetoslav Ganovc406be92012-05-11 16:12:32 -0700613 sAccessibilityNodeInfoCache.add(info);
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700614 }
615 }
616
617 /**
618 * Finalize {@link AccessibilityNodeInfo}s before passing them to the client.
619 *
620 * @param infos The {@link AccessibilityNodeInfo}s.
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800621 * @param connectionId The id of the connection to the system.
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700622 */
Svetoslav Ganov0d04e242012-02-21 13:46:36 -0800623 private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700624 int connectionId) {
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700625 if (infos != null) {
626 final int infosCount = infos.size();
627 for (int i = 0; i < infosCount; i++) {
628 AccessibilityNodeInfo info = infos.get(i);
Svetoslav Ganov152e9bb2012-10-12 20:15:29 -0700629 finalizeAndCacheAccessibilityNodeInfo(info, connectionId);
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700630 }
631 }
632 }
633
634 /**
635 * Gets the message stored if the interacted and interacting
636 * threads are the same.
637 *
638 * @return The message.
639 */
640 private Message getSameProcessMessageAndClear() {
641 synchronized (mInstanceLock) {
642 Message result = mSameThreadMessage;
643 mSameThreadMessage = null;
644 return result;
645 }
646 }
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800647
648 /**
649 * Gets a cached accessibility service connection.
650 *
651 * @param connectionId The connection id.
652 * @return The cached connection if such.
653 */
654 public IAccessibilityServiceConnection getConnection(int connectionId) {
Svetoslav Ganov36bcdb52011-12-01 11:48:21 -0800655 synchronized (sConnectionCache) {
656 return sConnectionCache.get(connectionId);
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800657 }
658 }
659
660 /**
661 * Adds a cached accessibility service connection.
662 *
663 * @param connectionId The connection id.
664 * @param connection The connection.
665 */
666 public void addConnection(int connectionId, IAccessibilityServiceConnection connection) {
Svetoslav Ganov36bcdb52011-12-01 11:48:21 -0800667 synchronized (sConnectionCache) {
668 sConnectionCache.put(connectionId, connection);
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800669 }
670 }
671
672 /**
673 * Removes a cached accessibility service connection.
674 *
675 * @param connectionId The connection id.
676 */
677 public void removeConnection(int connectionId) {
Svetoslav Ganov36bcdb52011-12-01 11:48:21 -0800678 synchronized (sConnectionCache) {
679 sConnectionCache.remove(connectionId);
Svetoslav Ganovd116d7c2011-11-21 18:41:59 -0800680 }
681 }
Svetoslav Ganov4528b4e2012-05-15 18:24:10 -0700682
683 /**
684 * Checks whether the infos are a fully connected tree with no duplicates.
685 *
686 * @param infos The result list to check.
687 */
688 private void checkFindAccessibilityNodeInfoResultIntegrity(List<AccessibilityNodeInfo> infos) {
689 if (infos.size() == 0) {
690 return;
691 }
692 // Find the root node.
693 AccessibilityNodeInfo root = infos.get(0);
694 final int infoCount = infos.size();
695 for (int i = 1; i < infoCount; i++) {
696 for (int j = i; j < infoCount; j++) {
697 AccessibilityNodeInfo candidate = infos.get(j);
698 if (root.getParentNodeId() == candidate.getSourceNodeId()) {
699 root = candidate;
700 break;
701 }
702 }
703 }
704 if (root == null) {
705 Log.e(LOG_TAG, "No root.");
706 }
707 // Check for duplicates.
708 HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>();
709 Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>();
710 fringe.add(root);
711 while (!fringe.isEmpty()) {
712 AccessibilityNodeInfo current = fringe.poll();
713 if (!seen.add(current)) {
714 Log.e(LOG_TAG, "Duplicate node.");
715 return;
716 }
717 SparseLongArray childIds = current.getChildNodeIds();
718 final int childCount = childIds.size();
719 for (int i = 0; i < childCount; i++) {
720 final long childId = childIds.valueAt(i);
721 for (int j = 0; j < infoCount; j++) {
722 AccessibilityNodeInfo child = infos.get(j);
723 if (child.getSourceNodeId() == childId) {
724 fringe.add(child);
725 }
726 }
727 }
728 }
729 final int disconnectedCount = infos.size() - seen.size();
730 if (disconnectedCount > 0) {
731 Log.e(LOG_TAG, disconnectedCount + " Disconnected nodes.");
732 }
733 }
Svetoslav Ganov8bd69612011-08-23 13:40:30 -0700734}