| /* |
| ** Copyright 2011, The Android Open Source Project |
| ** |
| ** Licensed under the Apache License, Version 2.0 (the "License"); |
| ** you may not use this file except in compliance with the License. |
| ** You may obtain a copy of the License at |
| ** |
| ** http://www.apache.org/licenses/LICENSE-2.0 |
| ** |
| ** Unless required by applicable law or agreed to in writing, software |
| ** distributed under the License is distributed on an "AS IS" BASIS, |
| ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| ** See the License for the specific language governing permissions and |
| ** limitations under the License. |
| */ |
| |
| package android.view.accessibility; |
| |
| import android.accessibilityservice.IAccessibilityServiceConnection; |
| import android.graphics.Rect; |
| import android.os.Message; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| /** |
| * This class is a singleton that performs accessibility interaction |
| * which is it queries remote view hierarchies about snapshots of their |
| * views as well requests from these hierarchies to perform certain |
| * actions on their views. |
| * |
| * Rationale: The content retrieval APIs are synchronous from a client's |
| * perspective but internally they are asynchronous. The client thread |
| * calls into the system requesting an action and providing a callback |
| * to receive the result after which it waits up to a timeout for that |
| * result. The system enforces security and the delegates the request |
| * to a given view hierarchy where a message is posted (from a binder |
| * thread) describing what to be performed by the main UI thread the |
| * result of which it delivered via the mentioned callback. However, |
| * the blocked client thread and the main UI thread of the target view |
| * hierarchy can be the same thread, for example an accessibility service |
| * and an activity run in the same process, thus they are executed on the |
| * same main thread. In such a case the retrieval will fail since the UI |
| * thread that has to process the message describing the work to be done |
| * is blocked waiting for a result is has to compute! To avoid this scenario |
| * when making a call the client also passes its process and thread ids so |
| * the accessed view hierarchy can detect if the client making the request |
| * is running in its main UI thread. In such a case the view hierarchy, |
| * specifically the binder thread performing the IPC to it, does not post a |
| * message to be run on the UI thread but passes it to the singleton |
| * interaction client through which all interactions occur and the latter is |
| * responsible to execute the message before starting to wait for the |
| * asynchronous result delivered via the callback. In this case the expected |
| * result is already received so no waiting is performed. |
| * |
| * @hide |
| */ |
| public final class AccessibilityInteractionClient |
| extends IAccessibilityInteractionConnectionCallback.Stub { |
| |
| public static final int NO_ID = -1; |
| |
| private static final String LOG_TAG = "AccessibilityInteractionClient"; |
| |
| private static final boolean DEBUG = false; |
| |
| private static final long TIMEOUT_INTERACTION_MILLIS = 5000; |
| |
| private static final Object sStaticLock = new Object(); |
| |
| private static AccessibilityInteractionClient sInstance; |
| |
| private final AtomicInteger mInteractionIdCounter = new AtomicInteger(); |
| |
| private final Object mInstanceLock = new Object(); |
| |
| private int mInteractionId = -1; |
| |
| private AccessibilityNodeInfo mFindAccessibilityNodeInfoResult; |
| |
| private List<AccessibilityNodeInfo> mFindAccessibilityNodeInfosResult; |
| |
| private boolean mPerformAccessibilityActionResult; |
| |
| private Message mSameThreadMessage; |
| |
| private final Rect mTempBounds = new Rect(); |
| |
| // The connection cache is shared between all interrogating threads. |
| private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache = |
| new SparseArray<IAccessibilityServiceConnection>(); |
| |
| /** |
| * @return The singleton of this class. |
| */ |
| public static AccessibilityInteractionClient getInstance() { |
| synchronized (sStaticLock) { |
| if (sInstance == null) { |
| sInstance = new AccessibilityInteractionClient(); |
| } |
| return sInstance; |
| } |
| } |
| |
| /** |
| * Sets the message to be processed if the interacted view hierarchy |
| * and the interacting client are running in the same thread. |
| * |
| * @param message The message. |
| */ |
| public void setSameThreadMessage(Message message) { |
| synchronized (mInstanceLock) { |
| mSameThreadMessage = message; |
| mInstanceLock.notifyAll(); |
| } |
| } |
| |
| /** |
| * Finds an {@link AccessibilityNodeInfo} by accessibility id. |
| * |
| * @param connectionId The id of a connection for interacting with the system. |
| * @param accessibilityWindowId A unique window id. |
| * @param accessibilityViewId A unique View accessibility id. |
| * @return An {@link AccessibilityNodeInfo} if found, null otherwise. |
| */ |
| public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, |
| int accessibilityWindowId, int accessibilityViewId) { |
| try { |
| IAccessibilityServiceConnection connection = getConnection(connectionId); |
| if (connection != null) { |
| final int interactionId = mInteractionIdCounter.getAndIncrement(); |
| final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId( |
| accessibilityWindowId, accessibilityViewId, interactionId, this, |
| Thread.currentThread().getId()); |
| // If the scale is zero the call has failed. |
| if (windowScale > 0) { |
| AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( |
| interactionId); |
| finalizeAccessibilityNodeInfo(info, connectionId, windowScale); |
| return info; |
| } |
| } else { |
| if (DEBUG) { |
| Log.w(LOG_TAG, "No connection for connection id: " + connectionId); |
| } |
| } |
| } catch (RemoteException re) { |
| if (DEBUG) { |
| Log.w(LOG_TAG, "Error while calling remote" |
| + " findAccessibilityNodeInfoByAccessibilityId", re); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed |
| * in the currently active window and starts from the root View in the window. |
| * |
| * @param connectionId The id of a connection for interacting with the system. |
| * @param viewId The id of the view. |
| * @return An {@link AccessibilityNodeInfo} if found, null otherwise. |
| */ |
| public AccessibilityNodeInfo findAccessibilityNodeInfoByViewIdInActiveWindow(int connectionId, |
| int viewId) { |
| try { |
| IAccessibilityServiceConnection connection = getConnection(connectionId); |
| if (connection != null) { |
| final int interactionId = mInteractionIdCounter.getAndIncrement(); |
| final float windowScale = |
| connection.findAccessibilityNodeInfoByViewIdInActiveWindow(viewId, |
| interactionId, this, Thread.currentThread().getId()); |
| // If the scale is zero the call has failed. |
| if (windowScale > 0) { |
| AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( |
| interactionId); |
| finalizeAccessibilityNodeInfo(info, connectionId, windowScale); |
| return info; |
| } |
| } else { |
| if (DEBUG) { |
| Log.w(LOG_TAG, "No connection for connection id: " + connectionId); |
| } |
| } |
| } catch (RemoteException re) { |
| if (DEBUG) { |
| Log.w(LOG_TAG, "Error while calling remote" |
| + " findAccessibilityNodeInfoByViewIdInActiveWindow", re); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Finds {@link AccessibilityNodeInfo}s by View text. The match is case |
| * insensitive containment. The search is performed in the currently |
| * active window and starts from the root View in the window. |
| * |
| * @param connectionId The id of a connection for interacting with the system. |
| * @param text The searched text. |
| * @return A list of found {@link AccessibilityNodeInfo}s. |
| */ |
| public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewTextInActiveWindow( |
| int connectionId, String text) { |
| try { |
| IAccessibilityServiceConnection connection = getConnection(connectionId); |
| if (connection != null) { |
| final int interactionId = mInteractionIdCounter.getAndIncrement(); |
| final float windowScale = |
| connection.findAccessibilityNodeInfosByViewTextInActiveWindow(text, |
| interactionId, this, Thread.currentThread().getId()); |
| // If the scale is zero the call has failed. |
| if (windowScale > 0) { |
| List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( |
| interactionId); |
| finalizeAccessibilityNodeInfos(infos, connectionId, windowScale); |
| return infos; |
| } |
| } else { |
| if (DEBUG) { |
| Log.w(LOG_TAG, "No connection for connection id: " + connectionId); |
| } |
| } |
| } catch (RemoteException re) { |
| if (DEBUG) { |
| Log.w(LOG_TAG, "Error while calling remote" |
| + " findAccessibilityNodeInfosByViewTextInActiveWindow", re); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Finds {@link AccessibilityNodeInfo}s by View text. The match is case |
| * insensitive containment. The search is performed in the window whose |
| * id is specified and starts from the View whose accessibility id is |
| * specified. |
| * |
| * @param connectionId The id of a connection for interacting with the system. |
| * @param text The searched text. |
| * @param accessibilityWindowId A unique window id. |
| * @param accessibilityViewId A unique View accessibility id from where to start the search. |
| * Use {@link android.view.View#NO_ID} to start from the root. |
| * @return A list of found {@link AccessibilityNodeInfo}s. |
| */ |
| public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewText(int connectionId, |
| String text, int accessibilityWindowId, int accessibilityViewId) { |
| try { |
| IAccessibilityServiceConnection connection = getConnection(connectionId); |
| if (connection != null) { |
| final int interactionId = mInteractionIdCounter.getAndIncrement(); |
| final float windowScale = connection.findAccessibilityNodeInfosByViewText(text, |
| accessibilityWindowId, accessibilityViewId, interactionId, this, |
| Thread.currentThread().getId()); |
| // If the scale is zero the call has failed. |
| if (windowScale > 0) { |
| List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( |
| interactionId); |
| finalizeAccessibilityNodeInfos(infos, connectionId, windowScale); |
| return infos; |
| } |
| } else { |
| if (DEBUG) { |
| Log.w(LOG_TAG, "No connection for connection id: " + connectionId); |
| } |
| } |
| } catch (RemoteException re) { |
| if (DEBUG) { |
| Log.w(LOG_TAG, "Error while calling remote" |
| + " findAccessibilityNodeInfosByViewText", re); |
| } |
| } |
| return Collections.emptyList(); |
| } |
| |
| /** |
| * Performs an accessibility action on an {@link AccessibilityNodeInfo}. |
| * |
| * @param connectionId The id of a connection for interacting with the system. |
| * @param accessibilityWindowId The id of the window. |
| * @param accessibilityViewId A unique View accessibility id. |
| * @param action The action to perform. |
| * @return Whether the action was performed. |
| */ |
| public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId, |
| int accessibilityViewId, int action) { |
| try { |
| IAccessibilityServiceConnection connection = getConnection(connectionId); |
| if (connection != null) { |
| final int interactionId = mInteractionIdCounter.getAndIncrement(); |
| final boolean success = connection.performAccessibilityAction( |
| accessibilityWindowId, accessibilityViewId, action, interactionId, this, |
| Thread.currentThread().getId()); |
| if (success) { |
| return getPerformAccessibilityActionResult(interactionId); |
| } |
| } else { |
| if (DEBUG) { |
| Log.w(LOG_TAG, "No connection for connection id: " + connectionId); |
| } |
| } |
| } catch (RemoteException re) { |
| if (DEBUG) { |
| Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Gets the the result of an async request that returns an {@link AccessibilityNodeInfo}. |
| * |
| * @param interactionId The interaction id to match the result with the request. |
| * @return The result {@link AccessibilityNodeInfo}. |
| */ |
| private AccessibilityNodeInfo getFindAccessibilityNodeInfoResultAndClear(int interactionId) { |
| synchronized (mInstanceLock) { |
| final boolean success = waitForResultTimedLocked(interactionId); |
| AccessibilityNodeInfo result = success ? mFindAccessibilityNodeInfoResult : null; |
| clearResultLocked(); |
| return result; |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, |
| int interactionId) { |
| synchronized (mInstanceLock) { |
| if (interactionId > mInteractionId) { |
| mFindAccessibilityNodeInfoResult = info; |
| mInteractionId = interactionId; |
| } |
| mInstanceLock.notifyAll(); |
| } |
| } |
| |
| /** |
| * Gets the the result of an async request that returns {@link AccessibilityNodeInfo}s. |
| * |
| * @param interactionId The interaction id to match the result with the request. |
| * @return The result {@link AccessibilityNodeInfo}s. |
| */ |
| private List<AccessibilityNodeInfo> getFindAccessibilityNodeInfosResultAndClear( |
| int interactionId) { |
| synchronized (mInstanceLock) { |
| final boolean success = waitForResultTimedLocked(interactionId); |
| List<AccessibilityNodeInfo> result = success ? mFindAccessibilityNodeInfosResult : null; |
| clearResultLocked(); |
| return result; |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos, |
| int interactionId) { |
| synchronized (mInstanceLock) { |
| if (interactionId > mInteractionId) { |
| mFindAccessibilityNodeInfosResult = infos; |
| mInteractionId = interactionId; |
| } |
| mInstanceLock.notifyAll(); |
| } |
| } |
| |
| /** |
| * Gets the result of a request to perform an accessibility action. |
| * |
| * @param interactionId The interaction id to match the result with the request. |
| * @return Whether the action was performed. |
| */ |
| private boolean getPerformAccessibilityActionResult(int interactionId) { |
| synchronized (mInstanceLock) { |
| final boolean success = waitForResultTimedLocked(interactionId); |
| final boolean result = success ? mPerformAccessibilityActionResult : false; |
| clearResultLocked(); |
| return result; |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId) { |
| synchronized (mInstanceLock) { |
| if (interactionId > mInteractionId) { |
| mPerformAccessibilityActionResult = succeeded; |
| mInteractionId = interactionId; |
| } |
| mInstanceLock.notifyAll(); |
| } |
| } |
| |
| /** |
| * Clears the result state. |
| */ |
| private void clearResultLocked() { |
| mInteractionId = -1; |
| mFindAccessibilityNodeInfoResult = null; |
| mFindAccessibilityNodeInfosResult = null; |
| mPerformAccessibilityActionResult = false; |
| } |
| |
| /** |
| * Waits up to a given bound for a result of a request and returns it. |
| * |
| * @param interactionId The interaction id to match the result with the request. |
| * @return Whether the result was received. |
| */ |
| private boolean waitForResultTimedLocked(int interactionId) { |
| long waitTimeMillis = TIMEOUT_INTERACTION_MILLIS; |
| final long startTimeMillis = SystemClock.uptimeMillis(); |
| while (true) { |
| try { |
| Message sameProcessMessage = getSameProcessMessageAndClear(); |
| if (sameProcessMessage != null) { |
| sameProcessMessage.getTarget().handleMessage(sameProcessMessage); |
| } |
| |
| if (mInteractionId == interactionId) { |
| return true; |
| } |
| if (mInteractionId > interactionId) { |
| return false; |
| } |
| final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; |
| waitTimeMillis = TIMEOUT_INTERACTION_MILLIS - elapsedTimeMillis; |
| if (waitTimeMillis <= 0) { |
| return false; |
| } |
| mInstanceLock.wait(waitTimeMillis); |
| } catch (InterruptedException ie) { |
| /* ignore */ |
| } |
| } |
| } |
| |
| /** |
| * Applies compatibility scale to the info bounds if it is not equal to one. |
| * |
| * @param info The info whose bounds to scale. |
| * @param scale The scale to apply. |
| */ |
| private void applyCompatibilityScaleIfNeeded(AccessibilityNodeInfo info, float scale) { |
| if (scale == 1.0f) { |
| return; |
| } |
| Rect bounds = mTempBounds; |
| info.getBoundsInParent(bounds); |
| bounds.scale(scale); |
| info.setBoundsInParent(bounds); |
| |
| info.getBoundsInScreen(bounds); |
| bounds.scale(scale); |
| info.setBoundsInScreen(bounds); |
| } |
| |
| /** |
| * Finalize an {@link AccessibilityNodeInfo} before passing it to the client. |
| * |
| * @param info The info. |
| * @param connectionId The id of the connection to the system. |
| * @param windowScale The source window compatibility scale. |
| */ |
| private void finalizeAccessibilityNodeInfo(AccessibilityNodeInfo info, int connectionId, |
| float windowScale) { |
| if (info != null) { |
| applyCompatibilityScaleIfNeeded(info, windowScale); |
| info.setConnectionId(connectionId); |
| info.setSealed(true); |
| } |
| } |
| |
| /** |
| * Finalize {@link AccessibilityNodeInfo}s before passing them to the client. |
| * |
| * @param infos The {@link AccessibilityNodeInfo}s. |
| * @param connectionId The id of the connection to the system. |
| * @param windowScale The source window compatibility scale. |
| */ |
| private void finalizeAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, |
| int connectionId, float windowScale) { |
| if (infos != null) { |
| final int infosCount = infos.size(); |
| for (int i = 0; i < infosCount; i++) { |
| AccessibilityNodeInfo info = infos.get(i); |
| finalizeAccessibilityNodeInfo(info, connectionId, windowScale); |
| } |
| } |
| } |
| |
| /** |
| * Gets the message stored if the interacted and interacting |
| * threads are the same. |
| * |
| * @return The message. |
| */ |
| private Message getSameProcessMessageAndClear() { |
| synchronized (mInstanceLock) { |
| Message result = mSameThreadMessage; |
| mSameThreadMessage = null; |
| return result; |
| } |
| } |
| |
| /** |
| * Gets a cached accessibility service connection. |
| * |
| * @param connectionId The connection id. |
| * @return The cached connection if such. |
| */ |
| public IAccessibilityServiceConnection getConnection(int connectionId) { |
| synchronized (sConnectionCache) { |
| return sConnectionCache.get(connectionId); |
| } |
| } |
| |
| /** |
| * Adds a cached accessibility service connection. |
| * |
| * @param connectionId The connection id. |
| * @param connection The connection. |
| */ |
| public void addConnection(int connectionId, IAccessibilityServiceConnection connection) { |
| synchronized (sConnectionCache) { |
| sConnectionCache.put(connectionId, connection); |
| } |
| } |
| |
| /** |
| * Removes a cached accessibility service connection. |
| * |
| * @param connectionId The connection id. |
| */ |
| public void removeConnection(int connectionId) { |
| synchronized (sConnectionCache) { |
| sConnectionCache.remove(connectionId); |
| } |
| } |
| } |