diff options
author | Svetoslav Ganov <svetoslavganov@google.com> | 2011-04-20 12:12:33 -0700 |
---|---|---|
committer | Svetoslav Ganov <svetoslavganov@google.com> | 2011-06-06 18:46:03 -0700 |
commit | 8643aa0179e598e78d938c59035389054535a229 (patch) | |
tree | c1d0ab0f77329fb34231e45703687d21a601fc88 /core/java | |
parent | 219451363d7c9e657cb856bebce34bc29d7876fa (diff) | |
download | frameworks_base-8643aa0179e598e78d938c59035389054535a229.zip frameworks_base-8643aa0179e598e78d938c59035389054535a229.tar.gz frameworks_base-8643aa0179e598e78d938c59035389054535a229.tar.bz2 |
Interrogation of the view hierarchy from an AccessibilityService.
1. Views are represented as AccessibilityNodeInfos to AccessibilityServices.
2. An accessibility service receives AccessibilityEvents and can ask
for its source and gets an AccessibilityNodeInfo which can be used
to get its parent and children infos and so on.
3. AccessibilityNodeInfo contains some attributes and actions that
can be performed on the source.
4. AccessibilityService can request the system to preform an action
on the source of an AccessibilityNodeInfo.
5. ViewAncestor provides an interaction connection to the
AccessibiltyManagerService and an accessibility service uses
its connection to the latter to interact with screen content.
6. AccessibilityService can interact ONLY with the focused window
and all calls are routed through the AccessibilityManagerService
which imposes security.
7. Hidden APIs on AccessibilityService can find AccessibilityNodeInfos
based on some criteria. These API go through the AccessibilityManagerServcie
for security check.
8. Some actions are hidden and are exposes only to eng builds for UI testing.
Change-Id: Ie34fa4219f350eb3f4f6f9f45b24f709bd98783c
Diffstat (limited to 'core/java')
19 files changed, 2297 insertions, 86 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 28fc21a..8bb305d 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -25,6 +25,7 @@ import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** * An accessibility service runs in the background and receives callbacks by the system @@ -51,7 +52,11 @@ import android.view.accessibility.AccessibilityEvent; * enabling or disabling it in the device settings. After the system binds to a service it * calls {@link AccessibilityService#onServiceConnected()}. This method can be * overriden by clients that want to perform post binding setup. + * </p> * <p> + * An accessibility service can be configured to receive specific types of accessibility events, + * listen only to specific packages, get events from each type only once in a given time frame, + * retrieve window content, specify a settings activity, etc. * </p> * There are two approaches for configuring an accessibility service: * <ul> @@ -59,20 +64,23 @@ import android.view.accessibility.AccessibilityEvent; * Providing a {@link #SERVICE_META_DATA meta-data} entry in the manifest when declaring * the service. A service declaration with a meta-data tag is presented below: * <p> - * <code> - * <service android:name=".MyAccessibilityService"><br> - * <intent-filter><br> - * <action android:name="android.accessibilityservice.AccessibilityService" /><br> - * </intent-filter><br> - * <meta-data android:name="android.accessibilityservice.as" android:resource="@xml/accessibilityservice" /><br> - * </service><br> - * </code> + * <code> + * <service android:name=".MyAccessibilityService"><br> + * <intent-filter><br> + * <action android:name="android.accessibilityservice.AccessibilityService" /><br> + * </intent-filter><br> + * <meta-data android:name="android.accessibilityservice.as" android:resource="@xml/accessibilityservice" /><br> + * </service><br> + * </code> * </p> * <p> * <strong> * This approach enables setting all accessibility service properties. * </strong> * </p> + * <p> + * For more details refer to {@link #SERVICE_META_DATA}. + * </p> * </li> * <li> * Calling {@link AccessibilityService#setServiceInfo(AccessibilityServiceInfo)}. Note @@ -88,6 +96,9 @@ import android.view.accessibility.AccessibilityEvent; * {@link AccessibilityServiceInfo#packageNames} * </strong> * </p> + * <p> + * For more details refer to {@link AccessibilityServiceInfo}. + * </p> * </li> * </ul> * <p> @@ -151,16 +162,49 @@ public abstract class AccessibilityService extends Service { * <code> * <?xml version="1.0" encoding="utf-8"?><br> * <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"<br> - * android:eventTypes="typeViewClicked|typeViewFocused"<br> + * android:accessibilityEventTypes="typeViewClicked|typeViewFocused"<br> * android:packageNames="foo.bar, foo.baz"<br> - * android:feedbackType="feedbackSpoken"<br> + * android:accessibilityFeedbackType="feedbackSpoken"<br> * android:notificationTimeout="100"<br> - * android:flags="flagDefault"<br> + * android:accessibilityFlags="flagDefault"<br> * android:settingsActivity="foo.bar.TestBackActivity"<br> * . . .<br> * /> * </code> * </p> + * <p> + * <strong>Note:</strong> A service can retrieve only the content of the active window. + * An active window is the source of the most recent event of type + * {@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_START}, + * {@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_END}, + * {@link AccessibilityEvent#TYPE_VIEW_CLICKED}, + * {@link AccessibilityEvent#TYPE_VIEW_FOCUSED}, + * {@link AccessibilityEvent#TYPE_VIEW_HOVER_ENTER}, + * {@link AccessibilityEvent#TYPE_VIEW_HOVER_EXIT}, + * {@link AccessibilityEvent#TYPE_VIEW_LONG_CLICKED}, + * {@link AccessibilityEvent#TYPE_VIEW_SELECTED}, + * {@link AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED}, + * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED}. + * Therefore the service should: + * <ul> + * <li> + * Register for all event types with no notification timeout and keep track + * for the active window by calling + * {@link AccessibilityEvent#getAccessibilityWindowId()} of the last received + * event and compare this with the + * {@link AccessibilityNodeInfo#getAccessibilityWindowId()} before calling + * retrieval methods on the latter. + * </li> + * <li> + * Prepare that a retrieval method on {@link AccessibilityNodeInfo} may fail + * since the active window has changed and the service did not get the + * accessibility event. Note that it is possible to have a retrieval method + * failing event adopting the strategy specified in the previous bullet + * because the accessibility event dispatch is asynchronous and crosses + * process boundaries. + * </li> + * <ul> + * </p> */ public static final String SERVICE_META_DATA = "android.accessibilityservice"; @@ -224,7 +268,7 @@ public abstract class AccessibilityService extends Service { /** * Implement to return the implementation of the internal accessibility - * service interface. Subclasses should not override. + * service interface. */ @Override public final IBinder onBind(Intent intent) { diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 7157def..19f0bf0 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -17,14 +17,72 @@ package android.accessibilityservice; import android.accessibilityservice.AccessibilityServiceInfo; +import android.view.accessibility.AccessibilityNodeInfo; /** - * Interface AccessibilityManagerService#Service implements, and passes to an - * AccessibilityService so it can dynamically configure how the system handles it. + * Interface given to an AccessibilitySerivce to talk to the AccessibilityManagerService. * * @hide */ -oneway interface IAccessibilityServiceConnection { +interface IAccessibilityServiceConnection { void setServiceInfo(in AccessibilityServiceInfo info); + + /** + * Finds an {@link AccessibilityNodeInfo} by accessibility id. + * <p> + * <strong> + * It is a client responsibility to recycle the received info by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * + * @param accessibilityWindowId A unique window id. + * @param accessibilityViewId A unique View accessibility id. + * @return The node info. + */ + AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, + int accessibilityViewId); + + /** + * Finds {@link AccessibilityNodeInfo}s by View text. The match is case + * insensitive containment. + * <p> + * <strong> + * It is a client responsibility to recycle the received infos by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * + * @param text The searched text. + * @return A list of node info. + */ + List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewText(String text); + + /** + * Finds an {@link AccessibilityNodeInfo} by View id. + * <p> + * <strong> + * It is a client responsibility to recycle the received info by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * + * @param id The id of the node. + * @return The node info. + */ + AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int viewId); + + /** + * Performs an accessibility action on an {@link AccessibilityNodeInfo}. + * + * @param accessibilityWindowId The id of the window. + * @param accessibilityViewId The of a view in the . + * @return Whether the action was performed. + */ + boolean performAccessibilityAction(int accessibilityWindowId, int accessibilityViewId, + int action); } diff --git a/core/java/android/util/FinitePool.java b/core/java/android/util/FinitePool.java index 3ef8293..4ae21ad 100644 --- a/core/java/android/util/FinitePool.java +++ b/core/java/android/util/FinitePool.java @@ -69,6 +69,7 @@ class FinitePool<T extends Poolable<T>> implements Pool<T> { if (element != null) { element.setNextPoolable(null); + element.setPooled(false); mManager.onAcquired(element); } @@ -76,9 +77,13 @@ class FinitePool<T extends Poolable<T>> implements Pool<T> { } public void release(T element) { + if (element.isPooled()) { + throw new IllegalArgumentException("Element already in the pool."); + } if (mInfinite || mPoolCount < mLimit) { mPoolCount++; element.setNextPoolable(mRoot); + element.setPooled(true); mRoot = element; } mManager.onReleased(element); diff --git a/core/java/android/util/Poolable.java b/core/java/android/util/Poolable.java index fd9bd9b..87e0529 100644 --- a/core/java/android/util/Poolable.java +++ b/core/java/android/util/Poolable.java @@ -22,4 +22,6 @@ package android.util; public interface Poolable<T> { void setNextPoolable(T element); T getNextPoolable(); + boolean isPooled(); + void setPooled(boolean isPooled); } diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java index fccef2b..5a91d31 100644 --- a/core/java/android/view/VelocityTracker.java +++ b/core/java/android/view/VelocityTracker.java @@ -50,6 +50,7 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { private int mPtr; private VelocityTracker mNext; + private boolean mIsPooled; private static native int nativeInitialize(); private static native void nativeDispose(int ptr); @@ -93,6 +94,20 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { return mNext; } + /** + * @hide + */ + public boolean isPooled() { + return mIsPooled; + } + + /** + * @hide + */ + public void setPooled(boolean isPooled) { + mIsPooled = isPooled; + } + private VelocityTracker() { mPtr = nativeInitialize(); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 014f19f..51eb13b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -61,6 +61,7 @@ import android.view.ContextMenu.ContextMenuInfo; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEventSource; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; @@ -1492,6 +1493,11 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit private static final Object sTagsLock = new Object(); /** + * The next available accessiiblity id. + */ + private static int sNextAccessibilityViewId; + + /** * The animation currently associated with this view. * @hide */ @@ -1533,6 +1539,11 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit int mID = NO_ID; /** + * The stable ID of this view for accessibility porposes. + */ + int mAccessibilityViewId = NO_ID; + + /** * The view's tag. * {@hide} * @@ -3649,6 +3660,7 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent) */ public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setSource(this); event.setClassName(getClass().getName()); event.setPackageName(getContext().getPackageName()); event.setEnabled(isEnabled()); @@ -3664,6 +3676,112 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit } /** + * Returns an {@link AccessibilityNodeInfo} representing this view from the + * point of view of an {@link android.accessibilityservice.AccessibilityService}. + * This method is responsible for obtaining an accessibility node info from a + * pool of reusable instances and calling + * {@link #onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)} on this view to + * initialize the former. + * <p> + * Note: The client is responsible for recycling the obtained instance by calling + * {@link AccessibilityNodeInfo#recycle()} to minimize object creation. + * </p> + * @return A populated {@link AccessibilityNodeInfo}. + * + * @see AccessibilityNodeInfo + */ + public AccessibilityNodeInfo createAccessibilityNodeInfo() { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this); + onInitializeAccessibilityNodeInfo(info); + return info; + } + + /** + * Initializes an {@link AccessibilityNodeInfo} with information about this view. + * The base implementation sets: + * <ul> + * <li>{@link AccessibilityNodeInfo#setParent(View)},</li> + * <li>{@link AccessibilityNodeInfo#setBounds(Rect)},</li> + * <li>{@link AccessibilityNodeInfo#setPackageName(CharSequence)},</li> + * <li>{@link AccessibilityNodeInfo#setClassName(CharSequence)},</li> + * <li>{@link AccessibilityNodeInfo#setContentDescription(CharSequence)},</li> + * <li>{@link AccessibilityNodeInfo#setEnabled(boolean)},</li> + * <li>{@link AccessibilityNodeInfo#setClickable(boolean)},</li> + * <li>{@link AccessibilityNodeInfo#setFocusable(boolean)},</li> + * <li>{@link AccessibilityNodeInfo#setFocused(boolean)},</li> + * <li>{@link AccessibilityNodeInfo#setLongClickable(boolean)},</li> + * <li>{@link AccessibilityNodeInfo#setSelected(boolean)},</li> + * </ul> + * <p> + * Subclasses should override this method, call the super implementation, + * and set additional attributes. + * </p> + * @param info The instance to initialize. + */ + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + Rect bounds = mAttachInfo.mTmpInvalRect; + getDrawingRect(bounds); + info.setBounds(bounds); + + ViewParent parent = getParent(); + if (parent instanceof View) { + View parentView = (View) parent; + info.setParent(parentView); + } + + info.setPackageName(mContext.getPackageName()); + info.setClassName(getClass().getName()); + info.setContentDescription(getContentDescription()); + + info.setEnabled(isEnabled()); + info.setClickable(isClickable()); + info.setFocusable(isFocusable()); + info.setFocused(isFocused()); + info.setSelected(isSelected()); + info.setLongClickable(isLongClickable()); + + // TODO: These make sense only if we are in an AdapterView but all + // views can be selected. Maybe from accessiiblity perspective + // we should report as selectable view in an AdapterView. + info.addAction(AccessibilityNodeInfo.ACTION_SELECT); + info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION); + + if (isFocusable()) { + if (isFocused()) { + info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS); + } else { + info.addAction(AccessibilityNodeInfo.ACTION_FOCUS); + } + } + } + + /** + * Gets the unique identifier of this view on the screen for accessibility purposes. + * If this {@link View} is not attached to any window, {@value #NO_ID} is returned. + * + * @return The view accessibility id. + * + * @hide + */ + public int getAccessibilityViewId() { + if (mAccessibilityViewId == NO_ID) { + mAccessibilityViewId = sNextAccessibilityViewId++; + } + return mAccessibilityViewId; + } + + /** + * Gets the unique identifier of the window in which this View reseides. + * + * @return The window accessibility id. + * + * @hide + */ + public int getAccessibilityWindowId() { + return mAttachInfo != null ? mAttachInfo.mAccessibilityWindowId : NO_ID; + } + + /** * Gets the {@link View} description. It briefly describes the view and is * primarily used for accessibility support. Set this property to enable * better accessibility support for your application. This is especially @@ -4571,6 +4689,16 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit } /** + * Finds the Views that contain given text. The containment is case insensitive. + * As View's text is considered any text content that View renders. + * + * @param outViews The output list of matching Views. + * @param text The text to match against. + */ + public void findViewsWithText(ArrayList<View> outViews, CharSequence text) { + } + + /** * Find and return all touchable views that are descendants of this view, * possibly including this view if it is touchable itself. * @@ -4677,8 +4805,8 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit // need to be focusable in touch mode if in touch mode if (isInTouchMode() && - (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) { - return false; + (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) { + return false; } // need to not have any parents blocking us @@ -12719,6 +12847,7 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit ); private InvalidateInfo mNext; + private boolean mIsPooled; View target; @@ -12742,6 +12871,14 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit void release() { sPool.release(this); } + + public boolean isPooled() { + return mIsPooled; + } + + public void setPooled(boolean isPooled) { + mIsPooled = isPooled; + } } final IWindowSession mSession; @@ -12952,6 +13089,11 @@ public class View implements Drawable.Callback2, KeyEvent.Callback, Accessibilit final ArrayList<View> mFocusablesTempList = new ArrayList<View>(24); /** + * The id of the window for accessibility purposes. + */ + int mAccessibilityWindowId = View.NO_ID; + + /** * Creates a new set of attachment information with the specified * events handler and thread. * diff --git a/core/java/android/view/ViewAncestor.java b/core/java/android/view/ViewAncestor.java index 1e72529..9ab4c82 100644 --- a/core/java/android/view/ViewAncestor.java +++ b/core/java/android/view/ViewAncestor.java @@ -50,18 +50,28 @@ import android.util.AndroidRuntimeException; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; +import android.util.Pool; +import android.util.Poolable; +import android.util.PoolableManager; +import android.util.Pools; import android.util.Slog; import android.util.SparseArray; import android.util.TypedValue; import android.view.View.MeasureSpec; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IAccessibilityInteractionConnection; +import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.Scroller; + import com.android.internal.policy.PolicyManager; +import com.android.internal.util.Predicate; import com.android.internal.view.BaseSurfaceHolder; import com.android.internal.view.IInputMethodCallback; import com.android.internal.view.IInputMethodSession; @@ -71,6 +81,7 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.List; /** * The top of a view hierarchy, implementing the needed protocol between View @@ -248,6 +259,12 @@ public final class ViewAncestor extends Handler implements ViewParent, */ AudioManager mAudioManager; + final AccessibilityManager mAccessibilityManager; + + AccessibilityInteractionController mAccessibilityInteractionContrtoller; + + AccessibilityInteractionConnectionManager mAccessibilityInteractionConnectionManager; + private final int mDensity; /** @@ -285,7 +302,7 @@ public final class ViewAncestor extends Handler implements ViewParent, // done here instead of in the static block because Zygote does not // allow the spawning of threads. getWindowSession(context.getMainLooper()); - + mThread = Thread.currentThread(); mLocation = new WindowLeaked(null); mLocation.fillInStackTrace(); @@ -302,6 +319,11 @@ public final class ViewAncestor extends Handler implements ViewParent, mPreviousTransparentRegion = new Region(); mFirst = true; // true for the first time the view is added mAdded = false; + mAccessibilityManager = AccessibilityManager.getInstance(context); + mAccessibilityInteractionConnectionManager = + new AccessibilityInteractionConnectionManager(); + mAccessibilityManager.addAccessibilityStateChangeListener( + mAccessibilityInteractionConnectionManager); mAttachInfo = new View.AttachInfo(sWindowSession, mWindow, this, this); mViewConfiguration = ViewConfiguration.get(context); mDensity = context.getResources().getDisplayMetrics().densityDpi; @@ -490,10 +512,14 @@ public final class ViewAncestor extends Handler implements ViewParent, InputQueue.registerInputChannel(mInputChannel, mInputHandler, Looper.myQueue()); } - + view.assignParent(this); mAddedTouchMode = (res&WindowManagerImpl.ADD_FLAG_IN_TOUCH_MODE) != 0; mAppVisible = (res&WindowManagerImpl.ADD_FLAG_APP_VISIBLE) != 0; + + if (mAccessibilityManager.isEnabled()) { + mAccessibilityInteractionConnectionManager.ensureConnection(); + } } } } @@ -2004,6 +2030,10 @@ public final class ViewAncestor extends Handler implements ViewParent, mView.dispatchDetachedFromWindow(); } + mAccessibilityInteractionConnectionManager.ensureNoConnection(); + mAccessibilityManager.removeAccessibilityStateChangeListener( + mAccessibilityInteractionConnectionManager); + mView = null; mAttachInfo.mRootView = null; mAttachInfo.mSurface = null; @@ -2020,7 +2050,6 @@ public final class ViewAncestor extends Handler implements ViewParent, InputQueue.unregisterInputChannel(mInputChannel); } } - try { sWindowSession.remove(mWindow); } catch (RemoteException e) { @@ -2098,6 +2127,10 @@ public final class ViewAncestor extends Handler implements ViewParent, public final static int DISPATCH_DRAG_LOCATION_EVENT = 1016; public final static int DISPATCH_SYSTEM_UI_VISIBILITY = 1017; public final static int DISPATCH_GENERIC_MOTION = 1018; + public final static int DO_PERFORM_ACCESSIBILITY_ACTION = 1019; + public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 1020; + public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID = 1021; + public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT = 1022; @Override public void handleMessage(Message msg) { @@ -2298,9 +2331,25 @@ public final class ViewAncestor extends Handler implements ViewParent, case DISPATCH_SYSTEM_UI_VISIBILITY: { handleDispatchSystemUiVisibilityChanged(msg.arg1); } break; + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: { + getAccessibilityInteractionController() + .findAccessibilityNodeInfoByAccessibilityIdUiThread(msg); + } break; + case DO_PERFORM_ACCESSIBILITY_ACTION: { + getAccessibilityInteractionController() + .perfromAccessibilityActionUiThread(msg); + } break; + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: { + getAccessibilityInteractionController() + .findAccessibilityNodeInfoByViewIdUiThread(msg); + } break; + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT: { + getAccessibilityInteractionController() + .findAccessibilityNodeInfosByViewTextUiThread(msg); + } break; } } - + private void startInputEvent(InputQueue.FinishedCallback finishedCallback) { if (mFinishedCallback != null) { Slog.w(TAG, "Received a new input event from the input queue but there is " @@ -3220,6 +3269,17 @@ public final class ViewAncestor extends Handler implements ViewParent, return mAudioManager; } + public AccessibilityInteractionController getAccessibilityInteractionController() { + if (mView == null) { + throw new IllegalStateException("getAccessibilityInteractionController" + + " called when there is no mView"); + } + if (mAccessibilityInteractionContrtoller == null) { + mAccessibilityInteractionContrtoller = new AccessibilityInteractionController(); + } + return mAccessibilityInteractionContrtoller; + } + private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending) throws RemoteException { @@ -3517,7 +3577,7 @@ public final class ViewAncestor extends Handler implements ViewParent, * send an {@link AccessibilityEvent} to announce that. */ private void sendAccessibilityEvents() { - if (!AccessibilityManager.getInstance(mView.getContext()).isEnabled()) { + if (!mAccessibilityManager.isEnabled()) { return; } mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); @@ -3545,7 +3605,7 @@ public final class ViewAncestor extends Handler implements ViewParent, if (mView == null) { return false; } - AccessibilityManager.getInstance(child.mContext).sendAccessibilityEvent(event); + mAccessibilityManager.sendAccessibilityEvent(event); return true; } @@ -3608,18 +3668,18 @@ public final class ViewAncestor extends Handler implements ViewParent, static class InputMethodCallback extends IInputMethodCallback.Stub { private WeakReference<ViewAncestor> mViewAncestor; - public InputMethodCallback(ViewAncestor viewRoot) { - mViewAncestor = new WeakReference<ViewAncestor>(viewRoot); + public InputMethodCallback(ViewAncestor viewAncestor) { + mViewAncestor = new WeakReference<ViewAncestor>(viewAncestor); } public void finishedEvent(int seq, boolean handled) { - final ViewAncestor viewRoot = mViewAncestor.get(); - if (viewRoot != null) { - viewRoot.dispatchFinishedEvent(seq, handled); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchFinishedEvent(seq, handled); } } - public void sessionCreated(IInputMethodSession session) throws RemoteException { + public void sessionCreated(IInputMethodSession session) { // Stub -- not for use in the client. } } @@ -3627,36 +3687,37 @@ public final class ViewAncestor extends Handler implements ViewParent, static class W extends IWindow.Stub { private final WeakReference<ViewAncestor> mViewAncestor; - W(ViewAncestor viewRoot) { - mViewAncestor = new WeakReference<ViewAncestor>(viewRoot); + W(ViewAncestor viewAncestor) { + mViewAncestor = new WeakReference<ViewAncestor>(viewAncestor); } public void resized(int w, int h, Rect coveredInsets, Rect visibleInsets, boolean reportDraw, Configuration newConfig) { - final ViewAncestor viewRoot = mViewAncestor.get(); - if (viewRoot != null) { - viewRoot.dispatchResized(w, h, coveredInsets, visibleInsets, reportDraw, newConfig); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchResized(w, h, coveredInsets, visibleInsets, reportDraw, + newConfig); } } public void dispatchAppVisibility(boolean visible) { - final ViewAncestor viewRoot = mViewAncestor.get(); - if (viewRoot != null) { - viewRoot.dispatchAppVisibility(visible); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchAppVisibility(visible); } } public void dispatchGetNewSurface() { - final ViewAncestor viewRoot = mViewAncestor.get(); - if (viewRoot != null) { - viewRoot.dispatchGetNewSurface(); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchGetNewSurface(); } } public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) { - final ViewAncestor viewRoot = mViewAncestor.get(); - if (viewRoot != null) { - viewRoot.windowFocusChanged(hasFocus, inTouchMode); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.windowFocusChanged(hasFocus, inTouchMode); } } @@ -3674,9 +3735,9 @@ public final class ViewAncestor extends Handler implements ViewParent, } public void executeCommand(String command, String parameters, ParcelFileDescriptor out) { - final ViewAncestor viewRoot = mViewAncestor.get(); - if (viewRoot != null) { - final View view = viewRoot.mView; + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + final View view = viewAncestor.mView; if (view != null) { if (checkCallingPermission(Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) { @@ -3705,9 +3766,9 @@ public final class ViewAncestor extends Handler implements ViewParent, } public void closeSystemDialogs(String reason) { - final ViewAncestor viewRoot = mViewAncestor.get(); - if (viewRoot != null) { - viewRoot.dispatchCloseSystemDialogs(reason); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchCloseSystemDialogs(reason); } } @@ -3720,7 +3781,7 @@ public final class ViewAncestor extends Handler implements ViewParent, } } } - + public void dispatchWallpaperCommand(String action, int x, int y, int z, Bundle extras, boolean sync) { if (sync) { @@ -3733,17 +3794,16 @@ public final class ViewAncestor extends Handler implements ViewParent, /* Drag/drop */ public void dispatchDragEvent(DragEvent event) { - final ViewAncestor viewRoot = mViewAncestor.get(); - if (viewRoot != null) { - viewRoot.dispatchDragEvent(event); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchDragEvent(event); } } - @Override public void dispatchSystemUiVisibilityChanged(int visibility) { - final ViewAncestor viewRoot = mViewAncestor.get(); - if (viewRoot != null) { - viewRoot.dispatchSystemUiVisibilityChanged(visibility); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchSystemUiVisibilityChanged(visibility); } } } @@ -4053,5 +4113,395 @@ public final class ViewAncestor extends Handler implements ViewParent, } } + /** + * Class for managing the accessibility interaction connection + * based on the global accessibility state. + */ + final class AccessibilityInteractionConnectionManager + implements AccessibilityStateChangeListener { + public void onAccessibilityStateChanged(boolean enabled) { + if (enabled) { + ensureConnection(); + } else { + ensureNoConnection(); + } + } + + public void ensureConnection() { + final boolean registered = mAttachInfo.mAccessibilityWindowId != View.NO_ID; + if (!registered) { + mAttachInfo.mAccessibilityWindowId = + mAccessibilityManager.addAccessibilityInteractionConnection(mWindow, + new AccessibilityInteractionConnection(ViewAncestor.this)); + } + } + + public void ensureNoConnection() { + final boolean registered = mAttachInfo.mAccessibilityWindowId != View.NO_ID; + if (registered) { + mAttachInfo.mAccessibilityWindowId = View.NO_ID; + mAccessibilityManager.removeAccessibilityInteractionConnection(mWindow); + } + } + } + + /** + * This class is an interface this ViewAncestor provides to the + * AccessibilityManagerService to the latter can interact with + * the view hierarchy in this ViewAncestor. + */ + final class AccessibilityInteractionConnection + extends IAccessibilityInteractionConnection.Stub { + private final WeakReference<ViewAncestor> mViewAncestor; + + AccessibilityInteractionConnection(ViewAncestor viewAncestor) { + mViewAncestor = new WeakReference<ViewAncestor>(viewAncestor); + } + + public void findAccessibilityNodeInfoByAccessibilityId(int accessibilityId, + int interactionId, IAccessibilityInteractionConnectionCallback callback) { + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor == null) { + return; + } + getAccessibilityInteractionController() + .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityId, + interactionId, callback); + } + + public void performAccessibilityAction(int accessibilityId, int action, + int interactionId, IAccessibilityInteractionConnectionCallback callback) { + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor == null) { + return; + } + getAccessibilityInteractionController() + .performAccessibilityActionClientThread(accessibilityId, action, interactionId, + callback); + } + + public void findAccessibilityNodeInfoByViewId(int viewId, + int interactionId, IAccessibilityInteractionConnectionCallback callback) { + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor == null) { + return; + } + getAccessibilityInteractionController() + .findAccessibilityNodeInfoByViewIdClientThread(viewId, interactionId, callback); + } + + public void findAccessibilityNodeInfosByViewText(String text, int interactionId, + IAccessibilityInteractionConnectionCallback callback) { + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor == null) { + return; + } + getAccessibilityInteractionController() + .findAccessibilityNodeInfosByViewTextClientThread(text, interactionId, callback); + } + } + + /** + * Class for managing accessibility interactions initiated from the system + * and targeting the view hierarchy. A *ClientThread method is to be + * called from the interaction connection this ViewAncestor gives the + * system to talk to it and a corresponding *UiThread method that is executed + * on the UI thread. + */ + final class AccessibilityInteractionController { + private static final int POOL_SIZE = 5; + + private FindByAccessibilitytIdPredicate mFindByAccessibilityIdPredicate = + new FindByAccessibilitytIdPredicate(); + + private ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList = + new ArrayList<AccessibilityNodeInfo>(); + + // Reusable poolable arguments for interacting with the view hierarchy + // to fit more arguments than Message and to avoid sharing objects between + // two messages since several threads can send messages concurrently. + private final Pool<SomeArgs> mPool = Pools.synchronizedPool(Pools.finitePool( + new PoolableManager<SomeArgs>() { + public SomeArgs newInstance() { + return new SomeArgs(); + } + + public void onAcquired(SomeArgs info) { + /* do nothing */ + } + + public void onReleased(SomeArgs info) { + info.clear(); + } + }, POOL_SIZE) + ); + + public class SomeArgs implements Poolable<SomeArgs> { + private SomeArgs mNext; + private boolean mIsPooled; + + public Object arg1; + public Object arg2; + public int argi1; + public int argi2; + public int argi3; + + public SomeArgs getNextPoolable() { + return mNext; + } + + public boolean isPooled() { + return mIsPooled; + } + + public void setNextPoolable(SomeArgs args) { + mNext = args; + } + + public void setPooled(boolean isPooled) { + mIsPooled = isPooled; + } + + private void clear() { + arg1 = null; + arg2 = null; + argi1 = 0; + argi2 = 0; + argi3 = 0; + } + } + + public void findAccessibilityNodeInfoByAccessibilityIdClientThread(int accessibilityId, + int interactionId, IAccessibilityInteractionConnectionCallback callback) { + Message message = Message.obtain(); + message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID; + message.arg1 = accessibilityId; + message.arg2 = interactionId; + message.obj = callback; + sendMessage(message); + } + + public void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) { + final int accessibilityId = message.arg1; + final int interactionId = message.arg2; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) message.obj; + + View root = ViewAncestor.this.mView; + if (root == null) { + return; + } + + FindByAccessibilitytIdPredicate predicate = mFindByAccessibilityIdPredicate; + predicate.init(accessibilityId); + + View target = root.findViewByPredicate(predicate); + if (target != null) { + AccessibilityNodeInfo info = target.createAccessibilityNodeInfo(); + try { + callback.setFindAccessibilityNodeInfoResult(info, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void findAccessibilityNodeInfoByViewIdClientThread(int viewId, int interactionId, + IAccessibilityInteractionConnectionCallback callback) { + Message message = Message.obtain(); + message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID; + message.arg1 = viewId; + message.arg2 = interactionId; + message.obj = callback; + sendMessage(message); + } + + public void findAccessibilityNodeInfoByViewIdUiThread(Message message) { + final int viewId = message.arg1; + final int interactionId = message.arg2; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) message.obj; + + View root = ViewAncestor.this.mView; + if (root == null) { + return; + } + View target = root.findViewById(viewId); + if (target != null) { + AccessibilityNodeInfo info = target.createAccessibilityNodeInfo(); + try { + callback.setFindAccessibilityNodeInfoResult(info, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void findAccessibilityNodeInfosByViewTextClientThread(String text, int interactionId, + IAccessibilityInteractionConnectionCallback callback) { + Message message = Message.obtain(); + message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT; + SomeArgs args = mPool.acquire(); + args.arg1 = text; + args.argi1 = interactionId; + args.arg2 = callback; + message.obj = args; + sendMessage(message); + } + + public void findAccessibilityNodeInfosByViewTextUiThread(Message message) { + SomeArgs args = (SomeArgs) message.obj; + final String text = (String) args.arg1; + final int interactionId = args.argi1; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg2; + mPool.release(args); + + View root = ViewAncestor.this.mView; + if (root == null) { + return; + } + + ArrayList<View> foundViews = mAttachInfo.mFocusablesTempList; + foundViews.clear(); + + root.findViewsWithText(foundViews, text); + if (foundViews.isEmpty()) { + return; + } + + List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList; + infos.clear(); + + final int viewCount = foundViews.size(); + for (int i = 0; i < viewCount; i++) { + View foundView = foundViews.get(i); + infos.add(foundView.createAccessibilityNodeInfo()); + } + + try { + callback.setFindAccessibilityNodeInfosResult(infos, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + + public void performAccessibilityActionClientThread(int accessibilityId, int action, + int interactionId, IAccessibilityInteractionConnectionCallback callback) { + Message message = Message.obtain(); + message.what = DO_PERFORM_ACCESSIBILITY_ACTION; + SomeArgs args = mPool.acquire(); + args.argi1 = accessibilityId; + args.argi2 = action; + args.argi3 = interactionId; + args.arg1 = callback; + message.obj = args; + sendMessage(message); + } + + public void perfromAccessibilityActionUiThread(Message message) { + SomeArgs args = (SomeArgs) message.obj; + final int accessibilityId = args.argi1; + final int action = args.argi2; + final int interactionId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + + if (ViewAncestor.this.mView == null) { + return; + } + + boolean succeeded = false; + switch (action) { + case AccessibilityNodeInfo.ACTION_FOCUS: { + succeeded = performActionFocus(accessibilityId); + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { + succeeded = performActionClearFocus(accessibilityId); + } break; + case AccessibilityNodeInfo.ACTION_SELECT: { + succeeded = performActionSelect(accessibilityId); + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: { + succeeded = performActionClearSelection(accessibilityId); + } break; + } + + try { + callback.setPerformAccessibilityActionResult(succeeded, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + + private boolean performActionFocus(int accessibilityId) { + View target = findViewByAccessibilityId(accessibilityId); + if (target == null) { + return false; + } + // Get out of touch mode since accessibility wants to move focus around. + ensureTouchMode(false); + return target.requestFocus(); + } + + private boolean performActionClearFocus(int accessibilityId) { + View target = findViewByAccessibilityId(accessibilityId); + if (target == null) { + return false; + } + if (!target.isFocused()) { + return false; + } + target.clearFocus(); + return !target.isFocused(); + } + + private boolean performActionSelect(int accessibilityId) { + View target = findViewByAccessibilityId(accessibilityId); + if (target == null) { + return false; + } + if (target.isSelected()) { + return false; + } + target.setSelected(true); + return target.isSelected(); + } + + private boolean performActionClearSelection(int accessibilityId) { + View target = findViewByAccessibilityId(accessibilityId); + if (target == null) { + return false; + } + if (!target.isSelected()) { + return false; + } + target.setSelected(false); + return !target.isSelected(); + } + + private View findViewByAccessibilityId(int accessibilityId) { + View root = ViewAncestor.this.mView; + if (root == null) { + return null; + } + mFindByAccessibilityIdPredicate.init(accessibilityId); + return root.findViewByPredicate(mFindByAccessibilityIdPredicate); + } + + private final class FindByAccessibilitytIdPredicate implements Predicate<View> { + public int mSerchedId; + + public void init(int searchedId) { + mSerchedId = searchedId; + } + + public boolean apply(View view) { + return (view.getAccessibilityViewId() == mSerchedId); + } + } + } + private static native void nativeShowFPS(Canvas canvas, int durationMillis); } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index f504b90..57ee8a0 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -35,6 +35,7 @@ import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -772,6 +773,18 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + @Override + public void findViewsWithText(ArrayList<View> outViews, CharSequence text) { + final int childrenCount = mChildrenCount; + final View[] children = mChildren; + for (int i = 0; i < childrenCount; i++) { + View child = children[i]; + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { + child.findViewsWithText(outViews, text); + } + } + } + /** * {@inheritDoc} */ @@ -2007,6 +2020,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return false; } + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + + for (int i = 0, count = mChildrenCount; i < count; i++) { + View child = mChildren[i]; + info.addChild(child); + } + } + /** * {@inheritDoc} */ diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index 7b80797..32bfa2f 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -16,9 +16,12 @@ package android.view.accessibility; +import android.accessibilityservice.IAccessibilityServiceConnection; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; import android.text.TextUtils; +import android.view.View; import java.util.ArrayList; @@ -159,6 +162,7 @@ import java.util.ArrayList; * @see android.accessibilityservice.AccessibilityService */ public final class AccessibilityEvent extends AccessibilityRecord implements Parcelable { + private static final boolean DEBUG = false; /** * Invalid selection/focus position. @@ -256,21 +260,38 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par private static final Object sPoolLock = new Object(); private static AccessibilityEvent sPool; private static int sPoolSize; - private AccessibilityEvent mNext; private boolean mIsInPool; private int mEventType; + private int mSourceAccessibilityViewId; + private int mSourceAccessibilityWindowId; private CharSequence mPackageName; private long mEventTime; private final ArrayList<AccessibilityRecord> mRecords = new ArrayList<AccessibilityRecord>(); + private IAccessibilityServiceConnection mConnection; + /* * Hide constructor from clients. */ private AccessibilityEvent() { + } + /** + * Initialize an event from another one. + * + * @param event The event to initialize from. + */ + void init(AccessibilityEvent event) { + super.init(event); + mEventType = event.mEventType; + mEventTime = event.mEventTime; + mSourceAccessibilityWindowId = event.mSourceAccessibilityWindowId; + mSourceAccessibilityViewId = event.mSourceAccessibilityViewId; + mPackageName = event.mPackageName; + mConnection = event.mConnection; } /** @@ -286,8 +307,11 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * Appends an {@link AccessibilityRecord} to the end of event records. * * @param record The record to append. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void appendRecord(AccessibilityRecord record) { + enforceNotSealed(); mRecords.add(record); } @@ -311,11 +335,89 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } /** + * Sets the event source. + * + * @param source The source. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setSource(View source) { + enforceNotSealed(); + if (source != null) { + mSourceAccessibilityWindowId = source.getAccessibilityWindowId(); + mSourceAccessibilityViewId = source.getAccessibilityViewId(); + } else { + mSourceAccessibilityWindowId = View.NO_ID; + mSourceAccessibilityViewId = View.NO_ID; + } + } + + /** + * Gets the {@link AccessibilityNodeInfo} of the event source. + * <p> + * <strong> + * It is a client responsibility to recycle the received info by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * @return The info. + */ + public AccessibilityNodeInfo getSource() { + enforceSealed(); + if (mSourceAccessibilityWindowId == View.NO_ID + || mSourceAccessibilityViewId == View.NO_ID) { + return null; + } + try { + return mConnection.findAccessibilityNodeInfoByAccessibilityId( + mSourceAccessibilityWindowId, mSourceAccessibilityViewId); + } catch (RemoteException e) { + return null; + } + } + + /** + * Gets the id of the window from which the event comes from. + * + * @return The window id. + */ + public int getAccessibilityWindowId() { + return mSourceAccessibilityWindowId; + } + + /** + * Sets the client token for the accessibility service that + * provided this node info. + * + * @param connection The connection. + * + * @hide + */ + public final void setConnection(IAccessibilityServiceConnection connection) { + mConnection = connection; + } + + /** + * Gets the accessibility window id of the source window. + * + * @return The id. + * + * @hide + */ + public int getSourceAccessibilityWindowId() { + return mSourceAccessibilityWindowId; + } + + /** * Sets the event type. * * @param eventType The event type. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setEventType(int eventType) { + enforceNotSealed(); mEventType = eventType; } @@ -332,8 +434,11 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * Sets the time in which this event was sent. * * @param eventTime The event time. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setEventTime(long eventTime) { + enforceNotSealed(); mEventTime = eventTime; } @@ -350,8 +455,11 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * Sets the package name of the source. * * @param packageName The package name. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setPackageName(CharSequence packageName) { + enforceNotSealed(); mPackageName = packageName; } @@ -370,6 +478,27 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par /** * Returns a cached instance if such is available or a new one is + * instantiated with type property set. + * + * @param event The other event. + * @return An instance. + */ + public static AccessibilityEvent obtain(AccessibilityEvent event) { + AccessibilityEvent eventClone = AccessibilityEvent.obtain(); + eventClone.init(event); + + final int recordCount = event.mRecords.size(); + for (int i = 0; i < recordCount; i++) { + AccessibilityRecord record = event.mRecords.get(i); + AccessibilityRecord recordClone = AccessibilityRecord.obtain(record); + eventClone.mRecords.add(recordClone); + } + + return eventClone; + } + + /** + * Returns a cached instance if such is available or a new one is * instantiated. * * @return An instance. @@ -413,11 +542,16 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par /** * Clears the state of this instance. + * + * @hide */ @Override protected void clear() { super.clear(); + mConnection = null; mEventType = 0; + mSourceAccessibilityViewId = View.NO_ID; + mSourceAccessibilityWindowId = View.NO_ID; mPackageName = null; mEventTime = 0; while (!mRecords.isEmpty()) { @@ -432,7 +566,14 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * @param parcel A parcel containing the state of a {@link AccessibilityEvent}. */ public void initFromParcel(Parcel parcel) { + if (parcel.readInt() == 1) { + mConnection = IAccessibilityServiceConnection.Stub.asInterface( + parcel.readStrongBinder()); + } + setSealed(parcel.readInt() == 1); mEventType = parcel.readInt(); + mSourceAccessibilityWindowId = parcel.readInt(); + mSourceAccessibilityViewId = parcel.readInt(); mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); mEventTime = parcel.readLong(); readAccessibilityRecordFromParcel(this, parcel); @@ -471,7 +612,16 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * {@inheritDoc} */ public void writeToParcel(Parcel parcel, int flags) { + if (mConnection == null) { + parcel.writeInt(0); + } else { + parcel.writeInt(1); + parcel.writeStrongBinder(mConnection.asBinder()); + } + parcel.writeInt(isSealed() ? 1 : 0); parcel.writeInt(mEventType); + parcel.writeInt(mSourceAccessibilityWindowId); + parcel.writeInt(mSourceAccessibilityViewId); TextUtils.writeToParcel(mPackageName, parcel, 0); parcel.writeLong(mEventTime); writeAccessibilityRecordToParcel(this, parcel, flags); @@ -519,18 +669,36 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par builder.append("; EventType: ").append(eventTypeToString(mEventType)); builder.append("; EventTime: ").append(mEventTime); builder.append("; PackageName: ").append(mPackageName); - builder.append(" \n{\n"); builder.append(super.toString()); - builder.append("\n"); - for (int i = 0; i < mRecords.size(); i++) { - AccessibilityRecord record = mRecords.get(i); - builder.append(" Record "); - builder.append(i); - builder.append(":"); - builder.append(record.toString()); + if (DEBUG) { builder.append("\n"); + builder.append("; sourceAccessibilityWindowId: ").append(mSourceAccessibilityWindowId); + builder.append("; sourceAccessibilityViewId: ").append(mSourceAccessibilityViewId); + for (int i = 0; i < mRecords.size(); i++) { + AccessibilityRecord record = mRecords.get(i); + builder.append(" Record "); + builder.append(i); + builder.append(":"); + builder.append(" [ ClassName: " + record.mClassName); + builder.append("; Text: " + record.mText); + builder.append("; ContentDescription: " + record.mContentDescription); + builder.append("; ItemCount: " + record.mItemCount); + builder.append("; CurrentItemIndex: " + record.mCurrentItemIndex); + builder.append("; IsEnabled: " + record.isEnabled()); + builder.append("; IsPassword: " + record.isPassword()); + builder.append("; IsChecked: " + record.isChecked()); + builder.append("; IsFullScreen: " + record.isFullScreen()); + builder.append("; BeforeText: " + record.mBeforeText); + builder.append("; FromIndex: " + record.mFromIndex); + builder.append("; AddedCount: " + record.mAddedCount); + builder.append("; RemovedCount: " + record.mRemovedCount); + builder.append("; ParcelableData: " + record.mParcelableData); + builder.append(" ]"); + builder.append("\n"); + } + } else { + builder.append("; recordCount: ").append(getAddedCount()); } - builder.append("}\n"); return builder.toString(); } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 88f8878..eece64a 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -28,10 +28,13 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.util.Log; +import android.view.IWindow; +import android.view.View; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; /** * System level service that serves as an event dispatch for {@link AccessibilityEvent}s. @@ -62,6 +65,21 @@ public final class AccessibilityManager { boolean mIsEnabled; + final CopyOnWriteArrayList<AccessibilityStateChangeListener> mAccessibilityStateChangeListeners = + new CopyOnWriteArrayList<AccessibilityStateChangeListener>(); + + /** + * Listener for the accessibility state. + */ + public interface AccessibilityStateChangeListener { + /** + * Called back on change in the accessibility state. + * + * @param enabled + */ + public void onAccessibilityStateChanged(boolean enabled); + } + final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { public void setEnabled(boolean enabled) { mHandler.obtainMessage(DO_SET_ENABLED, enabled ? 1 : 0, 0).sendToTarget(); @@ -78,9 +96,8 @@ public final class AccessibilityManager { public void handleMessage(Message message) { switch (message.what) { case DO_SET_ENABLED : - synchronized (mHandler) { - mIsEnabled = (message.arg1 == 1); - } + final boolean isEnabled = (message.arg1 == 1); + setAccessibilityState(isEnabled); return; default : Log.w(LOG_TAG, "Unknown message type: " + message.what); @@ -117,7 +134,8 @@ public final class AccessibilityManager { mService = service; try { - mIsEnabled = mService.addClient(mClient); + final boolean isEnabled = mService.addClient(mClient); + setAccessibilityState(isEnabled); } catch (RemoteException re) { Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); } @@ -253,4 +271,81 @@ public final class AccessibilityManager { } return Collections.unmodifiableList(services); } + + /** + * Registers an {@link AccessibilityStateChangeListener}. + * + * @param listener The listener. + * @return True if successfully registered. + */ + public boolean addAccessibilityStateChangeListener( + AccessibilityStateChangeListener listener) { + return mAccessibilityStateChangeListeners.add(listener); + } + + /** + * Unregisters an {@link AccessibilityStateChangeListener}. + * + * @param listener The listener. + * @return True if successfully unregistered. + */ + public boolean removeAccessibilityStateChangeListener( + AccessibilityStateChangeListener listener) { + return mAccessibilityStateChangeListeners.remove(listener); + } + + /** + * Sets the enabled state. + * + * @param isEnabled The accessibility state. + */ + private void setAccessibilityState(boolean isEnabled) { + synchronized (mHandler) { + if (isEnabled != mIsEnabled) { + mIsEnabled = isEnabled; + notifyAccessibilityStateChanged(); + } + } + } + + /** + * Notifies the registered {@link AccessibilityStateChangeListener}s. + */ + private void notifyAccessibilityStateChanged() { + final int listenerCount = mAccessibilityStateChangeListeners.size(); + for (int i = 0; i < listenerCount; i++) { + mAccessibilityStateChangeListeners.get(i).onAccessibilityStateChanged(mIsEnabled); + } + } + + /** + * Adds an accessibility interaction connection interface for a given window. + * @param windowToken The window token to which a connection is added. + * @param connection The connection. + * + * @hide + */ + public int addAccessibilityInteractionConnection(IWindow windowToken, + IAccessibilityInteractionConnection connection) { + try { + return mService.addAccessibilityInteractionConnection(windowToken, connection); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); + } + return View.NO_ID; + } + + /** + * Removed an accessibility interaction connection interface for a given window. + * @param windowToken The window token to which a connection is removed. + * + * @hide + */ + public void removeAccessibilityInteractionConnection(IWindow windowToken) { + try { + mService.removeAccessibilityInteractionConnection(windowToken); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re); + } + } } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.aidl b/core/java/android/view/accessibility/AccessibilityNodeInfo.aidl new file mode 100644 index 0000000..59175ce --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 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; + +parcelable AccessibilityNodeInfo; diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java new file mode 100644 index 0000000..752f864 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -0,0 +1,947 @@ +/* + * Copyright (C) 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.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.view.View; + +/** + * This class represents a node of the screen content. From the point of + * view of an accessibility service the screen content is presented as tree + * of accessibility nodes. + * + * TODO(svertoslavganov): Update the documentation, add sample, and describe + * the security policy. + */ +public class AccessibilityNodeInfo implements Parcelable { + + private static final boolean DEBUG = false; + + // Actions. + + /** + * Action that focuses the node. + */ + public static final int ACTION_FOCUS = 0x00000001; + + /** + * Action that unfocuses the node. + */ + public static final int ACTION_CLEAR_FOCUS = 0x00000002; + + /** + * Action that selects the node. + */ + public static final int ACTION_SELECT = 0x00000004; + + /** + * Action that unselects the node. + */ + public static final int ACTION_CLEAR_SELECTION = 0x00000008; + + // Boolean attributes. + + private static final int PROPERTY_CHECKABLE = 0x00000001; + + private static final int PROPERTY_CHECKED = 0x00000002; + + private static final int PROPERTY_FOCUSABLE = 0x00000004; + + private static final int PROPERTY_FOCUSED = 0x00000008; + + private static final int PROPERTY_SELECTED = 0x00000010; + + private static final int PROPERTY_CLICKABLE = 0x00000020; + + private static final int PROPERTY_LONG_CLICKABLE = 0x00000040; + + private static final int PROPERTY_ENABLED = 0x00000080; + + private static final int PROPERTY_PASSWORD = 0x00000100; + + // Readable representations - lazily initialized. + private static SparseArray<String> sActionSymbolicNames; + + // Housekeeping. + private static final int MAX_POOL_SIZE = 50; + private static final Object sPoolLock = new Object(); + private static AccessibilityNodeInfo sPool; + private static int sPoolSize; + private AccessibilityNodeInfo mNext; + private boolean mIsInPool; + private boolean mSealed; + + // Data. + private int mAccessibilityViewId; + private int mAccessibilityWindowId; + private int mParentAccessibilityViewId; + private int mBooleanProperties; + private final Rect mBounds = new Rect(); + + private CharSequence mPackageName; + private CharSequence mClassName; + private CharSequence mText; + private CharSequence mContentDescription; + + private final SparseIntArray mChildAccessibilityIds = new SparseIntArray(); + private int mActions; + + private IAccessibilityServiceConnection mConnection; + + /** + * Hide constructor from clients. + */ + private AccessibilityNodeInfo() { + /* do nothing */ + } + + /** + * Sets the source. + * + * @param source The info source. + */ + public void setSource(View source) { + enforceNotSealed(); + mAccessibilityViewId = source.getAccessibilityViewId(); + mAccessibilityWindowId = source.getAccessibilityWindowId(); + } + + /** + * Gets the id of the window from which the info comes from. + * + * @return The window id. + */ + public int getAccessibilityWindowId() { + return mAccessibilityWindowId; + } + + /** + * Gets the number of children. + * + * @return The child count. + */ + public int getChildCount() { + return mChildAccessibilityIds.size(); + } + + /** + * Get the child at given index. + * <p> + * <strong> + * It is a client responsibility to recycle the received info by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * @param index The child index. + * @return The child node. + * + * @throws IllegalStateException If called outside of an AccessibilityService. + * + */ + public AccessibilityNodeInfo getChild(int index) { + enforceSealed(); + final int childAccessibilityViewId = mChildAccessibilityIds.get(index); + try { + return mConnection.findAccessibilityNodeInfoByAccessibilityId(mAccessibilityWindowId, + childAccessibilityViewId); + } catch (RemoteException e) { + return null; + } + } + + /** + * Adds a child. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param child The child. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void addChild(View child) { + enforceNotSealed(); + final int childAccessibilityViewId = child.getAccessibilityViewId(); + final int index = mChildAccessibilityIds.size(); + mChildAccessibilityIds.put(index, childAccessibilityViewId); + } + + /** + * Gets the actions that can be performed on the node. + * + * @return The bit mask of with actions. + * + * @see AccessibilityNodeInfo#ACTION_FOCUS + * @see AccessibilityNodeInfo#ACTION_CLEAR_FOCUS + * @see AccessibilityNodeInfo#ACTION_SELECT + * @see AccessibilityNodeInfo#ACTION_CLEAR_SELECTION + */ + public int getActions() { + return mActions; + } + + /** + * Adds an action that can be performed on the node. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param action The action. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void addAction(int action) { + enforceNotSealed(); + mActions |= action; + } + + /** + * Performs an action on the node. + * <p> + * Note: An action can be performed only if the request is made + * from an {@link android.accessibilityservice.AccessibilityService}. + * </p> + * @param action The action to perform. + * @return True if the action was performed. + * + * @throws IllegalStateException If called outside of an AccessibilityService. + */ + public boolean performAction(int action) { + enforceSealed(); + try { + return mConnection.performAccessibilityAction(mAccessibilityWindowId, + mAccessibilityViewId, action); + } catch (RemoteException e) { + return false; + } + } + + /** + * Gets the unique id identifying this node's parent. + * <p> + * <strong> + * It is a client responsibility to recycle the received info by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * @return The node's patent id. + */ + public AccessibilityNodeInfo getParent() { + enforceSealed(); + try { + return mConnection.findAccessibilityNodeInfoByAccessibilityId(mAccessibilityWindowId, + mParentAccessibilityViewId); + } catch (RemoteException e) { + return null; + } + } + + /** + * Sets the parent. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param parent The parent. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setParent(View parent) { + enforceNotSealed(); + mParentAccessibilityViewId = parent.getAccessibilityViewId(); + } + + /** + * Gets the node bounds in parent coordinates. + * + * @param outBounds The output node bounds. + */ + public void getBounds(Rect outBounds) { + outBounds.set(mBounds.left, mBounds.top, mBounds.right, mBounds.bottom); + } + + /** + * Sets the node bounds in parent coordinates. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param bounds The node bounds. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setBounds(Rect bounds) { + enforceNotSealed(); + mBounds.set(bounds.left, bounds.top, bounds.right, bounds.bottom); + } + + /** + * Gets whether this node is checkable. + * + * @return True if the node is checkable. + */ + public boolean isCheckable() { + return getBooleanProperty(PROPERTY_CHECKABLE); + } + + /** + * Sets whether this node is checkable. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param checkable True if the node is checkable. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setCheckable(boolean checkable) { + setBooleanProperty(PROPERTY_CHECKABLE, checkable); + } + + /** + * Gets whether this node is checked. + * + * @return True if the node is checked. + */ + public boolean isChecked() { + return getBooleanProperty(PROPERTY_CHECKED); + } + + /** + * Sets whether this node is checked. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param checked True if the node is checked. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setChecked(boolean checked) { + setBooleanProperty(PROPERTY_CHECKED, checked); + } + + /** + * Gets whether this node is focusable. + * + * @return True if the node is focusable. + */ + public boolean isFocusable() { + return getBooleanProperty(PROPERTY_FOCUSABLE); + } + + /** + * Sets whether this node is focusable. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param focusable True if the node is focusable. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFocusable(boolean focusable) { + setBooleanProperty(PROPERTY_FOCUSABLE, focusable); + } + + /** + * Gets whether this node is focused. + * + * @return True if the node is focused. + */ + public boolean isFocused() { + return getBooleanProperty(PROPERTY_FOCUSED); + } + + /** + * Sets whether this node is focused. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param focused True if the node is focused. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFocused(boolean focused) { + setBooleanProperty(PROPERTY_FOCUSED, focused); + } + + /** + * Gets whether this node is selected. + * + * @return True if the node is selected. + */ + public boolean isSelected() { + return getBooleanProperty(PROPERTY_SELECTED); + } + + /** + * Sets whether this node is selected. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param selected True if the node is selected. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setSelected(boolean selected) { + setBooleanProperty(PROPERTY_SELECTED, selected); + } + + /** + * Gets whether this node is clickable. + * + * @return True if the node is clickable. + */ + public boolean isClickable() { + return getBooleanProperty(PROPERTY_CLICKABLE); + } + + /** + * Sets whether this node is clickable. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param clickable True if the node is clickable. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setClickable(boolean clickable) { + setBooleanProperty(PROPERTY_CLICKABLE, clickable); + } + + /** + * Gets whether this node is long clickable. + * + * @return True if the node is long clickable. + */ + public boolean isLongClickable() { + return getBooleanProperty(PROPERTY_LONG_CLICKABLE); + } + + /** + * Sets whether this node is long clickable. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param longClickable True if the node is long clickable. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setLongClickable(boolean longClickable) { + setBooleanProperty(PROPERTY_LONG_CLICKABLE, longClickable); + } + + /** + * Gets whether this node is enabled. + * + * @return True if the node is enabled. + */ + public boolean isEnabled() { + return getBooleanProperty(PROPERTY_ENABLED); + } + + /** + * Sets whether this node is enabled. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param enabled True if the node is enabled. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setEnabled(boolean enabled) { + setBooleanProperty(PROPERTY_ENABLED, enabled); + } + + /** + * Gets whether this node is a password. + * + * @return True if the node is a password. + */ + public boolean isPassword() { + return getBooleanProperty(PROPERTY_PASSWORD); + } + + /** + * Sets whether this node is a password. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param password True if the node is a password. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setPassword(boolean password) { + setBooleanProperty(PROPERTY_PASSWORD, password); + } + + /** + * Gets the package this node comes from. + * + * @return The package name. + */ + public CharSequence getPackageName() { + return mPackageName; + } + + /** + * Sets the package this node comes from. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param packageName The package name. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setPackageName(CharSequence packageName) { + enforceNotSealed(); + mPackageName = packageName; + } + + /** + * Gets the class this node comes from. + * + * @return The class name. + */ + public CharSequence getClassName() { + return mClassName; + } + + /** + * Sets the class this node comes from. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param className The class name. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setClassName(CharSequence className) { + enforceNotSealed(); + mClassName = className; + } + + /** + * Gets the text of this node. + * + * @return The text. + */ + public CharSequence getText() { + return mText; + } + + /** + * Sets the text of this node. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param text The text. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setText(CharSequence text) { + enforceNotSealed(); + mText = text; + } + + /** + * Gets the content description of this node. + * + * @return The content description. + */ + public CharSequence getContentDescription() { + return mContentDescription; + } + + /** + * Sets the content description of this node. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param contentDescription The content description. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setContentDescription(CharSequence contentDescription) { + enforceNotSealed(); + mContentDescription = contentDescription; + } + + /** + * Gets the value of a boolean property. + * + * @param property The property. + * @return The value. + */ + private boolean getBooleanProperty(int property) { + return (mBooleanProperties & property) != 0; + } + + /** + * Sets a boolean property. + * + * @param property The property. + * @param value The value. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + private void setBooleanProperty(int property, boolean value) { + enforceNotSealed(); + if (value) { + mBooleanProperties |= property; + } else { + mBooleanProperties &= ~property; + } + } + + /** + * Sets the connection for interacting with the system. + * + * @param connection The client token. + * + * @hide + */ + public final void setConnection(IAccessibilityServiceConnection connection) { + mConnection = connection; + } + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * Sets if this instance is sealed. + * + * @param sealed Whether is sealed. + * + * @hide + */ + public void setSealed(boolean sealed) { + mSealed = sealed; + } + + /** + * Gets if this instance is sealed. + * + * @return Whether is sealed. + * + * @hide + */ + public boolean isSealed() { + return mSealed; + } + + /** + * Enforces that this instance is sealed. + * + * @throws IllegalStateException If this instance is not sealed. + * + * @hide + */ + protected void enforceSealed() { + if (!isSealed()) { + throw new IllegalStateException("Cannot perform this " + + "action on a not sealed instance."); + } + } + + /** + * Enforces that this instance is not sealed. + * + * @throws IllegalStateException If this instance is sealed. + * + * @hide + */ + protected void enforceNotSealed() { + if (isSealed()) { + throw new IllegalStateException("Cannot perform this " + + "action on an sealed instance."); + } + } + + /** + * Returns a cached instance if such is available otherwise a new one + * and sets the source. + * + * @return An instance. + * + * @see #setSource(View) + */ + public static AccessibilityNodeInfo obtain(View source) { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + info.setSource(source); + return info; + } + + /** + * Returns a cached instance if such is available otherwise a new one. + * + * @return An instance. + */ + public static AccessibilityNodeInfo obtain() { + synchronized (sPoolLock) { + if (sPool != null) { + AccessibilityNodeInfo info = sPool; + sPool = sPool.mNext; + sPoolSize--; + info.mNext = null; + info.mIsInPool = false; + } + return new AccessibilityNodeInfo(); + } + } + + /** + * Return an instance back to be reused. + * <p> + * <b>Note: You must not touch the object after calling this function.</b> + * + * @throws IllegalStateException If the info is already recycled. + */ + public void recycle() { + if (mIsInPool) { + throw new IllegalStateException("Info already recycled!"); + } + clear(); + synchronized (sPoolLock) { + if (sPoolSize <= MAX_POOL_SIZE) { + mNext = sPool; + sPool = this; + mIsInPool = true; + sPoolSize++; + } + } + } + + /** + * {@inheritDoc} + * <p> + * <b>Note: After the instance is written to a parcel it is recycled. + * You must not touch the object after calling this function.</b> + * </p> + */ + public void writeToParcel(Parcel parcel, int flags) { + if (mConnection == null) { + parcel.writeInt(0); + } else { + parcel.writeInt(1); + parcel.writeStrongBinder(mConnection.asBinder()); + } + parcel.writeInt(isSealed() ? 1 : 0); + parcel.writeInt(mAccessibilityViewId); + parcel.writeInt(mAccessibilityWindowId); + parcel.writeInt(mParentAccessibilityViewId); + + SparseIntArray childIds = mChildAccessibilityIds; + final int childIdsSize = childIds.size(); + parcel.writeInt(childIdsSize); + for (int i = 0; i < childIdsSize; i++) { + parcel.writeInt(childIds.valueAt(i)); + } + + parcel.writeInt(mBounds.top); + parcel.writeInt(mBounds.bottom); + parcel.writeInt(mBounds.left); + parcel.writeInt(mBounds.right); + + parcel.writeInt(mActions); + + parcel.writeInt(mBooleanProperties); + + TextUtils.writeToParcel(mPackageName, parcel, flags); + TextUtils.writeToParcel(mClassName, parcel, flags); + TextUtils.writeToParcel(mText, parcel, flags); + TextUtils.writeToParcel(mContentDescription, parcel, flags); + + // Since instances of this class are fetched via synchronous i.e. blocking + // calls in IPCs and we always recycle as soon as the instance is marshaled. + recycle(); + } + + /** + * Creates a new instance from a {@link Parcel}. + * + * @param parcel A parcel containing the state of a {@link AccessibilityNodeInfo}. + */ + private void initFromParcel(Parcel parcel) { + if (parcel.readInt() == 1) { + mConnection = IAccessibilityServiceConnection.Stub.asInterface( + parcel.readStrongBinder()); + } + mSealed = (parcel.readInt() == 1); + mAccessibilityViewId = parcel.readInt(); + mAccessibilityWindowId = parcel.readInt(); + mParentAccessibilityViewId = parcel.readInt(); + + SparseIntArray childIds = mChildAccessibilityIds; + final int childrenSize = parcel.readInt(); + for (int i = 0; i < childrenSize; i++) { + final int childId = parcel.readInt(); + childIds.put(i, childId); + } + + mBounds.top = parcel.readInt(); + mBounds.bottom = parcel.readInt(); + mBounds.left = parcel.readInt(); + mBounds.right = parcel.readInt(); + + mActions = parcel.readInt(); + + mBooleanProperties = parcel.readInt(); + + mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + mClassName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + mText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + } + + /** + * Clears the state of this instance. + */ + private void clear() { + mSealed = false; + mConnection = null; + mAccessibilityViewId = View.NO_ID; + mParentAccessibilityViewId = View.NO_ID; + mChildAccessibilityIds.clear(); + mBounds.set(0, 0, 0, 0); + mBooleanProperties = 0; + mPackageName = null; + mClassName = null; + mText = null; + mContentDescription = null; + mActions = 0; + } + + /** + * Gets the human readable action symbolic name. + * + * @param action The action. + * @return The symbolic name. + */ + private static String getActionSymbolicName(int action) { + SparseArray<String> actionSymbolicNames = sActionSymbolicNames; + if (actionSymbolicNames == null) { + actionSymbolicNames = sActionSymbolicNames = new SparseArray<String>(); + actionSymbolicNames.put(ACTION_FOCUS, "ACTION_FOCUS"); + actionSymbolicNames.put(ACTION_CLEAR_FOCUS, "ACTION_UNFOCUS"); + actionSymbolicNames.put(ACTION_SELECT, "ACTION_SELECT"); + actionSymbolicNames.put(ACTION_CLEAR_SELECTION, "ACTION_UNSELECT"); + } + return actionSymbolicNames.get(action); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mAccessibilityViewId; + result = prime * result + mAccessibilityWindowId; + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(super.toString()); + + if (DEBUG) { + builder.append("; accessibilityId: " + mAccessibilityViewId); + builder.append("; parentAccessibilityId: " + mParentAccessibilityViewId); + SparseIntArray childIds = mChildAccessibilityIds; + builder.append("; childAccessibilityIds: ["); + for (int i = 0, count = childIds.size(); i < count; i++) { + builder.append(childIds.valueAt(i)); + if (i < count - 1) { + builder.append(", "); + } + } + builder.append("]"); + } + + builder.append("; bounds: " + mBounds); + + builder.append("; packageName: ").append(mPackageName); + builder.append("; className: ").append(mClassName); + builder.append("; text: ").append(mText); + builder.append("; contentDescription: ").append(mContentDescription); + + builder.append("; checkable: ").append(isCheckable()); + builder.append("; checked: ").append(isChecked()); + builder.append("; focusable: ").append(isFocusable()); + builder.append("; focused: ").append(isFocused()); + builder.append("; selected: ").append(isSelected()); + builder.append("; clickable: ").append(isClickable()); + builder.append("; longClickable: ").append(isLongClickable()); + builder.append("; enabled: ").append(isEnabled()); + builder.append("; password: ").append(isPassword()); + + builder.append("; ["); + + for (int actionBits = mActions; actionBits != 0;) { + final int action = 1 << Integer.numberOfTrailingZeros(actionBits); + actionBits &= ~action; + builder.append(getActionSymbolicName(action)); + if (actionBits != 0) { + builder.append(", "); + } + } + + builder.append("]"); + + return builder.toString(); + } + + /** + * @see Parcelable.Creator + */ + public static final Parcelable.Creator<AccessibilityNodeInfo> CREATOR = + new Parcelable.Creator<AccessibilityNodeInfo>() { + public AccessibilityNodeInfo createFromParcel(Parcel parcel) { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + info.initFromParcel(parcel); + return info; + } + + public AccessibilityNodeInfo[] newArray(int size) { + return new AccessibilityNodeInfo[size]; + } + }; +} diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java index 7819b17..4bf03a7 100644 --- a/core/java/android/view/accessibility/AccessibilityRecord.java +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -38,13 +38,14 @@ public class AccessibilityRecord { private static final int PROPERTY_PASSWORD = 0x00000004; private static final int PROPERTY_FULL_SCREEN = 0x00000080; + // Housekeeping private static final int MAX_POOL_SIZE = 10; private static final Object sPoolLock = new Object(); private static AccessibilityRecord sPool; private static int sPoolSize; - private AccessibilityRecord mNext; private boolean mIsInPool; + private boolean mSealed; protected int mBooleanProperties; protected int mCurrentItemIndex; @@ -68,6 +69,26 @@ public class AccessibilityRecord { } /** + * Initialize this record from another one. + * + * @param record The to initialize from. + */ + void init(AccessibilityRecord record) { + mSealed = record.isSealed(); + mBooleanProperties = record.mBooleanProperties; + mCurrentItemIndex = record.mCurrentItemIndex; + mItemCount = record.mItemCount; + mFromIndex = record.mFromIndex; + mAddedCount = record.mAddedCount; + mRemovedCount = record.mRemovedCount; + mClassName = record.mClassName; + mContentDescription = record.mContentDescription; + mBeforeText = record.mBeforeText; + mParcelableData = record.mParcelableData; + mText.addAll(record.mText); + } + + /** * Gets if the source is checked. * * @return True if the view is checked, false otherwise. @@ -80,8 +101,11 @@ public class AccessibilityRecord { * Sets if the source is checked. * * @param isChecked True if the view is checked, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setChecked(boolean isChecked) { + enforceNotSealed(); setBooleanProperty(PROPERTY_CHECKED, isChecked); } @@ -98,8 +122,11 @@ public class AccessibilityRecord { * Sets if the source is enabled. * * @param isEnabled True if the view is enabled, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setEnabled(boolean isEnabled) { + enforceNotSealed(); setBooleanProperty(PROPERTY_ENABLED, isEnabled); } @@ -116,27 +143,33 @@ public class AccessibilityRecord { * Sets if the source is a password field. * * @param isPassword True if the view is a password field, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setPassword(boolean isPassword) { + enforceNotSealed(); setBooleanProperty(PROPERTY_PASSWORD, isPassword); } /** - * Sets if the source is taking the entire screen. + * Gets if the source is taking the entire screen. * - * @param isFullScreen True if the source is full screen, false otherwise. + * @return True if the source is full screen, false otherwise. */ - public void setFullScreen(boolean isFullScreen) { - setBooleanProperty(PROPERTY_FULL_SCREEN, isFullScreen); + public boolean isFullScreen() { + return getBooleanProperty(PROPERTY_FULL_SCREEN); } /** - * Gets if the source is taking the entire screen. + * Sets if the source is taking the entire screen. * - * @return True if the source is full screen, false otherwise. + * @param isFullScreen True if the source is full screen, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. */ - public boolean isFullScreen() { - return getBooleanProperty(PROPERTY_FULL_SCREEN); + public void setFullScreen(boolean isFullScreen) { + enforceNotSealed(); + setBooleanProperty(PROPERTY_FULL_SCREEN, isFullScreen); } /** @@ -152,8 +185,11 @@ public class AccessibilityRecord { * Sets the number of items that can be visited. * * @param itemCount The number of items. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setItemCount(int itemCount) { + enforceNotSealed(); mItemCount = itemCount; } @@ -170,8 +206,11 @@ public class AccessibilityRecord { * Sets the index of the source in the list of items that can be visited. * * @param currentItemIndex The current item index. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setCurrentItemIndex(int currentItemIndex) { + enforceNotSealed(); mCurrentItemIndex = currentItemIndex; } @@ -188,8 +227,11 @@ public class AccessibilityRecord { * Sets the index of the first character of the changed sequence. * * @param fromIndex The index of the first character. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setFromIndex(int fromIndex) { + enforceNotSealed(); mFromIndex = fromIndex; } @@ -206,8 +248,11 @@ public class AccessibilityRecord { * Sets the number of added characters. * * @param addedCount The number of added characters. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setAddedCount(int addedCount) { + enforceNotSealed(); mAddedCount = addedCount; } @@ -224,8 +269,11 @@ public class AccessibilityRecord { * Sets the number of removed characters. * * @param removedCount The number of removed characters. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setRemovedCount(int removedCount) { + enforceNotSealed(); mRemovedCount = removedCount; } @@ -242,8 +290,11 @@ public class AccessibilityRecord { * Sets the class name of the source. * * @param className The lass name. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setClassName(CharSequence className) { + enforceNotSealed(); mClassName = className; } @@ -270,8 +321,11 @@ public class AccessibilityRecord { * Sets the text before a change. * * @param beforeText The text before the change. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setBeforeText(CharSequence beforeText) { + enforceNotSealed(); mBeforeText = beforeText; } @@ -288,8 +342,11 @@ public class AccessibilityRecord { * Sets the description of the source. * * @param contentDescription The description. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setContentDescription(CharSequence contentDescription) { + enforceNotSealed(); mContentDescription = contentDescription; } @@ -306,18 +363,71 @@ public class AccessibilityRecord { * Sets the {@link Parcelable} data of the event. * * @param parcelableData The parcelable data. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setParcelableData(Parcelable parcelableData) { + enforceNotSealed(); mParcelableData = parcelableData; } /** + * Sets if this instance is sealed. + * + * @param sealed Whether is sealed. + * + * @hide + */ + public void setSealed(boolean sealed) { + mSealed = sealed; + } + + /** + * Gets if this instance is sealed. + * + * @return Whether is sealed. + * + * @hide + */ + public boolean isSealed() { + return mSealed; + } + + /** + * Enforces that this instance is sealed. + * + * @throws IllegalStateException If this instance is not sealed. + * + * @hide + */ + protected void enforceSealed() { + if (!isSealed()) { + throw new IllegalStateException("Cannot perform this " + + "action on a not sealed instance."); + } + } + + /** + * Enforces that this instance is not sealed. + * + * @throws IllegalStateException If this instance is sealed. + * + * @hide + */ + protected void enforceNotSealed() { + if (isSealed()) { + throw new IllegalStateException("Cannot perform this " + + "action on an sealed instance."); + } + } + + /** * Gets the value of a boolean property. * * @param property The property. * @return The value. */ - public boolean getBooleanProperty(int property) { + private boolean getBooleanProperty(int property) { return (mBooleanProperties & property) == property; } @@ -337,6 +447,19 @@ public class AccessibilityRecord { /** * Returns a cached instance if such is available or a new one is + * instantiated. The instance is initialized with data from the + * given record. + * + * @return An instance. + */ + public static AccessibilityRecord obtain(AccessibilityRecord record) { + AccessibilityRecord clone = AccessibilityRecord.obtain(); + clone.init(record); + return clone; + } + + /** + * Returns a cached instance if such is available or a new one is * instantiated. * * @return An instance. @@ -379,8 +502,11 @@ public class AccessibilityRecord { /** * Clears the state of this instance. + * + * @hide */ protected void clear() { + mSealed = false; mBooleanProperties = 0; mCurrentItemIndex = INVALID_POSITION; mItemCount = 0; diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl new file mode 100644 index 0000000..77dcd07 --- /dev/null +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -0,0 +1,41 @@ +/* + * Copyright (C) 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.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IAccessibilityInteractionConnectionCallback; + +/** + * Interface for interaction between the AccessibilityManagerService + * and the ViewRoot in a given window. + * + * @hide + */ +oneway interface IAccessibilityInteractionConnection { + + void findAccessibilityNodeInfoByAccessibilityId(int accessibilityViewId, int interactionId, + IAccessibilityInteractionConnectionCallback callback); + + void findAccessibilityNodeInfoByViewId(int id, int interactionId, + IAccessibilityInteractionConnectionCallback callback); + + void findAccessibilityNodeInfosByViewText(String text, int interactionId, + IAccessibilityInteractionConnectionCallback callback); + + void performAccessibilityAction(int accessibilityId, int action, int interactionId, + IAccessibilityInteractionConnectionCallback callback); +} diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl new file mode 100644 index 0000000..9c5e8dc --- /dev/null +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl @@ -0,0 +1,36 @@ +/* + * Copyright (C) 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.view.accessibility.AccessibilityNodeInfo; +import java.util.List; + +/** + * Callback for specifying the result for an asynchronous request made + * via calling a method on IAccessibilityInteractionConnectionCallback. + * + * @hide + */ +oneway interface IAccessibilityInteractionConnectionCallback { + + void setFindAccessibilityNodeInfoResult(in AccessibilityNodeInfo info, int interactionId); + + void setFindAccessibilityNodeInfosResult(in List<AccessibilityNodeInfo> infos, + int interactionId); + + void setPerformAccessibilityActionResult(boolean succeeded, int interactionId); +} diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 6b2aae2..b14f02a 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -18,8 +18,13 @@ package android.view.accessibility; import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.IAccessibilityServiceConnection; +import android.accessibilityservice.IEventListener; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityManagerClient; +import android.view.IWindow; /** * Interface implemented by the AccessibilityManagerService called by @@ -38,4 +43,11 @@ interface IAccessibilityManager { List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int feedbackType); void interrupt(); + + int addAccessibilityInteractionConnection(IWindow windowToken, + in IAccessibilityInteractionConnection connection); + + void removeAccessibilityInteractionConnection(IWindow windowToken); + + IAccessibilityServiceConnection registerEventListener(IEventListener client); } diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index 5562bf7..a4771d5 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -1441,6 +1441,7 @@ public class RelativeLayout extends ViewGroup { ); private Node mNext; + private boolean mIsPooled; public void setNextPoolable(Node element) { mNext = element; @@ -1450,6 +1451,14 @@ public class RelativeLayout extends ViewGroup { return mNext; } + public boolean isPooled() { + return mIsPooled; + } + + public void setPooled(boolean isPooled) { + mIsPooled = isPooled; + } + static Node acquire(View view) { final Node node = sPool.acquire(); node.view = view; diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 1d123f6..eba9d37 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -113,6 +113,7 @@ import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AnimationUtils; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; @@ -7647,7 +7648,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener protected int computeVerticalScrollExtent() { return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom(); } - + + @Override + public void findViewsWithText(ArrayList<View> outViews, CharSequence text) { + CharSequence thisText = getText(); + if (TextUtils.isEmpty(thisText)) { + return; + } + if (thisText.toString().toLowerCase().contains(text)) { + outViews.add(this); + } + } + public enum BufferType { NORMAL, SPANNABLE, EDITABLE, } @@ -7899,6 +7911,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener event.setPassword(isPassword); } + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + + final boolean isPassword = hasPasswordTransformationMethod(); + if (!isPassword) { + info.setText(getText()); + } + info.setPassword(isPassword); + } + void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex, int removedCount, int addedCount) { AccessibilityEvent event = diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java index c41b2cb..b9948fe 100644 --- a/core/java/com/android/internal/view/BaseIWindow.java +++ b/core/java/com/android/internal/view/BaseIWindow.java @@ -16,8 +16,6 @@ package com.android.internal.view; -import android.content.ClipData; -import android.content.ClipDescription; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; @@ -26,8 +24,6 @@ import android.os.RemoteException; import android.view.DragEvent; import android.view.IWindow; import android.view.IWindowSession; -import android.view.KeyEvent; -import android.view.MotionEvent; public class BaseIWindow extends IWindow.Stub { private IWindowSession mSession; |