/* ** 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); } } }