diff options
43 files changed, 3952 insertions, 1265 deletions
@@ -60,7 +60,7 @@ LOCAL_SRC_FILES := $(filter-out \ ## READ ME: ######################################################## LOCAL_SRC_FILES += \ core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl \ - core/java/android/accessibilityservice/IEventListener.aidl \ + core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl \ core/java/android/accounts/IAccountManager.aidl \ core/java/android/accounts/IAccountManagerResponse.aidl \ core/java/android/accounts/IAccountAuthenticator.aidl \ diff --git a/api/current.txt b/api/current.txt index d94adc5..2f8dcb1 100644 --- a/api/current.txt +++ b/api/current.txt @@ -534,6 +534,7 @@ package android { field public static final int imeSubtypeLocale = 16843500; // 0x10102ec field public static final int imeSubtypeMode = 16843501; // 0x10102ed field public static final int immersive = 16843456; // 0x10102c0 + field public static final int importantForAccessibility = 16843699; // 0x10103b3 field public static final int inAnimation = 16843127; // 0x1010177 field public static final int includeFontPadding = 16843103; // 0x101015f field public static final int includeInGlobalSearch = 16843374; // 0x101026e @@ -1993,11 +1994,23 @@ package android.accessibilityservice { public abstract class AccessibilityService extends android.app.Service { ctor public AccessibilityService(); + method public final android.accessibilityservice.AccessibilityServiceInfo getServiceInfo(); method public abstract void onAccessibilityEvent(android.view.accessibility.AccessibilityEvent); method public final android.os.IBinder onBind(android.content.Intent); + method protected void onGesture(int); method public abstract void onInterrupt(); method protected void onServiceConnected(); method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo); + field public static final int GESTURE_CLOCKWISE_CIRCLE = 9; // 0x9 + field public static final int GESTURE_COUNTER_CLOCKWISE_CIRCLE = 10; // 0xa + field public static final int GESTURE_SWIPE_DOWN = 2; // 0x2 + field public static final int GESTURE_SWIPE_DOWN_AND_UP = 8; // 0x8 + field public static final int GESTURE_SWIPE_LEFT = 3; // 0x3 + field public static final int GESTURE_SWIPE_LEFT_AND_RIGHT = 5; // 0x5 + field public static final int GESTURE_SWIPE_RIGHT = 4; // 0x4 + field public static final int GESTURE_SWIPE_RIGHT_AND_LEFT = 6; // 0x6 + field public static final int GESTURE_SWIPE_UP = 1; // 0x1 + field public static final int GESTURE_SWIPE_UP_AND_DOWN = 7; // 0x7 field public static final java.lang.String SERVICE_INTERFACE = "android.accessibilityservice.AccessibilityService"; field public static final java.lang.String SERVICE_META_DATA = "android.accessibilityservice"; } @@ -2022,6 +2035,7 @@ package android.accessibilityservice { field public static final int FEEDBACK_HAPTIC = 2; // 0x2 field public static final int FEEDBACK_SPOKEN = 1; // 0x1 field public static final int FEEDBACK_VISUAL = 8; // 0x8 + field public static final int INCLUDE_NOT_IMPORTANT_VIEWS = 2; // 0x2 field public int eventTypes; field public int feedbackType; field public int flags; @@ -23327,6 +23341,7 @@ package android.view { ctor public View(android.content.Context); ctor public View(android.content.Context, android.util.AttributeSet); ctor public View(android.content.Context, android.util.AttributeSet, int); + method public void addChildrenForAccessibility(java.util.ArrayList<android.view.View>); method public void addFocusables(java.util.ArrayList<android.view.View>, int); method public void addFocusables(java.util.ArrayList<android.view.View>, int, int); method public void addOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener); @@ -23429,6 +23444,7 @@ package android.view { method public int getHorizontalFadingEdgeLength(); method protected int getHorizontalScrollbarHeight(); method public int getId(); + method public int getImportantForAccessibility(); method public boolean getKeepScreenOn(); method public android.view.KeyEvent.DispatcherState getKeyDispatcherState(); method public int getLayerType(); @@ -23462,6 +23478,7 @@ package android.view { method public int getPaddingStart(); method public int getPaddingTop(); method public final android.view.ViewParent getParent(); + method public android.view.ViewParent getParentForAccessibility(); method public float getPivotX(); method public float getPivotY(); method public int getResolvedLayoutDirection(); @@ -23617,6 +23634,7 @@ package android.view { method public void onWindowSystemUiVisibilityChanged(int); method protected void onWindowVisibilityChanged(int); method protected boolean overScrollBy(int, int, int, int, int, int, int, int, boolean); + method public boolean performAccessibilityAction(int); method public boolean performClick(); method public boolean performHapticFeedback(int); method public boolean performHapticFeedback(int, int); @@ -23688,6 +23706,7 @@ package android.view { method public void setHorizontalScrollBarEnabled(boolean); method public void setHovered(boolean); method public void setId(int); + method public void setImportantForAccessibility(int); method public void setKeepScreenOn(boolean); method public void setLayerType(int, android.graphics.Paint); method public void setLayoutDirection(int); @@ -23762,6 +23781,14 @@ package android.view { method protected boolean verifyDrawable(android.graphics.drawable.Drawable); method public boolean willNotCacheDrawing(); method public boolean willNotDraw(); + field public static final int ACCESSIBILITY_FOCUS_BACKWARD = 4097; // 0x1001 + field public static final int ACCESSIBILITY_FOCUS_DOWN = 4226; // 0x1082 + field public static final int ACCESSIBILITY_FOCUS_FORWARD = 4098; // 0x1002 + field public static final int ACCESSIBILITY_FOCUS_IN = 4100; // 0x1004 + field public static final int ACCESSIBILITY_FOCUS_LEFT = 4113; // 0x1011 + field public static final int ACCESSIBILITY_FOCUS_OUT = 4104; // 0x1008 + field public static final int ACCESSIBILITY_FOCUS_RIGHT = 4162; // 0x1042 + field public static final int ACCESSIBILITY_FOCUS_UP = 4129; // 0x1021 field public static final android.util.Property ALPHA; field public static final int DRAWING_CACHE_QUALITY_AUTO = 0; // 0x0 field public static final int DRAWING_CACHE_QUALITY_HIGH = 1048576; // 0x100000 @@ -23783,6 +23810,7 @@ package android.view { field protected static final int[] FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET; field protected static final int[] FOCUSED_STATE_SET; field protected static final int[] FOCUSED_WINDOW_FOCUSED_STATE_SET; + field public static final int FOCUS_ACCESSIBILITY = 4096; // 0x1000 field public static final int FOCUS_BACKWARD = 1; // 0x1 field public static final int FOCUS_DOWN = 130; // 0x82 field public static final int FOCUS_FORWARD = 2; // 0x2 @@ -23791,6 +23819,9 @@ package android.view { field public static final int FOCUS_UP = 33; // 0x21 field public static final int GONE = 8; // 0x8 field public static final int HAPTIC_FEEDBACK_ENABLED = 268435456; // 0x10000000 + field public static final int IMPORTANT_FOR_ACCESSIBILITY_AUTO = 0; // 0x0 + field public static final int IMPORTANT_FOR_ACCESSIBILITY_NO = 2; // 0x2 + field public static final int IMPORTANT_FOR_ACCESSIBILITY_YES = 1; // 0x1 field public static final int INVISIBLE = 4; // 0x4 field public static final int KEEP_SCREEN_ON = 67108864; // 0x4000000 field public static final int LAYER_TYPE_HARDWARE = 2; // 0x2 @@ -24215,6 +24246,7 @@ package android.view { method public abstract void focusableViewAvailable(android.view.View); method public abstract boolean getChildVisibleRect(android.view.View, android.graphics.Rect, android.graphics.Point); method public abstract android.view.ViewParent getParent(); + method public abstract android.view.ViewParent getParentForAccessibility(); method public abstract void invalidateChild(android.view.View, android.graphics.Rect); method public abstract android.view.ViewParent invalidateChildInParent(int[], android.graphics.Rect); method public abstract boolean isLayoutRequested(); @@ -24616,6 +24648,8 @@ package android.view.accessibility { field public static final int TYPE_NOTIFICATION_STATE_CHANGED = 64; // 0x40 field public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 1024; // 0x400 field public static final int TYPE_TOUCH_EXPLORATION_GESTURE_START = 512; // 0x200 + field public static final int TYPE_VIEW_ACCESSIBILITY_FOCUSED = 32768; // 0x8000 + field public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 65536; // 0x10000 field public static final int TYPE_VIEW_CLICKED = 1; // 0x1 field public static final int TYPE_VIEW_FOCUSED = 8; // 0x8 field public static final int TYPE_VIEW_HOVER_ENTER = 128; // 0x80 @@ -24656,6 +24690,8 @@ package android.view.accessibility { method public void addChild(android.view.View, int); method public int describeContents(); method public java.util.List<android.view.accessibility.AccessibilityNodeInfo> findAccessibilityNodeInfosByText(java.lang.String); + method public android.view.accessibility.AccessibilityNodeInfo findFocus(int); + method public android.view.accessibility.AccessibilityNodeInfo focusSearch(int); method public int getActions(); method public void getBoundsInParent(android.graphics.Rect); method public void getBoundsInScreen(android.graphics.Rect); @@ -24667,6 +24703,7 @@ package android.view.accessibility { method public android.view.accessibility.AccessibilityNodeInfo getParent(); method public java.lang.CharSequence getText(); method public int getWindowId(); + method public boolean isAccessibilityFocused(); method public boolean isCheckable(); method public boolean isChecked(); method public boolean isClickable(); @@ -24683,6 +24720,7 @@ package android.view.accessibility { method public static android.view.accessibility.AccessibilityNodeInfo obtain(android.view.accessibility.AccessibilityNodeInfo); method public boolean performAction(int); method public void recycle(); + method public void setAccessibilityFocused(boolean); method public void setBoundsInParent(android.graphics.Rect); method public void setBoundsInScreen(android.graphics.Rect); method public void setCheckable(boolean); @@ -24704,16 +24742,23 @@ package android.view.accessibility { method public void setSource(android.view.View, int); method public void setText(java.lang.CharSequence); method public void writeToParcel(android.os.Parcel, int); + field public static final int ACTION_ACCESSIBILITY_FOCUS = 16; // 0x10 + field public static final int ACTION_CLEAR_ACCESSIBILITY_FOCUS = 32; // 0x20 field public static final int ACTION_CLEAR_FOCUS = 2; // 0x2 field public static final int ACTION_CLEAR_SELECTION = 8; // 0x8 + field public static final int ACTION_CLICK = 64; // 0x40 field public static final int ACTION_FOCUS = 1; // 0x1 field public static final int ACTION_SELECT = 4; // 0x4 field public static final android.os.Parcelable.Creator CREATOR; + field public static final int FOCUS_ACCESSIBILITY = 2; // 0x2 + field public static final int FOCUS_INPUT = 1; // 0x1 } public abstract class AccessibilityNodeProvider { ctor public AccessibilityNodeProvider(); + method public android.view.accessibility.AccessibilityNodeInfo accessibilityFocusSearch(int, int); method public android.view.accessibility.AccessibilityNodeInfo createAccessibilityNodeInfo(int); + method public android.view.accessibility.AccessibilityNodeInfo findAccessibilitiyFocus(int); method public java.util.List<android.view.accessibility.AccessibilityNodeInfo> findAccessibilityNodeInfosByText(java.lang.String, int); method public boolean performAccessibilityAction(int, int); } diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index ddd7f7c..3da35d3 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -19,17 +19,22 @@ package android.accessibilityservice; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; +import android.util.LocaleUtil; import android.util.Log; +import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.os.HandlerCaller; +import java.util.Locale; + /** * An accessibility service runs in the background and receives callbacks by the system * when {@link AccessibilityEvent}s are fired. Such events denote some state transition @@ -202,12 +207,65 @@ import com.android.internal.os.HandlerCaller; * @see android.view.accessibility.AccessibilityManager */ public abstract class AccessibilityService extends Service { + + /** + * The user has performed a swipe up gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_UP = 1; + + /** + * The user has performed a swipe down gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_DOWN = 2; + + /** + * The user has performed a swipe left gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_LEFT = 3; + + /** + * The user has performed a swipe right gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_RIGHT = 4; + + /** + * The user has performed a swipe left and right gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_LEFT_AND_RIGHT = 5; + + /** + * The user has performed a swipe right and left gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_RIGHT_AND_LEFT = 6; + + /** + * The user has performed a swipe up and down gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_UP_AND_DOWN = 7; + + /** + * The user has performed a swipe down and up gesture on the touch screen. + */ + public static final int GESTURE_SWIPE_DOWN_AND_UP = 8; + + /** + * The user has performed a clockwise circle gesture on the touch screen. + */ + public static final int GESTURE_CLOCKWISE_CIRCLE = 9; + + /** + * The user has performed a counter clockwise circle gesture on the touch screen. + */ + public static final int GESTURE_COUNTER_CLOCKWISE_CIRCLE = 10; + /** * The {@link Intent} that must be declared as handled by the service. */ public static final String SERVICE_INTERFACE = "android.accessibilityservice.AccessibilityService"; + private static final int UNDEFINED = -1; + /** * Name under which an AccessibilityService component publishes information * about itself. This meta-data must reference an XML resource containing an @@ -233,12 +291,15 @@ public abstract class AccessibilityService extends Service { public void onInterrupt(); public void onServiceConnected(); public void onSetConnectionId(int connectionId); + public void onGesture(int gestureId); } private int mConnectionId; private AccessibilityServiceInfo mInfo; + private int mLayoutDirection; + /** * Callback for {@link android.view.accessibility.AccessibilityEvent}s. * @@ -264,6 +325,106 @@ public abstract class AccessibilityService extends Service { } /** + * Called by the system when the user performs a specific gesture on the + * touch screen. + * + * @param gestureId The unique id of the performed gesture. + * + * @see #GESTURE_SWIPE_UP + * @see #GESTURE_SWIPE_DOWN + * @see #GESTURE_SWIPE_LEFT + * @see #GESTURE_SWIPE_RIGHT + * @see #GESTURE_SWIPE_UP_AND_DOWN + * @see #GESTURE_SWIPE_DOWN_AND_UP + * @see #GESTURE_SWIPE_LEFT_AND_RIGHT + * @see #GESTURE_SWIPE_RIGHT_AND_LEFT + * @see #GESTURE_CLOCKWISE_CIRCLE + * @see #GESTURE_COUNTER_CLOCKWISE_CIRCLE + */ + protected void onGesture(int gestureId) { + // TODO: Describe the default gesture processing in the javaDoc once it is finalized. + + // Cache the id to avoid locking + final int connectionId = mConnectionId; + if (connectionId == UNDEFINED) { + throw new IllegalStateException("AccessibilityService not connected." + + " Did you receive a call of onServiceConnected()?"); + } + AccessibilityNodeInfo root = AccessibilityInteractionClient.getInstance() + .findAccessibilityNodeInfoByAccessibilityId(connectionId, + AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, + AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); + if (root == null) { + return; + } + AccessibilityNodeInfo current = root.findFocus(View.FOCUS_ACCESSIBILITY); + if (current == null) { + current = root; + } + AccessibilityNodeInfo next = null; + switch (gestureId) { + case GESTURE_SWIPE_UP: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_OUT); + } break; + case GESTURE_SWIPE_DOWN: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_IN); + } break; + case GESTURE_SWIPE_LEFT: { + if (mLayoutDirection == View.LAYOUT_DIRECTION_LTR) { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_BACKWARD); + } else { // LAYOUT_DIRECTION_RTL + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_FORWARD); + } + } break; + case GESTURE_SWIPE_RIGHT: { + if (mLayoutDirection == View.LAYOUT_DIRECTION_LTR) { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_FORWARD); + } else { // LAYOUT_DIRECTION_RTL + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_BACKWARD); + } + } break; + case GESTURE_SWIPE_UP_AND_DOWN: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_UP); + } break; + case GESTURE_SWIPE_DOWN_AND_UP: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_DOWN); + } break; + case GESTURE_SWIPE_LEFT_AND_RIGHT: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_LEFT); + } break; + case GESTURE_SWIPE_RIGHT_AND_LEFT: { + next = current.focusSearch(View.ACCESSIBILITY_FOCUS_RIGHT); + } break; + } + if (next != null && !next.equals(current)) { + next.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + } + } + + /** + * Gets the an {@link AccessibilityServiceInfo} describing this + * {@link AccessibilityService}. This method is useful if one wants + * to change some of the dynamically configurable properties at + * runtime. + * + * @return The accessibility service info. + * + * @see AccessibilityNodeInfo + */ + public final AccessibilityServiceInfo getServiceInfo() { + IAccessibilityServiceConnection connection = + AccessibilityInteractionClient.getInstance().getConnection(mConnectionId); + if (connection != null) { + try { + return connection.getServiceInfo(); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error while getting AccessibilityServiceInfo", re); + } + } + return null; + } + + /** * Sets the {@link AccessibilityServiceInfo} that describes this service. * <p> * Note: You can call this method any time but the info will be picked up after @@ -287,19 +448,33 @@ public abstract class AccessibilityService extends Service { if (mInfo != null && connection != null) { try { connection.setServiceInfo(mInfo); + mInfo = null; + AccessibilityInteractionClient.getInstance().clearCache(); } catch (RemoteException re) { Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re); } } } + @Override + public void onCreate() { + Locale locale = getResources().getConfiguration().locale; + mLayoutDirection = LocaleUtil.getLayoutDirectionFromLocale(locale); + } + + @Override + public void onConfigurationChanged(Configuration configuration) { + super.onConfigurationChanged(configuration); + mLayoutDirection = LocaleUtil.getLayoutDirectionFromLocale(configuration.locale); + } + /** * Implement to return the implementation of the internal accessibility * service interface. */ @Override public final IBinder onBind(Intent intent) { - return new IEventListenerWrapper(this, getMainLooper(), new Callbacks() { + return new IAccessibilityServiceClientWrapper(this, getMainLooper(), new Callbacks() { @Override public void onServiceConnected() { AccessibilityService.this.onServiceConnected(); @@ -319,14 +494,19 @@ public abstract class AccessibilityService extends Service { public void onSetConnectionId( int connectionId) { mConnectionId = connectionId; } + + @Override + public void onGesture(int gestureId) { + AccessibilityService.this.onGesture(gestureId); + } }); } /** - * Implements the internal {@link IEventListener} interface to convert + * Implements the internal {@link IAccessibilityServiceClient} interface to convert * incoming calls to it back to calls on an {@link AccessibilityService}. */ - static class IEventListenerWrapper extends IEventListener.Stub + static class IAccessibilityServiceClientWrapper extends IAccessibilityServiceClient.Stub implements HandlerCaller.Callback { static final int NO_ID = -1; @@ -334,12 +514,14 @@ public abstract class AccessibilityService extends Service { private static final int DO_SET_SET_CONNECTION = 10; private static final int DO_ON_INTERRUPT = 20; private static final int DO_ON_ACCESSIBILITY_EVENT = 30; + private static final int DO_ON_GESTURE = 40; private final HandlerCaller mCaller; private final Callbacks mCallback; - public IEventListenerWrapper(Context context, Looper looper, Callbacks callback) { + public IAccessibilityServiceClientWrapper(Context context, Looper looper, + Callbacks callback) { mCallback = callback; mCaller = new HandlerCaller(context, looper, this); } @@ -360,6 +542,11 @@ public abstract class AccessibilityService extends Service { mCaller.sendMessage(message); } + public void onGesture(int gestureId) { + Message message = mCaller.obtainMessageI(DO_ON_GESTURE, gestureId); + mCaller.sendMessage(message); + } + public void executeMessage(Message message) { switch (message.what) { case DO_ON_ACCESSIBILITY_EVENT : @@ -387,6 +574,10 @@ public abstract class AccessibilityService extends Service { mCallback.onSetConnectionId(AccessibilityInteractionClient.NO_ID); } return; + case DO_ON_GESTURE : + final int gestureId = message.arg1; + mCallback.onGesture(gestureId); + return; default : Log.w(LOG_TAG, "Unknown message type " + message.what); } diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index 8e53431..e77ed9a 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -25,11 +25,13 @@ import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.TypedValue; import android.util.Xml; +import android.view.View; import android.view.accessibility.AccessibilityEvent; import org.xmlpull.v1.XmlPullParser; @@ -101,6 +103,37 @@ public class AccessibilityServiceInfo implements Parcelable { public static final int DEFAULT = 0x0000001; /** + * If this flag is set the system will regard views that are not important + * for accessibility in addition to the ones that are important for accessibility. + * That is, views that are marked as not important for accessibility via + * {@link View#IMPORTANT_FOR_ACCESSIBILITY_NO} and views that are marked as + * potentially important for accessibility via + * {@link View#IMPORTANT_FOR_ACCESSIBILITY_AUTO} for which the system has determined + * that are not important for accessibility, are both reported while querying the + * window content and also the accessibility service will receive accessibility events + * from them. + * <p> + * <strong>Note:</strong> For accessibility services targeting API version + * {@link Build.VERSION_CODES#JELLY_BEAN} or higher this flag has to be explicitly + * set for the system to regard views that are not important for accessibility. For + * accessibility services targeting API version lower than + * {@link Build.VERSION_CODES#JELLY_BEAN} this flag is ignored and all views are + * regarded for accessibility purposes. + * </p> + * <p> + * Usually views not important for accessibility are layout managers that do not + * react to user actions, do not draw any content, and do not have any special + * semantics in the context of the screen content. For example, a three by three + * grid can be implemented as three horizontal linear layouts and one vertical, + * or three vertical linear layouts and one horizontal, or one grid layout, etc. + * In this context the actual layout mangers used to achieve the grid configuration + * are not important, rather it is important that there are nine evenly distributed + * elements. + * </p> + */ + public static final int INCLUDE_NOT_IMPORTANT_VIEWS = 0x0000002; + + /** * The event types an {@link AccessibilityService} is interested in. * <p> * <strong>Can be dynamically set at runtime.</strong> @@ -165,6 +198,7 @@ public class AccessibilityServiceInfo implements Parcelable { * <strong>Can be dynamically set at runtime.</strong> * </p> * @see #DEFAULT + * @see #INCLUDE_NOT_IMPORTANT_VIEWS */ public int flags; @@ -561,6 +595,8 @@ public class AccessibilityServiceInfo implements Parcelable { switch (flag) { case DEFAULT: return "DEFAULT"; + case INCLUDE_NOT_IMPORTANT_VIEWS: + return "REGARD_VIEWS_NOT_IMPORTANT_FOR_ACCESSIBILITY"; default: return null; } diff --git a/core/java/android/accessibilityservice/IEventListener.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl index 5536b3c..588728c 100644 --- a/core/java/android/accessibilityservice/IEventListener.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl @@ -20,15 +20,17 @@ import android.accessibilityservice.IAccessibilityServiceConnection; import android.view.accessibility.AccessibilityEvent; /** - * Top-level interface to accessibility service component (implemented in Service). + * Top-level interface to an accessibility service component. * * @hide */ - oneway interface IEventListener { + oneway interface IAccessibilityServiceClient { void setConnection(in IAccessibilityServiceConnection connection, int connectionId); void onAccessibilityEvent(in AccessibilityEvent event); void onInterrupt(); + + void onGesture(int gestureId); } diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 8d17325..30da9db 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -41,13 +41,13 @@ interface IAccessibilityServiceConnection { * to start from the root. * @param interactionId The id of the interaction for matching with the callback result. * @param callback Callback which to receive the result. + * @param flags Additional flags. * @param threadId The id of the calling thread. - * @param prefetchFlags flags to guide prefetching. * @return The current window scale, where zero means a failure. */ float findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, long accessibilityNodeId, int interactionId, - IAccessibilityInteractionConnectionCallback callback, long threadId, int prefetchFlags); + IAccessibilityInteractionConnectionCallback callback, int flags, long threadId); /** * Finds {@link android.view.accessibility.AccessibilityNodeInfo}s by View text. @@ -94,6 +94,48 @@ interface IAccessibilityServiceConnection { long threadId); /** + * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the specified + * focus type. The search is performed in the window whose id is specified and starts from + * the node whose accessibility id is specified. + * + * @param accessibilityWindowId A unique window id. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id or virtual descendant id from + * where to start the search. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} + * to start from the root. + * @param focusType The type of focus to find. + * @param interactionId The id of the interaction for matching with the callback result. + * @param callback Callback which to receive the result. + * @param threadId The id of the calling thread. + * @return The current window scale, where zero means a failure. + */ + float findFocus(int accessibilityWindowId, long accessibilityNodeId, int focusType, + int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); + + /** + * Finds an {@link android.view.accessibility.AccessibilityNodeInfo} to take accessibility + * focus in the given direction. The search is performed in the window whose id is + * specified and starts from the node whose accessibility id is specified. + * + * @param accessibilityWindowId A unique window id. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id or virtual descendant id from + * where to start the search. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} + * to start from the root. + * @param direction The direction in which to search for focusable. + * @param interactionId The id of the interaction for matching with the callback result. + * @param callback Callback which to receive the result. + * @param threadId The id of the calling thread. + * @return The current window scale, where zero means a failure. + */ + float focusSearch(int accessibilityWindowId, long accessibilityNodeId, int direction, + int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); + + /** * Performs an accessibility action on an * {@link android.view.accessibility.AccessibilityNodeInfo}. * @@ -113,4 +155,9 @@ interface IAccessibilityServiceConnection { boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); + + /** + * @return The associated accessibility service info. + */ + AccessibilityServiceInfo getServiceInfo(); } diff --git a/core/java/android/accessibilityservice/UiTestAutomationBridge.java b/core/java/android/accessibilityservice/UiTestAutomationBridge.java index a898c3f..c840bd6 100644 --- a/core/java/android/accessibilityservice/UiTestAutomationBridge.java +++ b/core/java/android/accessibilityservice/UiTestAutomationBridge.java @@ -17,7 +17,7 @@ package android.accessibilityservice; import android.accessibilityservice.AccessibilityService.Callbacks; -import android.accessibilityservice.AccessibilityService.IEventListenerWrapper; +import android.accessibilityservice.AccessibilityService.IAccessibilityServiceClientWrapper; import android.content.Context; import android.os.HandlerThread; import android.os.Looper; @@ -66,7 +66,7 @@ public class UiTestAutomationBridge { private volatile int mConnectionId = AccessibilityInteractionClient.NO_ID; - private IEventListenerWrapper mListener; + private IAccessibilityServiceClientWrapper mListener; private AccessibilityEvent mLastEvent; @@ -133,7 +133,7 @@ public class UiTestAutomationBridge { mHandlerThread.start(); Looper looper = mHandlerThread.getLooper(); - mListener = new IEventListenerWrapper(null, looper, new Callbacks() { + mListener = new IAccessibilityServiceClientWrapper(null, looper, new Callbacks() { @Override public void onServiceConnected() { /* do nothing */ @@ -175,6 +175,11 @@ public class UiTestAutomationBridge { mLock.notifyAll(); } } + + @Override + public void onGesture(int gestureId) { + /* do nothing */ + } }); final IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( @@ -252,6 +257,7 @@ public class UiTestAutomationBridge { public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, Predicate<AccessibilityEvent> predicate, long timeoutMillis) throws TimeoutException, Exception { + // TODO: This is broken - remove from here when finalizing this as public APIs. synchronized (mLock) { // Prepare to wait for an event. mWaitingForEventDelivery = true; diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java new file mode 100644 index 0000000..ab21b32 --- /dev/null +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -0,0 +1,900 @@ +/* + * Copyright (C) 2012 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; + +import static android.view.accessibility.AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.os.RemoteException; +import android.util.Pool; +import android.util.Poolable; +import android.util.PoolableManager; +import android.util.Pools; +import android.util.SparseLongArray; +import android.view.ViewGroup.ChildListForAccessibility; +import android.view.accessibility.AccessibilityInteractionClient; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; +import android.view.accessibility.IAccessibilityInteractionConnectionCallback; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 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 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 ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList = + new ArrayList<AccessibilityNodeInfo>(); + + private final Handler mHandler = new PrivateHandler(); + + private final ViewRootImpl mViewRootImpl; + + private final AccessibilityNodePrefetcher mPrefetcher; + + public AccessibilityInteractionController(ViewRootImpl viewRootImpl) { + mViewRootImpl = viewRootImpl; + mPrefetcher = new AccessibilityNodePrefetcher(); + } + + // 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) + ); + + private 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( + long accessibilityNodeId, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, + long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID; + message.arg1 = flags; + SomeArgs args = mPool.acquire(); + args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi3 = interactionId; + args.arg1 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interrogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) { + final int flags = message.arg1; + SomeArgs args = (SomeArgs) message.obj; + final int accessibilityViewId = args.argi1; + final int virtualDescendantId = args.argi2; + final int interactionId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList; + infos.clear(); + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View root = null; + if (accessibilityViewId == AccessibilityNodeInfo.UNDEFINED) { + root = mViewRootImpl.mView; + } else { + root = findViewByAccessibilityId(accessibilityViewId); + } + if (root != null && isDisplayedOnScreen(root)) { + mPrefetcher.prefetchAccessibilityNodeInfos(root, virtualDescendantId, flags, infos); + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setFindAccessibilityNodeInfosResult(infos, interactionId); + infos.clear(); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void findAccessibilityNodeInfoByViewIdClientThread(long accessibilityNodeId, + int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, + int flags, int interrogatingPid, long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID; + message.arg1 = flags; + message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + SomeArgs args = mPool.acquire(); + args.argi1 = viewId; + args.argi2 = interactionId; + args.arg1 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interrogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void findAccessibilityNodeInfoByViewIdUiThread(Message message) { + final int flags = message.arg1; + final int accessibilityViewId = message.arg2; + SomeArgs args = (SomeArgs) message.obj; + final int viewId = args.argi1; + final int interactionId = args.argi2; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + AccessibilityNodeInfo info = null; + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View root = null; + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + root = findViewByAccessibilityId(accessibilityViewId); + } else { + root = mViewRootImpl.mView; + } + if (root != null) { + View target = root.findViewById(viewId); + if (target != null && isDisplayedOnScreen(target)) { + info = target.createAccessibilityNodeInfo(); + } + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setFindAccessibilityNodeInfoResult(info, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void findAccessibilityNodeInfosByTextClientThread(long accessibilityNodeId, + String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, + int flags, int interrogatingPid, long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT; + message.arg1 = flags; + SomeArgs args = mPool.acquire(); + args.arg1 = text; + args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi3 = interactionId; + args.arg2 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interrogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void findAccessibilityNodeInfosByTextUiThread(Message message) { + final int flags = message.arg1; + SomeArgs args = (SomeArgs) message.obj; + final String text = (String) args.arg1; + final int accessibilityViewId = args.argi1; + final int virtualDescendantId = args.argi2; + final int interactionId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg2; + mPool.release(args); + List<AccessibilityNodeInfo> infos = null; + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View root = null; + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + root = findViewByAccessibilityId(accessibilityViewId); + } else { + root = mViewRootImpl.mView; + } + if (root != null && isDisplayedOnScreen(root)) { + AccessibilityNodeProvider provider = root.getAccessibilityNodeProvider(); + if (provider != null) { + infos = provider.findAccessibilityNodeInfosByText(text, + virtualDescendantId); + } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED) { + ArrayList<View> foundViews = mViewRootImpl.mAttachInfo.mTempArrayList; + foundViews.clear(); + root.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT + | View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION + | View.FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS); + if (!foundViews.isEmpty()) { + infos = mTempAccessibilityNodeInfoList; + infos.clear(); + final int viewCount = foundViews.size(); + for (int i = 0; i < viewCount; i++) { + View foundView = foundViews.get(i); + if (isDisplayedOnScreen(foundView)) { + provider = foundView.getAccessibilityNodeProvider(); + if (provider != null) { + List<AccessibilityNodeInfo> infosFromProvider = + provider.findAccessibilityNodeInfosByText(text, + virtualDescendantId); + if (infosFromProvider != null) { + infos.addAll(infosFromProvider); + } + } else { + infos.add(foundView.createAccessibilityNodeInfo()); + } + } + } + } + } + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setFindAccessibilityNodeInfosResult(infos, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void findFocusClientThread(long accessibilityNodeId, int interactionId, int focusType, + IAccessibilityInteractionConnectionCallback callback, int flags, int interogatingPid, + long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_FIND_FOCUS; + message.arg1 = flags; + message.arg2 = focusType; + SomeArgs args = mPool.acquire(); + args.argi1 = interactionId; + args.argi2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + args.argi3 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.arg1 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void findFocusUiThread(Message message) { + final int flags = message.arg1; + final int focusType = message.arg2; + SomeArgs args = (SomeArgs) message.obj; + final int interactionId = args.argi1; + final int accessibilityViewId = args.argi2; + final int virtualDescendantId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + AccessibilityNodeInfo focused = null; + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View root = null; + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + root = findViewByAccessibilityId(accessibilityViewId); + } else { + root = mViewRootImpl.mView; + } + if (root != null && isDisplayedOnScreen(root)) { + switch (focusType) { + case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: { + View host = mViewRootImpl.mAccessibilityFocusedHost; + // If there is no accessibility focus host or it is not a descendant + // of the root from which to start the search, then the search failed. + if (host == null || !ViewRootImpl.isViewDescendantOf(host, root)) { + break; + } + // If the host has a provider ask this provider to search for the + // focus instead fetching all provider nodes to do the search here. + AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider(); + if (provider != null) { + focused = provider.findAccessibilitiyFocus(virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + focused = host.createAccessibilityNodeInfo(); + } + } break; + case AccessibilityNodeInfo.FOCUS_INPUT: { + // Input focus cannot go to virtual views. + View target = root.findFocus(); + if (target != null && isDisplayedOnScreen(target)) { + focused = target.createAccessibilityNodeInfo(); + } + } break; + default: + throw new IllegalArgumentException("Unknown focus type: " + focusType); + } + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setFindAccessibilityNodeInfoResult(focused, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void focusSearchClientThread(long accessibilityNodeId, int interactionId, int direction, + IAccessibilityInteractionConnectionCallback callback, int flags, int interogatingPid, + long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_FOCUS_SEARCH; + message.arg1 = flags; + message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + SomeArgs args = mPool.acquire(); + args.argi1 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi2 = direction; + args.argi3 = interactionId; + args.arg1 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void focusSearchUiThread(Message message) { + final int flags = message.arg1; + final int accessibilityViewId = message.arg2; + SomeArgs args = (SomeArgs) message.obj; + final int virtualDescendantId = args.argi1; + final int direction = args.argi2; + final int interactionId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + AccessibilityNodeInfo next = null; + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View root = null; + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + root = findViewByAccessibilityId(accessibilityViewId); + } else { + root = mViewRootImpl.mView; + } + if (root != null && isDisplayedOnScreen(root)) { + if ((direction & View.FOCUS_ACCESSIBILITY) == View.FOCUS_ACCESSIBILITY) { + AccessibilityNodeProvider provider = root.getAccessibilityNodeProvider(); + if (provider != null) { + next = provider.accessibilityFocusSearch(direction, + virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + View nextView = root.focusSearch(direction); + if (nextView != null) { + // If the focus search reached a node with a provider + // we delegate to the provider to find the next one. + provider = nextView.getAccessibilityNodeProvider(); + if (provider != null) { + next = provider.accessibilityFocusSearch(direction, + virtualDescendantId); + } else { + next = nextView.createAccessibilityNodeInfo(); + } + } + } + } else { + View nextView = root.focusSearch(direction); + if (nextView != null) { + next = nextView.createAccessibilityNodeInfo(); + } + } + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setFindAccessibilityNodeInfoResult(next, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void performAccessibilityActionClientThread(long accessibilityNodeId, int action, + int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, + int interogatingPid, long interrogatingTid) { + Message message = mHandler.obtainMessage(); + message.what = PrivateHandler.MSG_PERFORM_ACCESSIBILITY_ACTION; + message.arg1 = flags; + message.arg2 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); + SomeArgs args = mPool.acquire(); + args.argi1 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); + args.argi2 = action; + args.argi3 = interactionId; + args.arg1 = callback; + message.obj = args; + // If the interrogation is performed by the same thread as the main UI + // thread in this process, set the message as a static reference so + // after this call completes the same thread but in the interrogating + // client can handle the message to generate the result. + if (interogatingPid == Process.myPid() + && interrogatingTid == Looper.getMainLooper().getThread().getId()) { + AccessibilityInteractionClient.getInstanceForThread( + interrogatingTid).setSameThreadMessage(message); + } else { + mHandler.sendMessage(message); + } + } + + private void perfromAccessibilityActionUiThread(Message message) { + final int flags = message.arg1; + final int accessibilityViewId = message.arg2; + SomeArgs args = (SomeArgs) message.obj; + final int virtualDescendantId = args.argi1; + final int action = args.argi2; + final int interactionId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + boolean succeeded = false; + try { + if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) { + return; + } + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = + (flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + View target = null; + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + target = findViewByAccessibilityId(accessibilityViewId); + } else { + target = mViewRootImpl.mView; + } + if (target != null && isDisplayedOnScreen(target)) { + AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); + if (provider != null) { + succeeded = provider.performAccessibilityAction(action, virtualDescendantId); + } else if (virtualDescendantId == View.NO_ID) { + succeeded = target.performAccessibilityAction(action); + } + } + } finally { + try { + mViewRootImpl.mAttachInfo.mIncludeNotImportantViews = false; + callback.setPerformAccessibilityActionResult(succeeded, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + private View findViewByAccessibilityId(int accessibilityId) { + View root = mViewRootImpl.mView; + if (root == null) { + return null; + } + View foundView = root.findViewByAccessibilityId(accessibilityId); + if (foundView != null && !isDisplayedOnScreen(foundView)) { + return null; + } + return foundView; + } + + /** + * Computes whether a view is visible on the screen. + * + * @param view The view to check. + * @return Whether the view is visible on the screen. + */ + private boolean isDisplayedOnScreen(View view) { + // The first two checks are made also made by isShown() which + // however traverses the tree up to the parent to catch that. + // Therefore, we do some fail fast check to minimize the up + // tree traversal. + return (view.mAttachInfo != null + && view.mAttachInfo.mWindowVisibility == View.VISIBLE + && view.isShown() + && view.getGlobalVisibleRect(mViewRootImpl.mTempRect)); + } + + /** + * This class encapsulates a prefetching strategy for the accessibility APIs for + * querying window content. It is responsible to prefetch a batch of + * AccessibilityNodeInfos in addition to the one for a requested node. + */ + private class AccessibilityNodePrefetcher { + + private static final int MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE = 50; + + public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int prefetchFlags, + List<AccessibilityNodeInfo> outInfos) { + AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider(); + if (provider == null) { + AccessibilityNodeInfo root = view.createAccessibilityNodeInfo(); + if (root != null) { + outInfos.add(root); + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { + prefetchPredecessorsOfRealNode(view, outInfos); + } + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { + prefetchSiblingsOfRealNode(view, outInfos); + } + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { + prefetchDescendantsOfRealNode(view, outInfos); + } + } + } else { + AccessibilityNodeInfo root = provider.createAccessibilityNodeInfo(virtualViewId); + if (root != null) { + outInfos.add(root); + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { + prefetchPredecessorsOfVirtualNode(root, view, provider, outInfos); + } + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { + prefetchSiblingsOfVirtualNode(root, view, provider, outInfos); + } + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { + prefetchDescendantsOfVirtualNode(root, provider, outInfos); + } + } + } + } + + private void prefetchPredecessorsOfRealNode(View view, + List<AccessibilityNodeInfo> outInfos) { + ViewParent parent = view.getParentForAccessibility(); + while (parent instanceof View + && outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + View parentView = (View) parent; + final long parentNodeId = AccessibilityNodeInfo.makeNodeId( + parentView.getAccessibilityViewId(), AccessibilityNodeInfo.UNDEFINED); + AccessibilityNodeInfo info = parentView.createAccessibilityNodeInfo(); + if (info != null) { + outInfos.add(info); + } + parent = parent.getParentForAccessibility(); + } + } + + private void prefetchSiblingsOfRealNode(View current, + List<AccessibilityNodeInfo> outInfos) { + ViewParent parent = current.getParentForAccessibility(); + if (parent instanceof ViewGroup) { + ViewGroup parentGroup = (ViewGroup) parent; + ChildListForAccessibility children = ChildListForAccessibility.obtain(parentGroup, + false); + final int childCount = children.getChildCount(); + for (int i = 0; i < childCount; i++) { + if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + children.recycle(); + return; + } + View child = children.getChildAt(i); + if (child.getAccessibilityViewId() != current.getAccessibilityViewId() + && isDisplayedOnScreen(child)) { + AccessibilityNodeInfo info = null; + AccessibilityNodeProvider provider = child.getAccessibilityNodeProvider(); + if (provider == null) { + info = child.createAccessibilityNodeInfo(); + } else { + info = provider.createAccessibilityNodeInfo( + AccessibilityNodeInfo.UNDEFINED); + } + if (info != null) { + outInfos.add(info); + } + } + } + children.recycle(); + } + } + + private void prefetchDescendantsOfRealNode(View root, + List<AccessibilityNodeInfo> outInfos) { + if (!(root instanceof ViewGroup)) { + return; + } + ViewGroup rootGroup = (ViewGroup) root; + HashMap<View, AccessibilityNodeInfo> addedChildren = + new HashMap<View, AccessibilityNodeInfo>(); + ChildListForAccessibility children = ChildListForAccessibility.obtain(rootGroup, false); + final int childCount = children.getChildCount(); + for (int i = 0; i < childCount; i++) { + if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + children.recycle(); + return; + } + View child = children.getChildAt(i); + if ( isDisplayedOnScreen(child)) { + AccessibilityNodeProvider provider = child.getAccessibilityNodeProvider(); + if (provider == null) { + AccessibilityNodeInfo info = child.createAccessibilityNodeInfo(); + if (info != null) { + outInfos.add(info); + addedChildren.put(child, null); + } + } else { + AccessibilityNodeInfo info = provider.createAccessibilityNodeInfo( + AccessibilityNodeInfo.UNDEFINED); + if (info != null) { + outInfos.add(info); + addedChildren.put(child, info); + } + } + } + } + children.recycle(); + if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + for (Map.Entry<View, AccessibilityNodeInfo> entry : addedChildren.entrySet()) { + View addedChild = entry.getKey(); + AccessibilityNodeInfo virtualRoot = entry.getValue(); + if (virtualRoot == null) { + prefetchDescendantsOfRealNode(addedChild, outInfos); + } else { + AccessibilityNodeProvider provider = + addedChild.getAccessibilityNodeProvider(); + prefetchDescendantsOfVirtualNode(virtualRoot, provider, outInfos); + } + } + } + } + + private void prefetchPredecessorsOfVirtualNode(AccessibilityNodeInfo root, + View providerHost, AccessibilityNodeProvider provider, + List<AccessibilityNodeInfo> outInfos) { + long parentNodeId = root.getParentNodeId(); + int accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId); + while (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + return; + } + final int virtualDescendantId = + AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId); + if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED + || accessibilityViewId == providerHost.getAccessibilityViewId()) { + AccessibilityNodeInfo parent = provider.createAccessibilityNodeInfo( + virtualDescendantId); + if (parent != null) { + outInfos.add(parent); + } + parentNodeId = parent.getParentNodeId(); + accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId( + parentNodeId); + } else { + prefetchPredecessorsOfRealNode(providerHost, outInfos); + return; + } + } + } + + private void prefetchSiblingsOfVirtualNode(AccessibilityNodeInfo current, View providerHost, + AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) { + final long parentNodeId = current.getParentNodeId(); + final int parentAccessibilityViewId = + AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId); + final int parentVirtualDescendantId = + AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId); + if (parentVirtualDescendantId != AccessibilityNodeInfo.UNDEFINED + || parentAccessibilityViewId == providerHost.getAccessibilityViewId()) { + AccessibilityNodeInfo parent = + provider.createAccessibilityNodeInfo(parentVirtualDescendantId); + if (parent != null) { + SparseLongArray childNodeIds = parent.getChildNodeIds(); + final int childCount = childNodeIds.size(); + for (int i = 0; i < childCount; i++) { + if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + return; + } + final long childNodeId = childNodeIds.get(i); + if (childNodeId != current.getSourceNodeId()) { + final int childVirtualDescendantId = + AccessibilityNodeInfo.getVirtualDescendantId(childNodeId); + AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo( + childVirtualDescendantId); + if (child != null) { + outInfos.add(child); + } + } + } + } + } else { + prefetchSiblingsOfRealNode(providerHost, outInfos); + } + } + + private void prefetchDescendantsOfVirtualNode(AccessibilityNodeInfo root, + AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) { + SparseLongArray childNodeIds = root.getChildNodeIds(); + final int initialOutInfosSize = outInfos.size(); + final int childCount = childNodeIds.size(); + for (int i = 0; i < childCount; i++) { + if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + return; + } + final long childNodeId = childNodeIds.get(i); + AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo( + AccessibilityNodeInfo.getVirtualDescendantId(childNodeId)); + if (child != null) { + outInfos.add(child); + } + } + if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { + final int addedChildCount = outInfos.size() - initialOutInfosSize; + for (int i = 0; i < addedChildCount; i++) { + AccessibilityNodeInfo child = outInfos.get(initialOutInfosSize + i); + prefetchDescendantsOfVirtualNode(child, provider, outInfos); + } + } + } + } + + private class PrivateHandler extends Handler { + private final static int MSG_PERFORM_ACCESSIBILITY_ACTION = 1; + private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 2; + private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID = 3; + private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 4; + private final static int MSG_FIND_FOCUS = 5; + private final static int MSG_FOCUS_SEARCH = 6; + + public PrivateHandler() { + super(Looper.getMainLooper()); + } + + @Override + public String getMessageName(Message message) { + final int type = message.what; + switch (type) { + case MSG_PERFORM_ACCESSIBILITY_ACTION: + return "MSG_PERFORM_ACCESSIBILITY_ACTION"; + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: + return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID"; + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: + return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID"; + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: + return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT"; + case MSG_FIND_FOCUS: + return "MSG_FIND_FOCUS"; + case MSG_FOCUS_SEARCH: + return "MSG_FOCUS_SEARCH"; + default: + throw new IllegalArgumentException("Unknown message type: " + type); + } + } + + @Override + public void handleMessage(Message message) { + final int type = message.what; + switch (type) { + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: { + findAccessibilityNodeInfoByAccessibilityIdUiThread(message); + } break; + case MSG_PERFORM_ACCESSIBILITY_ACTION: { + perfromAccessibilityActionUiThread(message); + } break; + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: { + findAccessibilityNodeInfoByViewIdUiThread(message); + } break; + case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: { + findAccessibilityNodeInfosByTextUiThread(message); + } break; + case MSG_FIND_FOCUS: { + findFocusUiThread(message); + } break; + case MSG_FOCUS_SEARCH: { + focusSearchUiThread(message); + } break; + default: + throw new IllegalArgumentException("Unknown message type: " + type); + } + } + } +} diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java index 3529b8e..8a01c15 100644 --- a/core/java/android/view/FocusFinder.java +++ b/core/java/android/view/FocusFinder.java @@ -17,10 +17,12 @@ package android.view; import android.graphics.Rect; +import android.view.ViewGroup.ChildListForAccessibility; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Stack; /** * The algorithm used for finding the next focusable view in a given direction @@ -30,7 +32,7 @@ public class FocusFinder { private static ThreadLocal<FocusFinder> tlFocusFinder = new ThreadLocal<FocusFinder>() { - + @Override protected FocusFinder initialValue() { return new FocusFinder(); } @@ -48,6 +50,10 @@ public class FocusFinder { Rect mBestCandidateRect = new Rect(); SequentialFocusComparator mSequentialFocusComparator = new SequentialFocusComparator(); + private final ArrayList<View> mTempList = new ArrayList<View>(); + + private Stack<View> mTempStack; + // enforce thread local access private FocusFinder() {} @@ -60,7 +66,30 @@ public class FocusFinder { * @return The next focusable view, or null if none exists. */ public final View findNextFocus(ViewGroup root, View focused, int direction) { + return findNextFocus(root, focused, mFocusedRect, direction); + } + + /** + * Find the next view to take focus in root's descendants, searching from + * a particular rectangle in root's coordinates. + * @param root Contains focusedRect. Cannot be null. + * @param focusedRect The starting point of the search. + * @param direction Direction to look. + * @return The next focusable view, or null if none exists. + */ + public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) { + return findNextFocus(root, null, focusedRect, direction); + } + + private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { + if ((direction & View.FOCUS_ACCESSIBILITY) != View.FOCUS_ACCESSIBILITY) { + return findNextInputFocus(root, focused, focusedRect, direction); + } else { + return findNextAccessibilityFocus(root, focused, direction); + } + } + private View findNextInputFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { if (focused != null) { // check for user specified next focus View userSetNextFocus = focused.findUserSetNextFocus(root, direction); @@ -79,90 +108,154 @@ public class FocusFinder { switch (direction) { case View.FOCUS_RIGHT: case View.FOCUS_DOWN: - setFocusBottomRight(root); + setFocusTopLeft(root); break; case View.FOCUS_FORWARD: if (root.isLayoutRtl()) { - setFocusTopLeft(root); - } else { setFocusBottomRight(root); + } else { + setFocusTopLeft(root); } break; case View.FOCUS_LEFT: case View.FOCUS_UP: - setFocusTopLeft(root); + setFocusBottomRight(root); break; case View.FOCUS_BACKWARD: if (root.isLayoutRtl()) { - setFocusBottomRight(root); - } else { setFocusTopLeft(root); + } else { + setFocusBottomRight(root); break; } } } - return findNextFocus(root, focused, mFocusedRect, direction); - } - private void setFocusTopLeft(ViewGroup root) { - final int rootBottom = root.getScrollY() + root.getHeight(); - final int rootRight = root.getScrollX() + root.getWidth(); - mFocusedRect.set(rootRight, rootBottom, - rootRight, rootBottom); - } + ArrayList<View> focusables = mTempList; + focusables.clear(); + root.addFocusables(focusables, direction); + if (focusables.isEmpty()) { + // The focus cannot change. + return null; + } - private void setFocusBottomRight(ViewGroup root) { - final int rootTop = root.getScrollY(); - final int rootLeft = root.getScrollX(); - mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop); + try { + switch (direction) { + case View.FOCUS_FORWARD: + case View.FOCUS_BACKWARD: + return findNextInputFocusInRelativeDirection(focusables, root, focused, + focusedRect, direction); + case View.FOCUS_UP: + case View.FOCUS_DOWN: + case View.FOCUS_LEFT: + case View.FOCUS_RIGHT: + return findNextInputFocusInAbsoluteDirection(focusables, root, focused, + focusedRect, direction); + default: + throw new IllegalArgumentException("Unknown direction: " + direction); + } + } finally { + focusables.clear(); + } } /** - * Find the next view to take focus in root's descendants, searching from - * a particular rectangle in root's coordinates. - * @param root Contains focusedRect. Cannot be null. - * @param focusedRect The starting point of the search. + * Find the next view to take accessibility focus in root's descendants, + * starting from the view that currently is accessibility focused. + * + * @param root The root which also contains the focused view. + * @param focused The current accessibility focused view. * @param direction Direction to look. * @return The next focusable view, or null if none exists. */ - public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) { - return findNextFocus(root, null, focusedRect, direction); - } - - private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { - ArrayList<View> focusables = root.getFocusables(direction); - if (focusables.isEmpty()) { - // The focus cannot change. - return null; + private View findNextAccessibilityFocus(ViewGroup root, View focused, int direction) { + switch (direction) { + case View.ACCESSIBILITY_FOCUS_IN: + case View.ACCESSIBILITY_FOCUS_OUT: + case View.ACCESSIBILITY_FOCUS_FORWARD: + case View.ACCESSIBILITY_FOCUS_BACKWARD: { + return findNextHierarchicalAcessibilityFocus(root, focused, direction); + } + case View.ACCESSIBILITY_FOCUS_LEFT: + case View.ACCESSIBILITY_FOCUS_RIGHT: + case View.ACCESSIBILITY_FOCUS_UP: + case View.ACCESSIBILITY_FOCUS_DOWN: { + return findNextDirectionalAccessibilityFocus(root, focused, direction); + } + default: + throw new IllegalArgumentException("Unknown direction: " + direction); } + } - if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) { - if (focused != null && !focusables.contains(focused)) { - // Add the currently focused view to the list to have it sorted - // along with the other views. - focusables.add(focused); + private View findNextHierarchicalAcessibilityFocus(ViewGroup root, View focused, + int direction) { + View current = (focused != null) ? focused : root; + switch (direction) { + case View.ACCESSIBILITY_FOCUS_IN: { + return findNextAccessibilityFocusIn(current); } - - try { - // Note: This sort is stable. - mSequentialFocusComparator.setRoot(root); - Collections.sort(focusables, mSequentialFocusComparator); - } finally { - mSequentialFocusComparator.recycle(); + case View.ACCESSIBILITY_FOCUS_OUT: { + return findNextAccessibilityFocusOut(current); + } + case View.ACCESSIBILITY_FOCUS_FORWARD: { + return findNextAccessibilityFocusForward(current); + } + case View.ACCESSIBILITY_FOCUS_BACKWARD: { + return findNextAccessibilityFocusBackward(current); } + } + return null; + } - final int count = focusables.size(); - switch (direction) { - case View.FOCUS_FORWARD: - return getForwardFocusable(root, focused, focusables, count); + private View findNextDirectionalAccessibilityFocus(ViewGroup root, View focused, + int direction) { + ArrayList<View> focusables = mTempList; + focusables.clear(); + root.addFocusables(focusables, direction, View.FOCUSABLES_ACCESSIBILITY); + Rect focusedRect = getFocusedRect(root, focused, direction); + final int inputFocusDirection = getCorrespondingInputFocusDirection(direction); + View next = findNextInputFocusInAbsoluteDirection(focusables, root, + focused, focusedRect, inputFocusDirection); + focusables.clear(); + return next; + } - case View.FOCUS_BACKWARD: - return getBackwardFocusable(root, focused, focusables, count); - } - return null; + private View findNextInputFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root, + View focused, Rect focusedRect, int direction) { + try { + // Note: This sort is stable. + mSequentialFocusComparator.setRoot(root); + Collections.sort(focusables, mSequentialFocusComparator); + } finally { + mSequentialFocusComparator.recycle(); } + final int count = focusables.size(); + switch (direction) { + case View.FOCUS_FORWARD: + return getForwardFocusable(root, focused, focusables, count); + case View.FOCUS_BACKWARD: + return getBackwardFocusable(root, focused, focusables, count); + } + return focusables.get(count - 1); + } + + private void setFocusBottomRight(ViewGroup root) { + final int rootBottom = root.getScrollY() + root.getHeight(); + final int rootRight = root.getScrollX() + root.getWidth(); + mFocusedRect.set(rootRight, rootBottom, + rootRight, rootBottom); + } + + private void setFocusTopLeft(ViewGroup root) { + final int rootTop = root.getScrollY(); + final int rootLeft = root.getScrollX(); + mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop); + } + + View findNextInputFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, + Rect focusedRect, int direction) { // initialize the best candidate to something impossible // (so the first plausible view will become the best choice) mBestCandidateRect.set(focusedRect); @@ -201,6 +294,140 @@ public class FocusFinder { return closest; } + private View findNextAccessibilityFocusIn(View view) { + // We have to traverse the full view tree to make sure + // we consider views in the order specified by their + // parent layout managers since some managers could be + // LTR while some could be RTL. + if (mTempStack == null) { + mTempStack = new Stack<View>(); + } + Stack<View> fringe = mTempStack; + fringe.clear(); + fringe.add(view); + while (!fringe.isEmpty()) { + View current = fringe.pop(); + if (current.getAccessibilityNodeProvider() != null) { + fringe.clear(); + return current; + } + if (current != view && current.includeForAccessibility()) { + fringe.clear(); + return current; + } + if (current instanceof ViewGroup) { + ViewGroup currentGroup = (ViewGroup) current; + ChildListForAccessibility children = ChildListForAccessibility.obtain( + currentGroup, true); + final int childCount = children.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + fringe.push(children.getChildAt(i)); + } + children.recycle(); + } + } + return null; + } + + private View findNextAccessibilityFocusOut(View view) { + ViewParent parent = view.getParentForAccessibility(); + if (parent instanceof View) { + return (View) parent; + } + return null; + } + + private View findNextAccessibilityFocusForward(View view) { + // We have to traverse the full view tree to make sure + // we consider views in the order specified by their + // parent layout managers since some managers could be + // LTR while some could be RTL. + View current = view; + while (current != null) { + ViewParent parent = current.getParent(); + if (!(parent instanceof ViewGroup)) { + return null; + } + ViewGroup parentGroup = (ViewGroup) parent; + // Ask the parent to find a sibling after the current view + // that can take accessibility focus. + ChildListForAccessibility children = ChildListForAccessibility.obtain( + parentGroup, true); + final int fromIndex = children.getChildIndex(current) + 1; + final int childCount = children.getChildCount(); + for (int i = fromIndex; i < childCount; i++) { + View child = children.getChildAt(i); + View next = null; + if (child.getAccessibilityNodeProvider() != null) { + next = child; + } else if (child.includeForAccessibility()) { + next = child; + } else { + next = findNextAccessibilityFocusIn(child); + } + if (next != null) { + children.recycle(); + return next; + } + } + children.recycle(); + // Reaching a regarded for accessibility predecessor without + // finding a next view to take focus means that at this level + // there is no next accessibility focusable sibling. + if (parentGroup.includeForAccessibility()) { + return null; + } + // Try asking a predecessor to find a focusable. + current = parentGroup; + } + return null; + } + + private View findNextAccessibilityFocusBackward(View view) { + // We have to traverse the full view tree to make sure + // we consider views in the order specified by their + // parent layout managers since some managers could be + // LTR while some could be RTL. + View current = view; + while (current != null) { + ViewParent parent = current.getParent(); + if (!(parent instanceof ViewGroup)) { + return null; + } + ViewGroup parentGroup = (ViewGroup) parent; + // Ask the parent to find a sibling after the current view + // to take accessibility focus + ChildListForAccessibility children = ChildListForAccessibility.obtain( + parentGroup, true); + final int fromIndex = children.getChildIndex(current) - 1; + for (int i = fromIndex; i >= 0; i--) { + View child = children.getChildAt(i); + View next = null; + if (child.getAccessibilityNodeProvider() != null) { + next = child; + } else if (child.includeForAccessibility()) { + next = child; + } else { + next = findNextAccessibilityFocusIn(child); + } + if (next != null) { + children.recycle(); + return next; + } + } + children.recycle(); + // Reaching a regarded for accessibility predecessor without + // finding a previous view to take focus means that at this level + // there is no previous accessibility focusable sibling. + if (parentGroup.includeForAccessibility()) { + return null; + } + // Try asking a predecessor to find a focusable. + current = parentGroup; + } + return null; + } + private static View getForwardFocusable(ViewGroup root, View focused, ArrayList<View> focusables, int count) { return (root.isLayoutRtl()) ? @@ -235,6 +462,47 @@ public class FocusFinder { return focusables.get(count - 1); } + private Rect getFocusedRect(ViewGroup root, View focused, int direction) { + Rect focusedRect = mFocusedRect; + if (focused != null) { + focused.getFocusedRect(focusedRect); + root.offsetDescendantRectToMyCoords(focused, focusedRect); + } else { + switch (direction) { + case View.FOCUS_RIGHT: + case View.FOCUS_DOWN: + final int rootTop = root.getScrollY(); + final int rootLeft = root.getScrollX(); + focusedRect.set(rootLeft, rootTop, rootLeft, rootTop); + break; + + case View.FOCUS_LEFT: + case View.FOCUS_UP: + final int rootBottom = root.getScrollY() + root.getHeight(); + final int rootRight = root.getScrollX() + root.getWidth(); + focusedRect.set(rootRight, rootBottom, rootRight, rootBottom); + break; + } + } + return focusedRect; + } + + private int getCorrespondingInputFocusDirection(int accessFocusDirection) { + switch (accessFocusDirection) { + case View.ACCESSIBILITY_FOCUS_LEFT: + return View.FOCUS_LEFT; + case View.ACCESSIBILITY_FOCUS_RIGHT: + return View.FOCUS_RIGHT; + case View.ACCESSIBILITY_FOCUS_UP: + return View.FOCUS_UP; + case View.ACCESSIBILITY_FOCUS_DOWN: + return View.FOCUS_DOWN; + default: + throw new IllegalArgumentException("Cannot map accessiblity focus" + + " direction: " + accessFocusDirection); + } + } + /** * Is rect1 a better candidate than rect2 for a focus search in a particular * direction from a source rect? This is the core routine that determines diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1fa19d1..962e13a 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -975,6 +975,14 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal public static final int FOCUSABLES_TOUCH_MODE = 0x00000001; /** + * View flag indicating whether {@link #addFocusables(ArrayList, int, int)} + * should add only accessibility focusable Views. + * + * @hide + */ + public static final int FOCUSABLES_ACCESSIBILITY = 0x00000002; + + /** * Use with {@link #focusSearch(int)}. Move focus to the previous selectable * item. */ @@ -1006,6 +1014,54 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ public static final int FOCUS_DOWN = 0x00000082; + // Accessibility focus directions. + + /** + * The accessibility focus which is the current user position when + * interacting with the accessibility framework. + */ + public static final int FOCUS_ACCESSIBILITY = 0x00001000; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus left. + */ + public static final int ACCESSIBILITY_FOCUS_LEFT = FOCUS_LEFT | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus up. + */ + public static final int ACCESSIBILITY_FOCUS_UP = FOCUS_UP | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus right. + */ + public static final int ACCESSIBILITY_FOCUS_RIGHT = FOCUS_RIGHT | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus down. + */ + public static final int ACCESSIBILITY_FOCUS_DOWN = FOCUS_DOWN | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus to the next view. + */ + public static final int ACCESSIBILITY_FOCUS_FORWARD = FOCUS_FORWARD | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus to the previous view. + */ + public static final int ACCESSIBILITY_FOCUS_BACKWARD = FOCUS_BACKWARD | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus in a view. + */ + public static final int ACCESSIBILITY_FOCUS_IN = 0x00000004 | FOCUS_ACCESSIBILITY; + + /** + * Use with {@link #focusSearch(int)}. Move acessibility focus out of a view. + */ + public static final int ACCESSIBILITY_FOCUS_OUT = 0x00000008 | FOCUS_ACCESSIBILITY; + /** * Bits of {@link #getMeasuredWidthAndState()} and * {@link #getMeasuredWidthAndState()} that provide the actual measured size. @@ -1330,7 +1386,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal R.attr.state_accelerated, VIEW_STATE_ACCELERATED, R.attr.state_hovered, VIEW_STATE_HOVERED, R.attr.state_drag_can_accept, VIEW_STATE_DRAG_CAN_ACCEPT, - R.attr.state_drag_hovered, VIEW_STATE_DRAG_HOVERED, + R.attr.state_drag_hovered, VIEW_STATE_DRAG_HOVERED }; static { @@ -1452,7 +1508,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED - | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED; + | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED + | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED; /** * Temporary Rect currently for use in setBackground(). This will probably @@ -1784,7 +1841,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ private static final int LAYOUT_DIRECTION_DEFAULT = LAYOUT_DIRECTION_INHERIT; - /** * Indicates that the view is tracking some sort of transient state * that the app should not need to be aware of, but that the framework @@ -1992,6 +2048,50 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal public static final int TEXT_ALIGNMENT_RESOLVED_DEFAULT = TEXT_ALIGNMENT_GRAVITY << TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT; + // Accessiblity constants for mPrivateFlags2 + + /** + * Shift for accessibility related bits in {@link #mPrivateFlags2}. + */ + static final int IMPORTANT_FOR_ACCESSIBILITY_SHIFT = 20; + + /** + * Automatically determine whether a view is important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_AUTO = 0x00000000; + + /** + * The view is important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_YES = 0x00000001; + + /** + * The view is not important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_NO = 0x00000002; + + /** + * The default whether the view is important for accessiblity. + */ + static final int IMPORTANT_FOR_ACCESSIBILITY_DEFAULT = IMPORTANT_FOR_ACCESSIBILITY_AUTO; + + /** + * Mask for obtainig the bits which specify how to determine + * whether a view is important for accessibility. + */ + static final int IMPORTANT_FOR_ACCESSIBILITY_MASK = (IMPORTANT_FOR_ACCESSIBILITY_AUTO + | IMPORTANT_FOR_ACCESSIBILITY_YES | IMPORTANT_FOR_ACCESSIBILITY_NO) + << IMPORTANT_FOR_ACCESSIBILITY_SHIFT; + + /** + * Flag indicating whether a view has accessibility focus. + */ + static final int ACCESSIBILITY_FOCUSED = 0x00000040 << IMPORTANT_FOR_ACCESSIBILITY_SHIFT; + + /** + * Flag indicating whether a view state for accessibility has changed. + */ + static final int ACCESSIBILITY_STATE_CHANGED = 0x00000080 << IMPORTANT_FOR_ACCESSIBILITY_SHIFT; /* End of masks for mPrivateFlags2 */ @@ -2952,7 +3052,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal // Set layout and text direction defaults mPrivateFlags2 = (LAYOUT_DIRECTION_DEFAULT << LAYOUT_DIRECTION_MASK_SHIFT) | (TEXT_DIRECTION_DEFAULT << TEXT_DIRECTION_MASK_SHIFT) | - (TEXT_ALIGNMENT_DEFAULT << TEXT_ALIGNMENT_MASK_SHIFT); + (TEXT_ALIGNMENT_DEFAULT << TEXT_ALIGNMENT_MASK_SHIFT) | + (IMPORTANT_FOR_ACCESSIBILITY_DEFAULT << IMPORTANT_FOR_ACCESSIBILITY_SHIFT); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS); mUserPaddingStart = -1; @@ -3340,6 +3441,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal final int textAlignment = a.getInt(attr, TEXT_ALIGNMENT_DEFAULT); mPrivateFlags2 |= TEXT_ALIGNMENT_FLAGS[textAlignment]; break; + case R.styleable.View_importantForAccessibility: + setImportantForAccessibility(a.getInt(attr, + IMPORTANT_FOR_ACCESSIBILITY_DEFAULT)); } } @@ -3970,6 +4074,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal onFocusChanged(true, direction, previouslyFocusedRect); refreshDrawableState(); + + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + notifyAccessibilityStateChanged(); + } } } @@ -4050,16 +4158,21 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } onFocusChanged(false, 0, null); + refreshDrawableState(); ensureInputFocusOnFirstFocusable(); + + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + notifyAccessibilityStateChanged(); + } } } void ensureInputFocusOnFirstFocusable() { View root = getRootView(); if (root != null) { - root.requestFocus(FOCUS_FORWARD); + root.requestFocus(); } } @@ -4077,6 +4190,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal onFocusChanged(false, 0, null); refreshDrawableState(); + + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + notifyAccessibilityStateChanged(); + } } } @@ -4127,7 +4244,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { if (gainFocus) { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + requestAccessibilityFocus(); + } } InputMethodManager imm = InputMethodManager.peekInstance(); @@ -4237,7 +4357,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { if (mAccessibilityDelegate != null) { - mAccessibilityDelegate.sendAccessibilityEventUnchecked(this, event); + mAccessibilityDelegate.sendAccessibilityEventUnchecked(this, event); } else { sendAccessibilityEventUncheckedInternal(event); } @@ -4257,6 +4377,31 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal if ((event.getEventType() & POPULATING_ACCESSIBILITY_EVENT_TYPES) != 0) { dispatchPopulateAccessibilityEvent(event); } + // Intercept accessibility focus events fired by virtual nodes to keep + // track of accessibility focus position in such nodes. + final int eventType = event.getEventType(); + switch (eventType) { + case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: { + final long virtualNodeId = AccessibilityNodeInfo.getVirtualDescendantId( + event.getSourceNodeId()); + if (virtualNodeId != AccessibilityNodeInfo.UNDEFINED) { + ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.setAccessibilityFocusedHost(this); + } + } + } break; + case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: { + final long virtualNodeId = AccessibilityNodeInfo.getVirtualDescendantId( + event.getSourceNodeId()); + if (virtualNodeId != AccessibilityNodeInfo.UNDEFINED) { + ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.setAccessibilityFocusedHost(null); + } + } + } break; + } // In the beginning we called #isShown(), so we know that getParent() is not null. getParent().requestSendAccessibilityEvent(this, event); } @@ -4399,7 +4544,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal event.setContentDescription(mContentDescription); if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && mAttachInfo != null) { - ArrayList<View> focusablesTempList = mAttachInfo.mFocusablesTempList; + ArrayList<View> focusablesTempList = mAttachInfo.mTempArrayList; getRootView().addFocusables(focusablesTempList, View.FOCUS_FORWARD, FOCUSABLES_ALL); event.setItemCount(focusablesTempList.size()); @@ -4488,10 +4633,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal info.setBoundsInScreen(bounds); if ((mPrivateFlags & IS_ROOT_NAMESPACE) == 0) { - ViewParent parent = getParent(); + ViewParent parent = getParentForAccessibility(); if (parent instanceof View) { - View parentView = (View) parent; - info.setParent(parentView); + info.setParent((View) parent); } } @@ -4503,6 +4647,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal info.setClickable(isClickable()); info.setFocusable(isFocusable()); info.setFocused(isFocused()); + info.setAccessibilityFocused(isAccessibilityFocused()); info.setSelected(isSelected()); info.setLongClickable(isLongClickable()); @@ -4597,10 +4742,11 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * true for views that do not have textual representation (For example, * ImageButton). * - * @return The content descriptiopn. + * @return The content description. * * @attr ref android.R.styleable#View_contentDescription */ + @ViewDebug.ExportedProperty(category = "accessibility") public CharSequence getContentDescription() { return mContentDescription; } @@ -5650,8 +5796,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * Adds any focusable views that are descendants of this view (possibly * including this view if it is focusable itself) to views. This method * adds all focusable views regardless if we are in touch mode or - * only views focusable in touch mode if we are in touch mode depending on - * the focusable mode paramater. + * only views focusable in touch mode if we are in touch mode or + * only views that can take accessibility focus if accessibility is enabeld + * depending on the focusable mode paramater. * * @param views Focusable views found so far or null if all we are interested is * the number of focusables. @@ -5660,19 +5807,32 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * * @see #FOCUSABLES_ALL * @see #FOCUSABLES_TOUCH_MODE + * @see #FOCUSABLES_ACCESSIBILITY */ public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { - if (!isFocusable()) { + if (views == null) { return; } - - if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && - isInTouchMode() && !isFocusableInTouchMode()) { - return; + if ((focusableMode & FOCUSABLE_IN_TOUCH_MODE) == FOCUSABLE_IN_TOUCH_MODE) { + if (isFocusable() && (!isInTouchMode() || isFocusableInTouchMode())) { + views.add(this); + return; + } } - - if (views != null) { - views.add(this); + if ((focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY) { + if (AccessibilityManager.getInstance(mContext).isEnabled() + && includeForAccessibility()) { + views.add(this); + return; + } + } + if ((focusableMode & FOCUSABLES_ALL) == FOCUSABLES_ALL) { + if (isFocusable()) { + views.add(this); + return; + } + } else { + throw new IllegalArgumentException("Unknow focusable mode: " + focusableMode); } } @@ -5734,6 +5894,149 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** + * Returns whether this View is accessibility focused. + * + * @return True if this View is accessibility focused. + */ + boolean isAccessibilityFocused() { + return (mPrivateFlags2 & ACCESSIBILITY_FOCUSED) != 0; + } + + /** + * Call this to try to give accessibility focus to this view. + * + * A view will not actually take focus if {@link AccessibilityManager#isEnabled()} + * returns false or the view is no visible or the view already has accessibility + * focus. + * + * See also {@link #focusSearch(int)}, which is what you call to say that you + * have focus, and you want your parent to look for the next one. + * + * @return Whether this view actually took accessibility focus. + * + * @hide + */ + public boolean requestAccessibilityFocus() { + if (!AccessibilityManager.getInstance(mContext).isEnabled()) { + return false; + } + if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) { + return false; + } + if ((mPrivateFlags2 & ACCESSIBILITY_FOCUSED) == 0) { + mPrivateFlags2 |= ACCESSIBILITY_FOCUSED; + ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.setAccessibilityFocusedHost(this); + } + invalidate(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + notifyAccessibilityStateChanged(); + // Try to give input focus to this view - not a descendant. + requestFocusNoSearch(View.FOCUS_DOWN, null); + return true; + } + return false; + } + + /** + * Call this to try to clear accessibility focus of this view. + * + * See also {@link #focusSearch(int)}, which is what you call to say that you + * have focus, and you want your parent to look for the next one. + * + * @hide + */ + public void clearAccessibilityFocus() { + if ((mPrivateFlags2 & ACCESSIBILITY_FOCUSED) != 0) { + mPrivateFlags2 &= ~ACCESSIBILITY_FOCUSED; + ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.setAccessibilityFocusedHost(null); + } + invalidate(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + notifyAccessibilityStateChanged(); + // Try to move accessibility focus to the input focus. + View rootView = getRootView(); + if (rootView != null) { + View inputFocus = rootView.findFocus(); + if (inputFocus != null) { + inputFocus.requestAccessibilityFocus(); + } + } + } + } + + /** + * Find the best view to take accessibility focus from a hover. + * This function finds the deepest actionable view and if that + * fails ask the parent to take accessibility focus from hover. + * + * @param x The X hovered location in this view coorditantes. + * @param y The Y hovered location in this view coorditantes. + * @return Whether the request was handled. + * + * @hide + */ + public boolean requestAccessibilityFocusFromHover(float x, float y) { + if (onRequestAccessibilityFocusFromHover(x, y)) { + return true; + } + ViewParent parent = mParent; + if (parent instanceof View) { + View parentView = (View) parent; + + float[] position = mAttachInfo.mTmpTransformLocation; + position[0] = x; + position[1] = y; + + // Compensate for the transformation of the current matrix. + if (!hasIdentityMatrix()) { + getMatrix().mapPoints(position); + } + + // Compensate for the parent scroll and the offset + // of this view stop from the parent top. + position[0] += mLeft - parentView.mScrollX; + position[1] += mTop - parentView.mScrollY; + + return parentView.requestAccessibilityFocusFromHover(position[0], position[1]); + } + return false; + } + + /** + * Requests to give this View focus from hover. + * + * @param x The X hovered location in this view coorditantes. + * @param y The Y hovered location in this view coorditantes. + * @return Whether the request was handled. + * + * @hide + */ + public boolean onRequestAccessibilityFocusFromHover(float x, float y) { + if (includeForAccessibility() + && (isActionableForAccessibility() || hasListenersForAccessibility())) { + return requestAccessibilityFocus(); + } + return false; + } + + /** + * Clears accessibility focus without calling any callback methods + * normally invoked in {@link #clearAccessibilityFocus()}. This method + * is used for clearing accessibility focus when giving this focus to + * another view. + */ + void clearAccessibilityFocusNoCallbacks() { + if ((mPrivateFlags2 & ACCESSIBILITY_FOCUSED) != 0) { + mPrivateFlags2 &= ~ACCESSIBILITY_FOCUSED; + invalidate(); + } + } + + /** * Call this to try to give focus to a specific view or to one of its * descendants. * @@ -5753,7 +6056,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal return requestFocus(View.FOCUS_DOWN); } - /** * Call this to try to give focus to a specific view or to one of its * descendants and give it a hint about what direction focus is heading. @@ -5805,6 +6107,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * @return Whether this view or one of its descendants actually took focus. */ public boolean requestFocus(int direction, Rect previouslyFocusedRect) { + return requestFocusNoSearch(direction, previouslyFocusedRect); + } + + private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) { // need to be focusable if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE || (mViewFlags & VISIBILITY_MASK) != VISIBLE) { @@ -5864,6 +6170,248 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } /** + * Gets the mode for determining whether this View is important for accessibility + * which is if it fires accessibility events and if it is reported to + * accessibility services that query the screen. + * + * @return The mode for determining whether a View is important for accessibility. + * + * @attr ref android.R.styleable#View_importantForAccessibility + * + * @see #IMPORTANT_FOR_ACCESSIBILITY_YES + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO + * @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO + */ + @ViewDebug.ExportedProperty(category = "accessibility", mapping = { + @ViewDebug.IntToString(from = IMPORTANT_FOR_ACCESSIBILITY_AUTO, + to = "IMPORTANT_FOR_ACCESSIBILITY_AUTO"), + @ViewDebug.IntToString(from = IMPORTANT_FOR_ACCESSIBILITY_YES, + to = "IMPORTANT_FOR_ACCESSIBILITY_YES"), + @ViewDebug.IntToString(from = IMPORTANT_FOR_ACCESSIBILITY_NO, + to = "IMPORTANT_FOR_ACCESSIBILITY_NO") + }) + public int getImportantForAccessibility() { + return (mPrivateFlags2 & IMPORTANT_FOR_ACCESSIBILITY_MASK) + >> IMPORTANT_FOR_ACCESSIBILITY_SHIFT; + } + + /** + * Sets how to determine whether this view is important for accessibility + * which is if it fires accessibility events and if it is reported to + * accessibility services that query the screen. + * + * @param mode How to determine whether this view is important for accessibility. + * + * @attr ref android.R.styleable#View_importantForAccessibility + * + * @see #IMPORTANT_FOR_ACCESSIBILITY_YES + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO + * @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO + */ + public void setImportantForAccessibility(int mode) { + if (mode != getImportantForAccessibility()) { + mPrivateFlags2 &= ~IMPORTANT_FOR_ACCESSIBILITY_MASK; + mPrivateFlags2 |= (mode << IMPORTANT_FOR_ACCESSIBILITY_SHIFT) + & IMPORTANT_FOR_ACCESSIBILITY_MASK; + notifyAccessibilityStateChanged(); + } + } + + /** + * Gets whether this view should be exposed for accessibility. + * + * @return Whether the view is exposed for accessibility. + * + * @hide + */ + public boolean isImportantForAccessibility() { + final int mode = (mPrivateFlags2 & IMPORTANT_FOR_ACCESSIBILITY_MASK) + >> IMPORTANT_FOR_ACCESSIBILITY_SHIFT; + switch (mode) { + case IMPORTANT_FOR_ACCESSIBILITY_YES: + return true; + case IMPORTANT_FOR_ACCESSIBILITY_NO: + return false; + case IMPORTANT_FOR_ACCESSIBILITY_AUTO: + return isActionableForAccessibility() || hasListenersForAccessibility(); + default: + throw new IllegalArgumentException("Unknow important for accessibility mode: " + + mode); + } + } + + /** + * Gets the parent for accessibility purposes. Note that the parent for + * accessibility is not necessary the immediate parent. It is the first + * predecessor that is important for accessibility. + * + * @return The parent for accessibility purposes. + */ + public ViewParent getParentForAccessibility() { + if (mParent instanceof View) { + View parentView = (View) mParent; + if (parentView.includeForAccessibility()) { + return mParent; + } else { + return mParent.getParentForAccessibility(); + } + } + return null; + } + + /** + * Adds the children of a given View for accessibility. Since some Views are + * not important for accessibility the children for accessibility are not + * necessarily direct children of the riew, rather they are the first level of + * descendants important for accessibility. + * + * @param children The list of children for accessibility. + */ + public void addChildrenForAccessibility(ArrayList<View> children) { + if (includeForAccessibility()) { + children.add(this); + } + } + + /** + * Whether to regard this view for accessibility. A view is regarded for + * accessibility if it is important for accessibility or the querying + * accessibility service has explicitly requested that view not + * important for accessibility are regarded. + * + * @return Whether to regard the view for accessibility. + */ + boolean includeForAccessibility() { + if (mAttachInfo != null) { + if (!mAttachInfo.mIncludeNotImportantViews) { + return isImportantForAccessibility(); + } else { + return true; + } + } + return false; + } + + /** + * Returns whether the View is considered actionable from + * accessibility perspective. Such view are important for + * accessiiblity. + * + * @return True if the view is actionable for accessibility. + */ + private boolean isActionableForAccessibility() { + return (isClickable() || isLongClickable() || isFocusable()); + } + + /** + * Returns whether the View has registered callbacks wich makes it + * important for accessiiblity. + * + * @return True if the view is actionable for accessibility. + */ + private boolean hasListenersForAccessibility() { + ListenerInfo info = getListenerInfo(); + return mTouchDelegate != null || info.mOnKeyListener != null + || info.mOnTouchListener != null || info.mOnGenericMotionListener != null + || info.mOnHoverListener != null || info.mOnDragListener != null; + } + + /** + * Notifies accessibility services that some view's important for + * accessibility state has changed. Note that such notifications + * are made at most once every + * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()} + * to avoid unnecessary load to the system. Also once a view has + * made a notifucation this method is a NOP until the notification has + * been sent to clients. + * + * @hide + * + * TODO: Makse sure this method is called for any view state change + * that is interesting for accessilility purposes. + */ + public void notifyAccessibilityStateChanged() { + if ((mPrivateFlags2 & ACCESSIBILITY_STATE_CHANGED) == 0) { + mPrivateFlags2 |= ACCESSIBILITY_STATE_CHANGED; + if (mParent != null) { + mParent.childAccessibilityStateChanged(this); + } + } + } + + /** + * Reset the state indicating the this view has requested clients + * interested in its accessiblity state to be notified. + * + * @hide + */ + public void resetAccessibilityStateChanged() { + mPrivateFlags2 &= ~ACCESSIBILITY_STATE_CHANGED; + } + + /** + * Performs the specified accessibility action on the view. For + * possible accessibility actions look at {@link AccessibilityNodeInfo}. + * + * @param action The action to perform. + * @return Whether the action was performed. + */ + public boolean performAccessibilityAction(int action) { + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: { + final long now = SystemClock.uptimeMillis(); + // Send down. + MotionEvent event = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, + getWidth() / 2, getHeight() / 2, 0); + onTouchEvent(event); + // Send up. + event.setAction(MotionEvent.ACTION_UP); + onTouchEvent(event); + // Clean up. + event.recycle(); + } break; + case AccessibilityNodeInfo.ACTION_FOCUS: { + if (!hasFocus()) { + // Get out of touch mode since accessibility + // wants to move focus around. + getViewRootImpl().ensureTouchMode(false); + return requestFocus(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { + if (hasFocus()) { + clearFocus(); + return !isFocused(); + } + } break; + case AccessibilityNodeInfo.ACTION_SELECT: { + if (!isSelected()) { + setSelected(true); + return isSelected(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: { + if (isSelected()) { + setSelected(false); + return !isSelected(); + } + } break; + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { + if (!isAccessibilityFocused()) { + return requestAccessibilityFocus(); + } + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { + if (isAccessibilityFocused()) { + clearAccessibilityFocus(); + return true; + } + } break; + } + return false; + } + + /** * @hide */ public void dispatchStartTemporaryDetach() { @@ -6757,21 +7305,27 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal // The root view may receive hover (or touch) events that are outside the bounds of // the window. This code ensures that we only send accessibility events for // hovers that are actually within the bounds of the root view. - final int action = event.getAction(); + final int action = event.getActionMasked(); if (!mSendingHoverAccessibilityEvents) { if ((action == MotionEvent.ACTION_HOVER_ENTER || action == MotionEvent.ACTION_HOVER_MOVE) && !hasHoveredChild() && pointInView(event.getX(), event.getY())) { - mSendingHoverAccessibilityEvents = true; sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + mSendingHoverAccessibilityEvents = true; + requestAccessibilityFocusFromHover((int) event.getX(), (int) event.getY()); } } else { if (action == MotionEvent.ACTION_HOVER_EXIT - || (action == MotionEvent.ACTION_HOVER_MOVE + || (action == MotionEvent.ACTION_MOVE && !pointInView(event.getX(), event.getY()))) { mSendingHoverAccessibilityEvents = false; sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + // If the window does not have input focus we take away accessibility + // focus as soon as the user stop hovering over the view. + if (!mAttachInfo.mHasWindowFocus) { + getViewRootImpl().setAccessibilityFocusedHost(null); + } } } @@ -6795,6 +7349,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal dispatchGenericMotionEventInternal(event); return true; } + return false; } @@ -6806,7 +7361,6 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ private boolean isHoverable() { final int viewFlags = mViewFlags; - //noinspection SimplifiableIfStatement if ((viewFlags & ENABLED_MASK) == DISABLED) { return false; } @@ -7130,6 +7684,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ if (mParent != null) mParent.focusableViewAvailable(this); } + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + notifyAccessibilityStateChanged(); + } } if ((flags & VISIBILITY_MASK) == VISIBLE) { @@ -7161,6 +7718,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal if (((mViewFlags & VISIBILITY_MASK) == GONE)) { if (hasFocus()) clearFocus(); + clearAccessibilityFocus(); destroyDrawingCache(); if (mParent instanceof View) { // GONE views noop invalidation, so invalidate the parent @@ -7185,9 +7743,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal mPrivateFlags |= DRAWN; if (((mViewFlags & VISIBILITY_MASK) == INVISIBLE) && hasFocus()) { - // root view becoming invisible shouldn't clear focus + // root view becoming invisible shouldn't clear focus and accessibility focus if (getRootView() != this) { clearFocus(); + clearAccessibilityFocus(); } } if (mAttachInfo != null) { @@ -7241,6 +7800,12 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal mParent.recomputeViewAttributes(this); } } + + if (AccessibilityManager.getInstance(mContext).isEnabled() + && ((changed & FOCUSABLE) != 0 || (changed & CLICKABLE) != 0 + || (changed & LONG_CLICKABLE) != 0 || (changed & ENABLED) != 0)) { + notifyAccessibilityStateChanged(); + } } /** @@ -7319,6 +7884,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * @param canvas the canvas on which to draw the view */ protected void dispatchDraw(Canvas canvas) { + } /** @@ -10227,6 +10793,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal resolvePadding(); resolveTextDirection(); resolveTextAlignment(); + clearAccessibilityFocus(); if (isFocused()) { InputMethodManager imm = InputMethodManager.peekInstance(); imm.focusIn(this); @@ -10457,6 +11024,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal resetResolvedLayoutDirection(); resetResolvedTextAlignment(); + resetAccessibilityStateChanged(); + clearAccessibilityFocus(); } /** @@ -13287,6 +13856,9 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal invalidate(true); refreshDrawableState(); dispatchSetSelected(selected); + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + notifyAccessibilityStateChanged(); + } } } @@ -13456,7 +14028,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal position[1] += view.mTop; viewParent = view.mParent; - } + } if (viewParent instanceof ViewRootImpl) { // *cough* @@ -16291,7 +16863,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal /** * Temporary list for use in collecting focusable descendents of a view. */ - final ArrayList<View> mFocusablesTempList = new ArrayList<View>(24); + final ArrayList<View> mTempArrayList = new ArrayList<View>(24); /** * The id of the window for accessibility purposes. @@ -16299,6 +16871,17 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal int mAccessibilityWindowId = View.NO_ID; /** + * Whether to ingore not exposed for accessibility Views when + * reporting the view tree to accessibility services. + */ + boolean mIncludeNotImportantViews; + + /** + * The drawable for highlighting accessibility focus. + */ + Drawable mAccessibilityFocusDrawable; + + /** * Creates a new set of attachment information with the specified * events handler and thread. * diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 9d06145..9134966 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -197,7 +197,7 @@ public class ViewConfiguration { * gesture and the touch up event of a subsequent tap for the latter tap to be * considered as a tap i.e. to perform a click. */ - private static final int TOUCH_EXPLORATION_TAP_SLOP = 80; + private static final int TOUCH_EXPLORE_TAP_SLOP = 80; /** * Delay before dispatching a recurring accessibility event in milliseconds. @@ -238,7 +238,7 @@ public class ViewConfiguration { private final int mDoubleTapTouchSlop; private final int mPagingTouchSlop; private final int mDoubleTapSlop; - private final int mScaledTouchExplorationTapSlop; + private final int mScaledTouchExploreTapSlop; private final int mWindowTouchSlop; private final int mMaximumDrawingCacheSize; private final int mOverscrollDistance; @@ -265,7 +265,7 @@ public class ViewConfiguration { mDoubleTapTouchSlop = DOUBLE_TAP_TOUCH_SLOP; mPagingTouchSlop = PAGING_TOUCH_SLOP; mDoubleTapSlop = DOUBLE_TAP_SLOP; - mScaledTouchExplorationTapSlop = TOUCH_EXPLORATION_TAP_SLOP; + mScaledTouchExploreTapSlop = TOUCH_EXPLORE_TAP_SLOP; mWindowTouchSlop = WINDOW_TOUCH_SLOP; //noinspection deprecation mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE; @@ -302,7 +302,7 @@ public class ViewConfiguration { mMaximumFlingVelocity = (int) (density * MAXIMUM_FLING_VELOCITY + 0.5f); mScrollbarSize = (int) (density * SCROLL_BAR_SIZE + 0.5f); mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f); - mScaledTouchExplorationTapSlop = (int) (density * TOUCH_EXPLORATION_TAP_SLOP + 0.5f); + mScaledTouchExploreTapSlop = (int) (density * TOUCH_EXPLORE_TAP_SLOP + 0.5f); mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f); final Display display = WindowManagerImpl.getDefault().getDefaultDisplay(); @@ -559,8 +559,8 @@ public class ViewConfiguration { * * @hide */ - public int getScaledTouchExplorationTapSlop() { - return mScaledTouchExplorationTapSlop; + public int getScaledTouchExploreTapSlop() { + return mScaledTouchExploreTapSlop; } /** diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 8f6badf..cb37a1c 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -141,6 +141,18 @@ public class ViewDebug { public static final String DEBUG_LATENCY_TAG = "ViewLatency"; /** + * Enables detailed logging of accessibility focus operations. + * @hide + */ + public static final boolean DEBUG_ACCESSIBILITY_FOCUS = false; + + /** + * Tag for logging of accessibility focus operations + * @hide + */ + public static final String DEBUG_ACCESSIBILITY_FOCUS_TAG = "AccessibilityFocus"; + + /** * <p>Enables or disables views consistency check. Even when this property is enabled, * view consistency checks happen only if {@link false} is set * to true. The value of this property can be configured externally in one of the diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 121b544..7e90e2b 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -45,6 +45,7 @@ import com.android.internal.R; import com.android.internal.util.Predicate; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; /** @@ -611,13 +612,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * {@inheritDoc} */ + @Override public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) { - ViewParent parent = getParent(); + ViewParent parent = mParent; if (parent == null) { return false; } final boolean propagate = onRequestSendAccessibilityEvent(child, event); - //noinspection SimplifiableIfStatement if (!propagate) { return false; } @@ -1552,6 +1553,33 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return mFirstHoverTarget != null; } + @Override + public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) { + View[] children = mChildren; + final int childrenCount = mChildrenCount; + for (int i = 0; i < childrenCount; i++) { + View child = children[i]; + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE + && (child.mPrivateFlags & IS_ROOT_NAMESPACE) == 0) { + if (child.includeForAccessibility()) { + childrenForAccessibility.add(child); + } else { + child.addChildrenForAccessibility(childrenForAccessibility); + } + } + } + } + + /** + * @hide + */ + @Override + public void childAccessibilityStateChanged(View child) { + if (mParent != null) { + mParent.childAccessibilityStateChanged(child); + } + } + /** * Implement this method to intercept hover events before they are handled * by child views. @@ -2294,33 +2322,43 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @Override boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { - boolean handled = super.dispatchPopulateAccessibilityEventInternal(event); - if (handled) { - return handled; + boolean handled = false; + if (includeForAccessibility()) { + handled = super.dispatchPopulateAccessibilityEventInternal(event); + if (handled) { + return handled; + } } // Let our children have a shot in populating the event. - for (int i = 0, count = getChildCount(); i < count; i++) { - View child = getChildAt(i); + ChildListForAccessibility children = ChildListForAccessibility.obtain(this, true); + final int childCount = children.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = children.getChildAt(i); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { - handled = getChildAt(i).dispatchPopulateAccessibilityEvent(event); + handled = child.dispatchPopulateAccessibilityEvent(event); if (handled) { + children.recycle(); return handled; } } } + children.recycle(); return false; } @Override void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfoInternal(info); - info.setClassName(ViewGroup.class.getName()); - for (int i = 0, count = mChildrenCount; i < count; i++) { - View child = mChildren[i]; - if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE - && (child.mPrivateFlags & IS_ROOT_NAMESPACE) == 0) { + if (mAttachInfo != null) { + ArrayList<View> childrenForAccessibility = mAttachInfo.mTempArrayList; + childrenForAccessibility.clear(); + addChildrenForAccessibility(childrenForAccessibility); + final int childrenForAccessibilityCount = childrenForAccessibility.size(); + for (int i = 0; i < childrenForAccessibilityCount; i++) { + View child = childrenForAccessibility.get(i); info.addChild(child); } + childrenForAccessibility.clear(); } } @@ -2331,6 +2369,20 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * @hide + */ + @Override + public void resetAccessibilityStateChanged() { + super.resetAccessibilityStateChanged(); + View[] children = mChildren; + final int childCount = mChildrenCount; + for (int i = 0; i < childCount; i++) { + View child = children[i]; + child.resetAccessibilityStateChanged(); + } + } + + /** * {@inheritDoc} */ @Override @@ -3400,6 +3452,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager clearChildFocus(view); ensureInputFocusOnFirstFocusable(); } + + if (view.isAccessibilityFocused()) { + view.clearAccessibilityFocus(); + } } /** @@ -5622,4 +5678,218 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } } + + /** + * Pooled class that orderes the children of a ViewGroup from start + * to end based on how they are laid out and the layout direction. + */ + static class ChildListForAccessibility { + + private static final int MAX_POOL_SIZE = 32; + + private static final Object sPoolLock = new Object(); + + private static ChildListForAccessibility sPool; + + private static int sPoolSize; + + private boolean mIsPooled; + + private ChildListForAccessibility mNext; + + private final ArrayList<View> mChildren = new ArrayList<View>(); + + private final ArrayList<ViewLocationHolder> mHolders = new ArrayList<ViewLocationHolder>(); + + public static ChildListForAccessibility obtain(ViewGroup parent, boolean sort) { + ChildListForAccessibility list = null; + synchronized (sPoolLock) { + if (sPool != null) { + list = sPool; + sPool = list.mNext; + list.mNext = null; + list.mIsPooled = false; + sPoolSize--; + } else { + list = new ChildListForAccessibility(); + } + list.init(parent, sort); + return list; + } + } + + public void recycle() { + if (mIsPooled) { + throw new IllegalStateException("Instance already recycled."); + } + clear(); + if (sPoolSize < MAX_POOL_SIZE) { + mNext = sPool; + mIsPooled = true; + sPool = this; + sPoolSize++; + } + } + + public int getChildCount() { + return mChildren.size(); + } + + public View getChildAt(int index) { + return mChildren.get(index); + } + + public int getChildIndex(View child) { + return mChildren.indexOf(child); + } + + private void init(ViewGroup parent, boolean sort) { + ArrayList<View> children = mChildren; + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = parent.getChildAt(i); + children.add(child); + } + if (sort) { + ArrayList<ViewLocationHolder> holders = mHolders; + for (int i = 0; i < childCount; i++) { + View child = children.get(i); + ViewLocationHolder holder = ViewLocationHolder.obtain(parent, child); + holders.add(holder); + } + Collections.sort(holders); + for (int i = 0; i < childCount; i++) { + ViewLocationHolder holder = holders.get(i); + children.set(i, holder.mView); + holder.recycle(); + } + holders.clear(); + } + } + + private void clear() { + mChildren.clear(); + } + } + + /** + * Pooled class that holds a View and its location with respect to + * a specified root. This enables sorting of views based on their + * coordinates without recomputing the position relative to the root + * on every comparison. + */ + static class ViewLocationHolder implements Comparable<ViewLocationHolder> { + + private static final int MAX_POOL_SIZE = 32; + + private static final Object sPoolLock = new Object(); + + private static ViewLocationHolder sPool; + + private static int sPoolSize; + + private boolean mIsPooled; + + private ViewLocationHolder mNext; + + private final Rect mLocation = new Rect(); + + public View mView; + + private int mLayoutDirection; + + public static ViewLocationHolder obtain(ViewGroup root, View view) { + ViewLocationHolder holder = null; + synchronized (sPoolLock) { + if (sPool != null) { + holder = sPool; + sPool = holder.mNext; + holder.mNext = null; + holder.mIsPooled = false; + sPoolSize--; + } else { + holder = new ViewLocationHolder(); + } + holder.init(root, view); + return holder; + } + } + + public void recycle() { + if (mIsPooled) { + throw new IllegalStateException("Instance already recycled."); + } + clear(); + if (sPoolSize < MAX_POOL_SIZE) { + mNext = sPool; + mIsPooled = true; + sPool = this; + sPoolSize++; + } + } + + @Override + public int compareTo(ViewLocationHolder another) { + // This instance is greater than an invalid argument. + if (another == null) { + return 1; + } + if (getClass() != another.getClass()) { + return 1; + } + // First is above second. + if (mLocation.bottom - another.mLocation.top <= 0) { + return -1; + } + // First is below second. + if (mLocation.top - another.mLocation.bottom >= 0) { + return 1; + } + // LTR + if (mLayoutDirection == LAYOUT_DIRECTION_LTR) { + final int leftDifference = mLocation.left - another.mLocation.left; + // First more to the left than second. + if (leftDifference != 0) { + return leftDifference; + } + } else { // RTL + final int rightDifference = mLocation.right - another.mLocation.right; + // First more to the right than second. + if (rightDifference != 0) { + return -rightDifference; + } + } + // Break tie by top. + final int topDiference = mLocation.top - another.mLocation.top; + if (topDiference != 0) { + return topDiference; + } + // Break tie by height. + final int heightDiference = mLocation.height() - another.mLocation.height(); + if (heightDiference != 0) { + return -heightDiference; + } + // Break tie by width. + final int widthDiference = mLocation.width() - another.mLocation.width(); + if (widthDiference != 0) { + return -widthDiference; + } + // Return nondeterministically one of them since we do + // not want to ignore any views. + return 1; + } + + private void init(ViewGroup root, View view) { + Rect viewLocation = mLocation; + view.getDrawingRect(viewLocation); + root.offsetDescendantRectToMyCoords(view, viewLocation); + mView = view; + mLayoutDirection = root.getResolvedLayoutDirection(); + } + + private void clear() { + mView = null; + mLocation.set(0, 0, 0, 0); + } + } } diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 75e9151..ddff91d 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -277,4 +277,22 @@ public interface ViewParent { * View.fitSystemWindows(Rect)} be performed. */ public void requestFitSystemWindows(); + + /** + * Gets the parent of a given View for accessibility. Since some Views are not + * exposed to the accessibility layer the parent for accessibility is not + * necessarily the direct parent of the View, rather it is a predecessor. + * + * @return The parent or <code>null</code> if no such is found. + */ + public ViewParent getParentForAccessibility(); + + /** + * A child notifies its parent that its state for accessibility has changed. + * That is some of the child properties reported to accessibility services has + * changed, hence the interested services have to be notified for the new state. + * + * @hide + */ + public void childAccessibilityStateChanged(View child); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 7a43cf1..b4554d5 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -37,6 +37,7 @@ import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Region; +import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.os.Binder; import android.os.Bundle; @@ -56,17 +57,11 @@ 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.SparseLongArray; import android.util.TypedValue; import android.view.View.AttachInfo; import android.view.View.MeasureSpec; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.view.accessibility.AccessibilityNodeInfo; @@ -79,6 +74,7 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.Scroller; +import com.android.internal.R; import com.android.internal.policy.PolicyManager; import com.android.internal.view.BaseSurfaceHolder; import com.android.internal.view.IInputMethodCallback; @@ -89,9 +85,7 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.HashSet; /** * The top of a view hierarchy, implementing the needed protocol between View @@ -181,6 +175,10 @@ public final class ViewRootImpl implements ViewParent, View mFocusedView; View mRealFocusedView; // this is not set to null in touch mode View mOldFocusedView; + + View mAccessibilityFocusedHost; + AccessibilityNodeInfo mAccessibilityFocusedVirtualView; + int mViewVisibility; boolean mAppVisible = true; int mOrigWindowType = -1; @@ -321,7 +319,7 @@ public final class ViewRootImpl implements ViewParent, SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent; - AccessibilityNodePrefetcher mAccessibilityNodePrefetcher; + HashSet<View> mTempHashSet; private final int mDensity; @@ -630,6 +628,10 @@ public final class ViewRootImpl implements ViewParent, if (mAccessibilityManager.isEnabled()) { mAccessibilityInteractionConnectionManager.ensureConnection(); } + + if (view.getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } } } } @@ -1418,6 +1420,8 @@ public final class ViewRootImpl implements ViewParent, mView.draw(layerCanvas); + drawAccessibilityFocusedDrawableIfNeeded(layerCanvas); + mResizeBufferStartTime = SystemClock.uptimeMillis(); mResizeBufferDuration = mView.getResources().getInteger( com.android.internal.R.integer.config_mediumAnimTime); @@ -1712,7 +1716,7 @@ public final class ViewRootImpl implements ViewParent, attachInfo.mTreeObserver.dispatchOnGlobalLayout(); if (AccessibilityManager.getInstance(host.mContext).isEnabled()) { - postSendWindowContentChangedCallback(); + postSendWindowContentChangedCallback(mView); } } @@ -1880,6 +1884,7 @@ public final class ViewRootImpl implements ViewParent, mResizePaint.setAlpha(mResizeAlpha); canvas.drawHardwareLayer(mResizeBuffer, 0.0f, mHardwareYOffset, mResizePaint); } + drawAccessibilityFocusedDrawableIfNeeded(canvas); } /** @@ -2234,6 +2239,8 @@ public final class ViewRootImpl implements ViewParent, mView.draw(canvas); + drawAccessibilityFocusedDrawableIfNeeded(canvas); + if (ViewDebug.DEBUG_LATENCY) { long now = System.nanoTime(); Log.d(ViewDebug.DEBUG_LATENCY_TAG, "- draw() took " @@ -2274,6 +2281,64 @@ public final class ViewRootImpl implements ViewParent, return true; } + /** + * We want to draw a highlight around the current accessibility focused. + * Since adding a style for all possible view is not a viable option we + * have this specialized drawing method. + * + * Note: We are doing this here to be able to draw the highlight for + * virtual views in addition to real ones. + * + * @param canvas The canvas on which to draw. + */ + private void drawAccessibilityFocusedDrawableIfNeeded(Canvas canvas) { + if (!AccessibilityManager.getInstance(mView.mContext).isEnabled()) { + return; + } + if (mAccessibilityFocusedHost == null || mAccessibilityFocusedHost.mAttachInfo == null) { + return; + } + Drawable drawable = getAccessibilityFocusedDrawable(); + if (drawable == null) { + return; + } + AccessibilityNodeProvider provider = + mAccessibilityFocusedHost.getAccessibilityNodeProvider(); + Rect bounds = mView.mAttachInfo.mTmpInvalRect; + if (provider == null) { + mAccessibilityFocusedHost.getDrawingRect(bounds); + if (mView instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) mView; + viewGroup.offsetDescendantRectToMyCoords(mAccessibilityFocusedHost, bounds); + } + } else { + if (mAccessibilityFocusedVirtualView == null) { + mAccessibilityFocusedVirtualView = provider.findAccessibilitiyFocus(View.NO_ID); + } + mAccessibilityFocusedVirtualView.getBoundsInScreen(bounds); + bounds.offset(-mAttachInfo.mWindowLeft, -mAttachInfo.mWindowTop); + } + drawable.setBounds(bounds); + drawable.draw(canvas); + } + + private Drawable getAccessibilityFocusedDrawable() { + if (mAttachInfo != null) { + // Lazily load the accessibility focus drawable. + if (mAttachInfo.mAccessibilityFocusDrawable == null) { + TypedValue value = new TypedValue(); + final boolean resolved = mView.mContext.getTheme().resolveAttribute( + R.attr.accessibilityFocusedDrawable, value, true); + if (resolved) { + mAttachInfo.mAccessibilityFocusDrawable = + mView.mContext.getResources().getDrawable(value.resourceId); + } + } + return mAttachInfo.mAccessibilityFocusDrawable; + } + return null; + } + void invalidateDisplayLists() { final ArrayList<DisplayList> displayLists = mDisplayLists; final int count = displayLists.size(); @@ -2407,6 +2472,14 @@ public final class ViewRootImpl implements ViewParent, return handled; } + void setAccessibilityFocusedHost(View host) { + if (mAccessibilityFocusedHost != null && mAccessibilityFocusedVirtualView == null) { + mAccessibilityFocusedHost.clearAccessibilityFocusNoCallbacks(); + } + mAccessibilityFocusedHost = host; + mAccessibilityFocusedVirtualView = null; + } + public void requestChildFocus(View child, View focused) { checkThread(); @@ -2437,9 +2510,13 @@ public final class ViewRootImpl implements ViewParent, mFocusedView = mRealFocusedView = null; } + @Override + public ViewParent getParentForAccessibility() { + return null; + } + public void focusableViewAvailable(View v) { checkThread(); - if (mView != null) { if (!mView.hasFocus()) { v.requestFocus(); @@ -2547,7 +2624,7 @@ public final class ViewRootImpl implements ViewParent, /** * Return true if child is an ancestor of parent, (or equal to the parent). */ - private static boolean isViewDescendantOf(View child, View parent) { + static boolean isViewDescendantOf(View child, View parent) { if (child == parent) { return true; } @@ -2585,13 +2662,9 @@ public final class ViewRootImpl implements ViewParent, private final static int MSG_DISPATCH_DRAG_LOCATION_EVENT = 16; private final static int MSG_DISPATCH_SYSTEM_UI_VISIBILITY = 17; private final static int MSG_UPDATE_CONFIGURATION = 18; - private final static int MSG_PERFORM_ACCESSIBILITY_ACTION = 19; - private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 20; - private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID = 21; - private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 22; - private final static int MSG_PROCESS_INPUT_EVENTS = 23; - private final static int MSG_DISPATCH_SCREEN_STATE = 24; - private final static int MSG_INVALIDATE_DISPLAY_LIST = 25; + private final static int MSG_PROCESS_INPUT_EVENTS = 19; + private final static int MSG_DISPATCH_SCREEN_STATE = 20; + private final static int MSG_INVALIDATE_DISPLAY_LIST = 21; final class ViewRootHandler extends Handler { @Override @@ -2633,14 +2706,6 @@ public final class ViewRootImpl implements ViewParent, return "MSG_DISPATCH_SYSTEM_UI_VISIBILITY"; case MSG_UPDATE_CONFIGURATION: return "MSG_UPDATE_CONFIGURATION"; - case MSG_PERFORM_ACCESSIBILITY_ACTION: - return "MSG_PERFORM_ACCESSIBILITY_ACTION"; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: - return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID"; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: - return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID"; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: - return "MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT"; case MSG_PROCESS_INPUT_EVENTS: return "MSG_PROCESS_INPUT_EVENTS"; case MSG_DISPATCH_SCREEN_STATE: @@ -2770,8 +2835,28 @@ public final class ViewRootImpl implements ViewParent, mHasHadWindowFocus = true; } - if (hasWindowFocus && mView != null && mAccessibilityManager.isEnabled()) { - mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + if (mView != null && mAccessibilityManager.isEnabled()) { + if (hasWindowFocus) { + mView.sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + // Give accessibility focus to the view that has input + // focus if such, otherwise to the first one. + if (mView instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) mView; + View focused = viewGroup.findFocus(); + if (focused != null) { + focused.requestAccessibilityFocus(); + } + } + // There is no accessibility focus, despite our effort + // above, now just give it to the first view. + if (mAccessibilityFocusedHost == null) { + mView.requestAccessibilityFocus(); + } + } else { + // Clear accessibility focus when the window loses input focus. + setAccessibilityFocusedHost(null); + } } } } break; @@ -2828,30 +2913,6 @@ public final class ViewRootImpl implements ViewParent, } updateConfiguration(config, false); } break; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: { - if (mView != null) { - getAccessibilityInteractionController() - .findAccessibilityNodeInfoByAccessibilityIdUiThread(msg); - } - } break; - case MSG_PERFORM_ACCESSIBILITY_ACTION: { - if (mView != null) { - getAccessibilityInteractionController() - .perfromAccessibilityActionUiThread(msg); - } - } break; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: { - if (mView != null) { - getAccessibilityInteractionController() - .findAccessibilityNodeInfoByViewIdUiThread(msg); - } - } break; - case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: { - if (mView != null) { - getAccessibilityInteractionController() - .findAccessibilityNodeInfosByTextUiThread(msg); - } - } break; case MSG_DISPATCH_SCREEN_STATE: { if (mView != null) { handleScreenStateChange(msg.arg1 == 1); @@ -2917,28 +2978,25 @@ public final class ViewRootImpl implements ViewParent, // set yet. final View focused = mView.findFocus(); if (focused != null && !focused.isFocusableInTouchMode()) { - final ViewGroup ancestorToTakeFocus = findAncestorToTakeFocusInTouchMode(focused); if (ancestorToTakeFocus != null) { // there is an ancestor that wants focus after its descendants that // is focusable in touch mode.. give it focus return ancestorToTakeFocus.requestFocus(); - } else { - // nothing appropriate to have focus in touch mode, clear it out - mView.unFocus(); - mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused, null); - mFocusedView = null; - mOldFocusedView = null; - return true; } } + // nothing appropriate to have focus in touch mode, clear it out + mView.unFocus(); + mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused, null); + mFocusedView = null; + mOldFocusedView = null; + return true; } } return false; } - /** * Find an ancestor of focused that wants focus after its descendants and is * focusable in touch mode. @@ -2964,25 +3022,45 @@ public final class ViewRootImpl implements ViewParent, private boolean leaveTouchMode() { if (mView != null) { + boolean inputFocusValid = false; if (mView.hasFocus()) { // i learned the hard way to not trust mFocusedView :) mFocusedView = mView.findFocus(); if (!(mFocusedView instanceof ViewGroup)) { // some view has focus, let it keep it - return false; - } else if (((ViewGroup)mFocusedView).getDescendantFocusability() != + inputFocusValid = true; + } else if (((ViewGroup) mFocusedView).getDescendantFocusability() != ViewGroup.FOCUS_AFTER_DESCENDANTS) { // some view group has focus, and doesn't prefer its children // over itself for focus, so let them keep it. - return false; + inputFocusValid = true; } } - - // find the best view to give focus to in this brave new non-touch-mode - // world - final View focused = focusSearch(null, View.FOCUS_DOWN); - if (focused != null) { - return focused.requestFocus(View.FOCUS_DOWN); + // In accessibility mode we always have a view that has the + // accessibility focus and input focus follows it, i.e. we + // try to give input focus to the accessibility focused view. + if (!AccessibilityManager.getInstance(mView.mContext).isEnabled()) { + // If the current input focus is not valid, find the best view to give + // focus to in this brave new non-touch-mode world. + if (!inputFocusValid) { + final View focused = focusSearch(null, View.FOCUS_DOWN); + if (focused != null) { + return focused.requestFocus(View.FOCUS_DOWN); + } + } + } else { + // If the current input focus is not valid clear it but do not + // give it to another view since the accessibility focus is + // leading now and the input one follows. + if (!inputFocusValid) { + if (mFocusedView != null) { + mView.unFocus(); + mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mFocusedView, null); + mFocusedView = null; + mOldFocusedView = null; + return true; + } + } } } return false; @@ -3487,37 +3565,36 @@ public final class ViewRootImpl implements ViewParent, if (event.getAction() == KeyEvent.ACTION_DOWN) { int direction = 0; switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (event.hasNoModifiers()) { - direction = View.FOCUS_LEFT; - } - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (event.hasNoModifiers()) { - direction = View.FOCUS_RIGHT; - } - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (event.hasNoModifiers()) { - direction = View.FOCUS_UP; - } - break; - case KeyEvent.KEYCODE_DPAD_DOWN: - if (event.hasNoModifiers()) { - direction = View.FOCUS_DOWN; - } - break; - case KeyEvent.KEYCODE_TAB: - if (event.hasNoModifiers()) { - direction = View.FOCUS_FORWARD; - } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { - direction = View.FOCUS_BACKWARD; - } - break; + case KeyEvent.KEYCODE_DPAD_LEFT: + if (event.hasNoModifiers()) { + direction = View.FOCUS_LEFT; + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (event.hasNoModifiers()) { + direction = View.FOCUS_RIGHT; + } + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (event.hasNoModifiers()) { + direction = View.FOCUS_UP; + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (event.hasNoModifiers()) { + direction = View.FOCUS_DOWN; + } + break; + case KeyEvent.KEYCODE_TAB: + if (event.hasNoModifiers()) { + direction = View.FOCUS_FORWARD; + } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { + direction = View.FOCUS_BACKWARD; + } + break; } - if (direction != 0) { - View focused = mView != null ? mView.findFocus() : null; + View focused = mView.findFocus(); if (focused != null) { View v = focused.focusSearch(direction); if (v != null && v != focused) { @@ -3532,8 +3609,8 @@ public final class ViewRootImpl implements ViewParent, v, mTempRect); } if (v.requestFocus(direction, mTempRect)) { - playSoundEffect( - SoundEffectConstants.getContantForFocusDirection(direction)); + playSoundEffect(SoundEffectConstants + .getContantForFocusDirection(direction)); finishInputEvent(q, true); return; } @@ -3683,22 +3760,11 @@ public final class ViewRootImpl implements ViewParent, + " called when there is no mView"); } if (mAccessibilityInteractionController == null) { - mAccessibilityInteractionController = new AccessibilityInteractionController(); + mAccessibilityInteractionController = new AccessibilityInteractionController(this); } return mAccessibilityInteractionController; } - public AccessibilityNodePrefetcher getAccessibilityNodePrefetcher() { - if (mView == null) { - throw new IllegalStateException("getAccessibilityNodePrefetcher" - + " called when there is no mView"); - } - if (mAccessibilityNodePrefetcher == null) { - mAccessibilityNodePrefetcher = new AccessibilityNodePrefetcher(); - } - return mAccessibilityNodePrefetcher; - } - private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending) throws RemoteException { @@ -4375,15 +4441,19 @@ public final class ViewRootImpl implements ViewParent, * This event is send at most once every * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}. */ - private void postSendWindowContentChangedCallback() { + private void postSendWindowContentChangedCallback(View source) { if (mSendWindowContentChangedAccessibilityEvent == null) { mSendWindowContentChangedAccessibilityEvent = new SendWindowContentChangedAccessibilityEvent(); } - if (!mSendWindowContentChangedAccessibilityEvent.mIsPending) { - mSendWindowContentChangedAccessibilityEvent.mIsPending = true; + View oldSource = mSendWindowContentChangedAccessibilityEvent.mSource; + if (oldSource == null) { + mSendWindowContentChangedAccessibilityEvent.mSource = source; mHandler.postDelayed(mSendWindowContentChangedAccessibilityEvent, ViewConfiguration.getSendRecurringAccessibilityEventsInterval()); + } else { + View newSource = getCommonPredecessor(oldSource, source); + mSendWindowContentChangedAccessibilityEvent.mSource = newSource; } } @@ -4419,6 +4489,46 @@ public final class ViewRootImpl implements ViewParent, return true; } + @Override + public void childAccessibilityStateChanged(View child) { + postSendWindowContentChangedCallback(child); + } + + private View getCommonPredecessor(View first, View second) { + if (mAttachInfo != null) { + if (mTempHashSet == null) { + mTempHashSet = new HashSet<View>(); + } + HashSet<View> seen = mTempHashSet; + seen.clear(); + View firstCurrent = first; + while (firstCurrent != null) { + seen.add(firstCurrent); + ViewParent firstCurrentParent = firstCurrent.mParent; + if (firstCurrentParent instanceof View) { + firstCurrent = (View) firstCurrentParent; + } else { + firstCurrent = null; + } + } + View secondCurrent = second; + while (secondCurrent != null) { + if (seen.contains(secondCurrent)) { + seen.clear(); + return secondCurrent; + } + ViewParent secondCurrentParent = secondCurrent.mParent; + if (secondCurrentParent instanceof View) { + secondCurrent = (View) secondCurrentParent; + } else { + secondCurrent = null; + } + } + seen.clear(); + } + return null; + } + void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( @@ -4953,6 +5063,7 @@ public final class ViewRootImpl implements ViewParent, } } else { ensureNoConnection(); + setAccessibilityFocusedHost(null); } } @@ -4991,14 +5102,15 @@ public final class ViewRootImpl implements ViewParent, mViewRootImpl = new WeakReference<ViewRootImpl>(viewRootImpl); } + @Override public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, - int prefetchFlags, int interrogatingPid, long interrogatingTid) { + int flags, int interrogatingPid, long interrogatingTid) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId, - interactionId, callback, prefetchFlags, interrogatingPid, interrogatingTid); + interactionId, callback, flags, interrogatingPid, interrogatingTid); } else { // We cannot make the call and notify the caller so it does not wait. try { @@ -5009,16 +5121,17 @@ public final class ViewRootImpl implements ViewParent, } } + @Override public void performAccessibilityAction(long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, - int interogatingPid, long interrogatingTid) { + int flags, int interogatingPid, long interrogatingTid) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .performAccessibilityActionClientThread(accessibilityNodeId, action, - interactionId, callback, interogatingPid, interrogatingTid); + interactionId, callback, flags, interogatingPid, interrogatingTid); } else { - // We cannot make the call and notify the caller so it does not + // We cannot make the call and notify the caller so it does not wait. try { callback.setPerformAccessibilityActionResult(false, interactionId); } catch (RemoteException re) { @@ -5027,16 +5140,17 @@ public final class ViewRootImpl implements ViewParent, } } + @Override public void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, - int interrogatingPid, long interrogatingTid) { + int flags, int interrogatingPid, long interrogatingTid) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .findAccessibilityNodeInfoByViewIdClientThread(accessibilityNodeId, viewId, - interactionId, callback, interrogatingPid, interrogatingTid); + interactionId, callback, flags, interrogatingPid, interrogatingTid); } else { - // We cannot make the call and notify the caller so it does not + // We cannot make the call and notify the caller so it does not wait. try { callback.setFindAccessibilityNodeInfoResult(null, interactionId); } catch (RemoteException re) { @@ -5045,16 +5159,17 @@ public final class ViewRootImpl implements ViewParent, } } + @Override public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, - int interrogatingPid, long interrogatingTid) { + int flags, int interrogatingPid, long interrogatingTid) { ViewRootImpl viewRootImpl = mViewRootImpl.get(); if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .findAccessibilityNodeInfosByTextClientThread(accessibilityNodeId, text, - interactionId, callback, interrogatingPid, interrogatingTid); + interactionId, callback, flags, interrogatingPid, interrogatingTid); } else { - // We cannot make the call and notify the caller so it does not + // We cannot make the call and notify the caller so it does not wait. try { callback.setFindAccessibilityNodeInfosResult(null, interactionId); } catch (RemoteException re) { @@ -5062,610 +5177,54 @@ public final class ViewRootImpl implements ViewParent, } } } - } - - /** - * Computes whether a view is visible on the screen. - * - * @param view The view to check. - * @return Whether the view is visible on the screen. - */ - private boolean isDisplayedOnScreen(View view) { - // The first two checks are made also made by isShown() which - // however traverses the tree up to the parent to catch that. - // Therefore, we do some fail fast check to minimize the up - // tree traversal. - return (view.mAttachInfo != null - && view.mAttachInfo.mWindowVisibility == View.VISIBLE - && view.isShown() - && view.getGlobalVisibleRect(mTempRect)); - } - - /** - * 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 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( - long accessibilityNodeId, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int prefetchFlags, + @Override + public void findFocus(long accessibilityNodeId, int interactionId, int focusType, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid) { - Message message = mHandler.obtainMessage(); - message.what = MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID; - message.arg1 = prefetchFlags; - SomeArgs args = mPool.acquire(); - args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); - args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); - args.argi3 = interactionId; - args.arg1 = callback; - message.obj = args; - // If the interrogation is performed by the same thread as the main UI - // thread in this process, set the message as a static reference so - // after this call completes the same thread but in the interrogating - // client can handle the message to generate the result. - if (interrogatingPid == Process.myPid() - && interrogatingTid == Looper.getMainLooper().getThread().getId()) { - AccessibilityInteractionClient.getInstanceForThread( - interrogatingTid).setSameThreadMessage(message); + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { + viewRootImpl.getAccessibilityInteractionController() + .findFocusClientThread(accessibilityNodeId, interactionId, focusType, + callback, flags, interrogatingPid, interrogatingTid); } else { - mHandler.sendMessage(message); - } - } - - public void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) { - final int prefetchFlags = message.arg1; - SomeArgs args = (SomeArgs) message.obj; - final int accessibilityViewId = args.argi1; - final int virtualDescendantId = args.argi2; - final int interactionId = args.argi3; - final IAccessibilityInteractionConnectionCallback callback = - (IAccessibilityInteractionConnectionCallback) args.arg1; - mPool.release(args); - List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList; - infos.clear(); - try { - View target = null; - if (accessibilityViewId == AccessibilityNodeInfo.UNDEFINED) { - target = ViewRootImpl.this.mView; - } else { - target = findViewByAccessibilityId(accessibilityViewId); - } - if (target != null && isDisplayedOnScreen(target)) { - getAccessibilityNodePrefetcher().prefetchAccessibilityNodeInfos(target, - virtualDescendantId, prefetchFlags, infos); - } - } finally { + // We cannot make the call and notify the caller so it does not wait. try { - callback.setFindAccessibilityNodeInfosResult(infos, interactionId); - infos.clear(); + callback.setFindAccessibilityNodeInfoResult(null, interactionId); } catch (RemoteException re) { - /* ignore - the other side will time out */ + /* best effort - ignore */ } } } - public void findAccessibilityNodeInfoByViewIdClientThread(long accessibilityNodeId, - int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, + @Override + public void focusSearch(long accessibilityNodeId, int interactionId, int direction, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid) { - Message message = mHandler.obtainMessage(); - message.what = MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID; - message.arg1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); - SomeArgs args = mPool.acquire(); - args.argi1 = viewId; - args.argi2 = interactionId; - args.arg1 = callback; - message.obj = args; - // If the interrogation is performed by the same thread as the main UI - // thread in this process, set the message as a static reference so - // after this call completes the same thread but in the interrogating - // client can handle the message to generate the result. - if (interrogatingPid == Process.myPid() - && interrogatingTid == Looper.getMainLooper().getThread().getId()) { - AccessibilityInteractionClient.getInstanceForThread( - interrogatingTid).setSameThreadMessage(message); - } else { - mHandler.sendMessage(message); - } - } - - public void findAccessibilityNodeInfoByViewIdUiThread(Message message) { - final int accessibilityViewId = message.arg1; - SomeArgs args = (SomeArgs) message.obj; - final int viewId = args.argi1; - final int interactionId = args.argi2; - final IAccessibilityInteractionConnectionCallback callback = - (IAccessibilityInteractionConnectionCallback) args.arg1; - mPool.release(args); - AccessibilityNodeInfo info = null; - try { - View root = null; - if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { - root = findViewByAccessibilityId(accessibilityViewId); - } else { - root = ViewRootImpl.this.mView; - } - if (root != null) { - View target = root.findViewById(viewId); - if (target != null && isDisplayedOnScreen(target)) { - info = target.createAccessibilityNodeInfo(); - } - } - } finally { - try { - callback.setFindAccessibilityNodeInfoResult(info, interactionId); - } catch (RemoteException re) { - /* ignore - the other side will time out */ - } - } - } - - public void findAccessibilityNodeInfosByTextClientThread(long accessibilityNodeId, - String text, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, - long interrogatingTid) { - Message message = mHandler.obtainMessage(); - message.what = MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT; - SomeArgs args = mPool.acquire(); - args.arg1 = text; - args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); - args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); - args.argi3 = interactionId; - args.arg2 = callback; - message.obj = args; - // If the interrogation is performed by the same thread as the main UI - // thread in this process, set the message as a static reference so - // after this call completes the same thread but in the interrogating - // client can handle the message to generate the result. - if (interrogatingPid == Process.myPid() - && interrogatingTid == Looper.getMainLooper().getThread().getId()) { - AccessibilityInteractionClient.getInstanceForThread( - interrogatingTid).setSameThreadMessage(message); - } else { - mHandler.sendMessage(message); - } - } - - public void findAccessibilityNodeInfosByTextUiThread(Message message) { - SomeArgs args = (SomeArgs) message.obj; - final String text = (String) args.arg1; - final int accessibilityViewId = args.argi1; - final int virtualDescendantId = args.argi2; - final int interactionId = args.argi3; - final IAccessibilityInteractionConnectionCallback callback = - (IAccessibilityInteractionConnectionCallback) args.arg2; - mPool.release(args); - List<AccessibilityNodeInfo> infos = null; - try { - View target; - if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { - target = findViewByAccessibilityId(accessibilityViewId); - } else { - target = ViewRootImpl.this.mView; - } - if (target != null && isDisplayedOnScreen(target)) { - AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); - if (provider != null) { - infos = provider.findAccessibilityNodeInfosByText(text, - virtualDescendantId); - } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED) { - ArrayList<View> foundViews = mAttachInfo.mFocusablesTempList; - foundViews.clear(); - target.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT - | View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION - | View.FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS); - if (!foundViews.isEmpty()) { - infos = mTempAccessibilityNodeInfoList; - infos.clear(); - final int viewCount = foundViews.size(); - for (int i = 0; i < viewCount; i++) { - View foundView = foundViews.get(i); - if (isDisplayedOnScreen(foundView)) { - provider = foundView.getAccessibilityNodeProvider(); - if (provider != null) { - List<AccessibilityNodeInfo> infosFromProvider = - provider.findAccessibilityNodeInfosByText(text, - virtualDescendantId); - if (infosFromProvider != null) { - infos.addAll(infosFromProvider); - } - } else { - infos.add(foundView.createAccessibilityNodeInfo()); - } - } - } - } - } - } - } finally { - try { - callback.setFindAccessibilityNodeInfosResult(infos, interactionId); - } catch (RemoteException re) { - /* ignore - the other side will time out */ - } - } - } - - public void performAccessibilityActionClientThread(long accessibilityNodeId, int action, - int interactionId, IAccessibilityInteractionConnectionCallback callback, - int interogatingPid, long interrogatingTid) { - Message message = mHandler.obtainMessage(); - message.what = MSG_PERFORM_ACCESSIBILITY_ACTION; - message.arg1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId); - message.arg2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId); - SomeArgs args = mPool.acquire(); - args.argi1 = action; - args.argi2 = interactionId; - args.arg1 = callback; - message.obj = args; - // If the interrogation is performed by the same thread as the main UI - // thread in this process, set the message as a static reference so - // after this call completes the same thread but in the interrogating - // client can handle the message to generate the result. - if (interogatingPid == Process.myPid() - && interrogatingTid == Looper.getMainLooper().getThread().getId()) { - AccessibilityInteractionClient.getInstanceForThread( - interrogatingTid).setSameThreadMessage(message); + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { + viewRootImpl.getAccessibilityInteractionController() + .focusSearchClientThread(accessibilityNodeId, interactionId, direction, + callback, flags, interrogatingPid, interrogatingTid); } else { - mHandler.sendMessage(message); - } - } - - public void perfromAccessibilityActionUiThread(Message message) { - final int accessibilityViewId = message.arg1; - final int virtualDescendantId = message.arg2; - SomeArgs args = (SomeArgs) message.obj; - final int action = args.argi1; - final int interactionId = args.argi2; - final IAccessibilityInteractionConnectionCallback callback = - (IAccessibilityInteractionConnectionCallback) args.arg1; - mPool.release(args); - boolean succeeded = false; - try { - View target = findViewByAccessibilityId(accessibilityViewId); - if (target != null && isDisplayedOnScreen(target)) { - AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); - if (provider != null) { - succeeded = provider.performAccessibilityAction(action, - virtualDescendantId); - } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED) { - switch (action) { - case AccessibilityNodeInfo.ACTION_FOCUS: { - if (!target.hasFocus()) { - // Get out of touch mode since accessibility - // wants to move focus around. - ensureTouchMode(false); - succeeded = target.requestFocus(); - } - } break; - case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { - if (target.hasFocus()) { - target.clearFocus(); - succeeded = !target.isFocused(); - } - } break; - case AccessibilityNodeInfo.ACTION_SELECT: { - if (!target.isSelected()) { - target.setSelected(true); - succeeded = target.isSelected(); - } - } break; - case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: { - if (target.isSelected()) { - target.setSelected(false); - succeeded = !target.isSelected(); - } - } break; - } - } - } - } finally { + // We cannot make the call and notify the caller so it does not wait. try { - callback.setPerformAccessibilityActionResult(succeeded, interactionId); + callback.setFindAccessibilityNodeInfoResult(null, interactionId); } catch (RemoteException re) { - /* ignore - the other side will time out */ + /* best effort - ignore */ } } } - - private View findViewByAccessibilityId(int accessibilityId) { - View root = ViewRootImpl.this.mView; - if (root == null) { - return null; - } - View foundView = root.findViewByAccessibilityId(accessibilityId); - if (foundView != null && foundView.getVisibility() != View.VISIBLE) { - return null; - } - return foundView; - } } private class SendWindowContentChangedAccessibilityEvent implements Runnable { - public volatile boolean mIsPending; + public View mSource; public void run() { - if (mView != null) { - mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - mIsPending = false; - } - } - } - - /** - * This class encapsulates a prefetching strategy for the accessibility APIs for - * querying window content. It is responsible to prefetch a batch of - * AccessibilityNodeInfos in addition to the one for a requested node. - */ - class AccessibilityNodePrefetcher { - - private static final int MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE = 50; - - public void prefetchAccessibilityNodeInfos(View view, int virtualViewId, int prefetchFlags, - List<AccessibilityNodeInfo> outInfos) { - AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider(); - if (provider == null) { - AccessibilityNodeInfo root = view.createAccessibilityNodeInfo(); - if (root != null) { - outInfos.add(root); - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { - prefetchPredecessorsOfRealNode(view, outInfos); - } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { - prefetchSiblingsOfRealNode(view, outInfos); - } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { - prefetchDescendantsOfRealNode(view, outInfos); - } - } - } else { - AccessibilityNodeInfo root = provider.createAccessibilityNodeInfo(virtualViewId); - if (root != null) { - outInfos.add(root); - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) { - prefetchPredecessorsOfVirtualNode(root, view, provider, outInfos); - } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) { - prefetchSiblingsOfVirtualNode(root, view, provider, outInfos); - } - if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) { - prefetchDescendantsOfVirtualNode(root, provider, outInfos); - } - } - } - } - - private void prefetchPredecessorsOfRealNode(View view, - List<AccessibilityNodeInfo> outInfos) { - ViewParent parent = view.getParent(); - while (parent instanceof View - && outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - View parentView = (View) parent; - final long parentNodeId = AccessibilityNodeInfo.makeNodeId( - parentView.getAccessibilityViewId(), AccessibilityNodeInfo.UNDEFINED); - AccessibilityNodeInfo info = parentView.createAccessibilityNodeInfo(); - if (info != null) { - outInfos.add(info); - } - parent = parent.getParent(); - } - } - - private void prefetchSiblingsOfRealNode(View current, - List<AccessibilityNodeInfo> outInfos) { - ViewParent parent = current.getParent(); - if (parent instanceof ViewGroup) { - ViewGroup parentGroup = (ViewGroup) parent; - final int childCount = parentGroup.getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = parentGroup.getChildAt(i); - if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE - && child.getAccessibilityViewId() != current.getAccessibilityViewId() - && isDisplayedOnScreen(child)) { - final long childNodeId = AccessibilityNodeInfo.makeNodeId( - child.getAccessibilityViewId(), AccessibilityNodeInfo.UNDEFINED); - AccessibilityNodeInfo info = null; - AccessibilityNodeProvider provider = child.getAccessibilityNodeProvider(); - if (provider == null) { - info = child.createAccessibilityNodeInfo(); - } else { - info = provider.createAccessibilityNodeInfo( - AccessibilityNodeInfo.UNDEFINED); - } - if (info != null) { - outInfos.add(info); - } - } - } - } - } - - private void prefetchDescendantsOfRealNode(View root, - List<AccessibilityNodeInfo> outInfos) { - if (root instanceof ViewGroup) { - ViewGroup rootGroup = (ViewGroup) root; - HashMap<View, AccessibilityNodeInfo> addedChildren = - new HashMap<View, AccessibilityNodeInfo>(); - final int childCount = rootGroup.getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = rootGroup.getChildAt(i); - if (isDisplayedOnScreen(child) - && outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - final long childNodeId = AccessibilityNodeInfo.makeNodeId( - child.getAccessibilityViewId(), AccessibilityNodeInfo.UNDEFINED); - AccessibilityNodeProvider provider = child.getAccessibilityNodeProvider(); - if (provider == null) { - AccessibilityNodeInfo info = child.createAccessibilityNodeInfo(); - if (info != null) { - outInfos.add(info); - addedChildren.put(child, null); - } - } else { - AccessibilityNodeInfo info = provider.createAccessibilityNodeInfo( - AccessibilityNodeInfo.UNDEFINED); - if (info != null) { - outInfos.add(info); - addedChildren.put(child, info); - } - } - } - } - if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - for (Map.Entry<View, AccessibilityNodeInfo> entry : addedChildren.entrySet()) { - View addedChild = entry.getKey(); - AccessibilityNodeInfo virtualRoot = entry.getValue(); - if (virtualRoot == null) { - prefetchDescendantsOfRealNode(addedChild, outInfos); - } else { - AccessibilityNodeProvider provider = - addedChild.getAccessibilityNodeProvider(); - prefetchDescendantsOfVirtualNode(virtualRoot, provider, outInfos); - } - } - } - } - } - - private void prefetchPredecessorsOfVirtualNode(AccessibilityNodeInfo root, - View providerHost, AccessibilityNodeProvider provider, - List<AccessibilityNodeInfo> outInfos) { - long parentNodeId = root.getParentNodeId(); - int accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId); - while (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { - final int virtualDescendantId = - AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId); - if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED - || accessibilityViewId == providerHost.getAccessibilityViewId()) { - AccessibilityNodeInfo parent = provider.createAccessibilityNodeInfo( - virtualDescendantId); - if (parent != null) { - outInfos.add(parent); - } - parentNodeId = parent.getParentNodeId(); - accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId( - parentNodeId); - } else { - prefetchPredecessorsOfRealNode(providerHost, outInfos); - return; - } - } - } - - private void prefetchSiblingsOfVirtualNode(AccessibilityNodeInfo current, View providerHost, - AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) { - final long parentNodeId = current.getParentNodeId(); - final int parentAccessibilityViewId = - AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId); - final int parentVirtualDescendantId = - AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId); - if (parentVirtualDescendantId != AccessibilityNodeInfo.UNDEFINED - || parentAccessibilityViewId == providerHost.getAccessibilityViewId()) { - AccessibilityNodeInfo parent = - provider.createAccessibilityNodeInfo(parentVirtualDescendantId); - if (parent != null) { - SparseLongArray childNodeIds = parent.getChildNodeIds(); - final int childCount = childNodeIds.size(); - for (int i = 0; i < childCount; i++) { - final long childNodeId = childNodeIds.get(i); - if (childNodeId != current.getSourceNodeId() - && outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - final int childVirtualDescendantId = - AccessibilityNodeInfo.getVirtualDescendantId(childNodeId); - AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo( - childVirtualDescendantId); - if (child != null) { - outInfos.add(child); - } - } - } - } - } else { - prefetchSiblingsOfRealNode(providerHost, outInfos); - } - } - - private void prefetchDescendantsOfVirtualNode(AccessibilityNodeInfo root, - AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) { - SparseLongArray childNodeIds = root.getChildNodeIds(); - final int initialOutInfosSize = outInfos.size(); - final int childCount = childNodeIds.size(); - for (int i = 0; i < childCount; i++) { - if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - final long childNodeId = childNodeIds.get(i); - AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo( - AccessibilityNodeInfo.getVirtualDescendantId(childNodeId)); - if (child != null) { - outInfos.add(child); - } - } - } - if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { - final int addedChildCount = outInfos.size() - initialOutInfosSize; - for (int i = 0; i < addedChildCount; i++) { - AccessibilityNodeInfo child = outInfos.get(initialOutInfosSize + i); - prefetchDescendantsOfVirtualNode(child, provider, outInfos); - } + if (mSource != null) { + mSource.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + mSource.resetAccessibilityStateChanged(); + mSource = null; } } } diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index 0998c80..6cb1578 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -508,7 +508,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par public static final int TYPE_VIEW_SELECTED = 0x00000004; /** - * Represents the event of focusing a {@link android.view.View}. + * Represents the event of setting input focus of a {@link android.view.View}. */ public static final int TYPE_VIEW_FOCUSED = 0x00000008; @@ -549,7 +549,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400; /** - * Represents the event of changing the content of a window. + * Represents the event of changing the content of a window and more + * specifically the sub-tree rooted at the event's source. */ public static final int TYPE_WINDOW_CONTENT_CHANGED = 0x00000800; @@ -569,6 +570,16 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par public static final int TYPE_ANNOUNCEMENT = 0x00004000; /** + * Represents the event of gaining accessibility focus. + */ + public static final int TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000; + + /** + * Represents the event of clearing accessibility focus. + */ + public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000; + + /** * Mask for {@link AccessibilityEvent} all types. * * @see #TYPE_VIEW_CLICKED @@ -1018,6 +1029,10 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par return "TYPE_VIEW_SCROLLED"; case TYPE_ANNOUNCEMENT: return "TYPE_ANNOUNCEMENT"; + case TYPE_VIEW_ACCESSIBILITY_FOCUSED: + return "TYPE_VIEW_ACCESSIBILITY_FOCUSED"; + case TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: + return "TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED"; default: return null; } diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index be74b31..35f0d9d 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -18,7 +18,9 @@ package android.view.accessibility; import android.accessibilityservice.IAccessibilityServiceConnection; import android.graphics.Rect; +import android.os.Binder; import android.os.Message; +import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; @@ -174,7 +176,7 @@ public final class AccessibilityInteractionClient final int interactionId = mInteractionIdCounter.getAndIncrement(); final float windowScale = connection.findAccessibilityNodeInfoByAccessibilityId( accessibilityWindowId, accessibilityNodeId, interactionId, this, - Thread.currentThread().getId(), prefetchFlags); + prefetchFlags, Thread.currentThread().getId()); // If the scale is zero the call has failed. if (windowScale > 0) { List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( @@ -293,6 +295,96 @@ public final class AccessibilityInteractionClient } /** + * Finds the {@link android.view.accessibility.AccessibilityNodeInfo} that has the + * specified focus type. The search is performed in the window whose id is specified + * and starts from the node whose accessibility id is specified. + * + * @param connectionId The id of a connection for interacting with the system. + * @param accessibilityWindowId A unique window id. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id or virtual descendant id from + * where to start the search. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} + * to start from the root. + * @param focusType The focus type. + * @return The accessibility focused {@link AccessibilityNodeInfo}. + */ + public AccessibilityNodeInfo findFocus(int connectionId, int accessibilityWindowId, + long accessibilityNodeId, int focusType) { + try { + IAccessibilityServiceConnection connection = getConnection(connectionId); + if (connection != null) { + final int interactionId = mInteractionIdCounter.getAndIncrement(); + final float windowScale = connection.findFocus(accessibilityWindowId, + accessibilityNodeId, focusType, interactionId, this, + Thread.currentThread().getId()); + // If the scale is zero the call has failed. + if (windowScale > 0) { + AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( + interactionId); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); + return info; + } + } else { + if (DEBUG) { + Log.w(LOG_TAG, "No connection for connection id: " + connectionId); + } + } + } catch (RemoteException re) { + if (DEBUG) { + Log.w(LOG_TAG, "Error while calling remote findAccessibilityFocus", re); + } + } + return null; + } + + /** + * Finds the accessibility focused {@link android.view.accessibility.AccessibilityNodeInfo}. + * The search is performed in the window whose id is specified and starts from the + * node whose accessibility id is specified. + * + * @param connectionId The id of a connection for interacting with the system. + * @param accessibilityWindowId A unique window id. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * to query the currently active window. + * @param accessibilityNodeId A unique view id or virtual descendant id from + * where to start the search. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} + * to start from the root. + * @param direction The direction in which to search for focusable. + * @return The accessibility focused {@link AccessibilityNodeInfo}. + */ + public AccessibilityNodeInfo focusSearch(int connectionId, int accessibilityWindowId, + long accessibilityNodeId, int direction) { + try { + IAccessibilityServiceConnection connection = getConnection(connectionId); + if (connection != null) { + final int interactionId = mInteractionIdCounter.getAndIncrement(); + final float windowScale = connection.focusSearch(accessibilityWindowId, + accessibilityNodeId, direction, interactionId, this, + Thread.currentThread().getId()); + // If the scale is zero the call has failed. + if (windowScale > 0) { + AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( + interactionId); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId, windowScale); + return info; + } + } else { + if (DEBUG) { + Log.w(LOG_TAG, "No connection for connection id: " + connectionId); + } + } + } catch (RemoteException re) { + if (DEBUG) { + Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re); + } + } + return null; + } + + /** * Performs an accessibility action on an {@link AccessibilityNodeInfo}. * * @param connectionId The id of a connection for interacting with the system. @@ -382,7 +474,12 @@ public final class AccessibilityInteractionClient int interactionId) { synchronized (mInstanceLock) { final boolean success = waitForResultTimedLocked(interactionId); - List<AccessibilityNodeInfo> result = success ? mFindAccessibilityNodeInfosResult : null; + List<AccessibilityNodeInfo> result = null; + if (success) { + result = mFindAccessibilityNodeInfosResult; + } else { + result = Collections.emptyList(); + } clearResultLocked(); return result; } @@ -395,13 +492,18 @@ public final class AccessibilityInteractionClient int interactionId) { synchronized (mInstanceLock) { if (interactionId > mInteractionId) { - // If the call is not an IPC, i.e. it is made from the same process, we need to - // instantiate new result list to avoid passing internal instances to clients. - final boolean isIpcCall = (queryLocalInterface(getInterfaceDescriptor()) == null); - if (!isIpcCall) { - mFindAccessibilityNodeInfosResult = new ArrayList<AccessibilityNodeInfo>(infos); + if (infos != null) { + // If the call is not an IPC, i.e. it is made from the same process, we need to + // instantiate new result list to avoid passing internal instances to clients. + final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid()); + if (!isIpcCall) { + mFindAccessibilityNodeInfosResult = + new ArrayList<AccessibilityNodeInfo>(infos); + } else { + mFindAccessibilityNodeInfosResult = infos; + } } else { - mFindAccessibilityNodeInfosResult = infos; + mFindAccessibilityNodeInfosResult = Collections.emptyList(); } mInteractionId = interactionId; } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index e37de6f..77fd12a 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -204,6 +204,12 @@ public final class AccessibilityManager { * @param event The event to send. * * @throws IllegalStateException if accessibility is not enabled. + * + * <strong>Note:</strong> The preferred mechanism for sending custom accessibility + * events is through calling + * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} + * instead of this method to allow predecessors to augment/filter events sent by + * their descendants. */ public void sendAccessibilityEvent(AccessibilityEvent event) { if (!mIsEnabled) { diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index f616dca..1071c65 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -74,29 +74,57 @@ public class AccessibilityNodeInfo implements Parcelable { public static final int FLAG_PREFETCH_SIBLINGS = 0x00000002; /** @hide */ - public static final int FLAG_PREFETCH_DESCENDANTS = 0x00000003; + public static final int FLAG_PREFETCH_DESCENDANTS = 0x00000004; + + /** @hide */ + public static final int INCLUDE_NOT_IMPORTANT_VIEWS = 0x00000008; // Actions. /** - * Action that focuses the node. + * Action that gives input focus to the node. */ - public static final int ACTION_FOCUS = 0x00000001; + public static final int ACTION_FOCUS = 0x00000001; /** - * Action that unfocuses the node. + * Action that clears input focus of the node. */ - public static final int ACTION_CLEAR_FOCUS = 0x00000002; + public static final int ACTION_CLEAR_FOCUS = 0x00000002; /** * Action that selects the node. */ - public static final int ACTION_SELECT = 0x00000004; + public static final int ACTION_SELECT = 0x00000004; /** * Action that unselects the node. */ - public static final int ACTION_CLEAR_SELECTION = 0x00000008; + public static final int ACTION_CLEAR_SELECTION = 0x00000008; + + /** + * Action that gives accessibility focus to the node. + */ + public static final int ACTION_ACCESSIBILITY_FOCUS = 0x00000010; + + /** + * Action that clears accessibility focus of the node. + */ + public static final int ACTION_CLEAR_ACCESSIBILITY_FOCUS = 0x00000020; + + /** + * Action that clicks on the node info./AccessibilityNodeInfoCache.java + */ + public static final int ACTION_CLICK = 0x00000040; + + /** + * The input focus. + */ + public static final int FOCUS_INPUT = 1; + + /** + * The accessibility focus. + */ + public static final int FOCUS_ACCESSIBILITY = 2; // Boolean attributes. @@ -120,6 +148,8 @@ public class AccessibilityNodeInfo implements Parcelable { private static final int PROPERTY_SCROLLABLE = 0x00000200; + private static final int PROPERTY_ACCESSIBILITY_FOCUSED = 0x00000400; + /** * Bits that provide the id of a virtual descendant of a view. */ @@ -248,6 +278,57 @@ public class AccessibilityNodeInfo implements Parcelable { (root != null) ? root.getAccessibilityViewId() : UNDEFINED; mSourceNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); } + + /** + * Find the view that has the input focus. The search starts from + * the view represented by this node info. + * + * @param focus The focus to find. One of {@link #FOCUS_INPUT} or + * {@link #FOCUS_ACCESSIBILITY}. + * @return The node info of the focused view or null. + * + * @see #FOCUS_INPUT + * @see #FOCUS_ACCESSIBILITY + */ + public AccessibilityNodeInfo findFocus(int focus) { + enforceSealed(); + if (!canPerformRequestOverConnection(mSourceNodeId)) { + return null; + } + return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId, mWindowId, + mSourceNodeId, focus); + } + + /** + * Searches for the nearest view in the specified direction that can take + * the input focus. + * + * @param direction The direction. Can be one of: + * {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_UP}, + * {@link View#FOCUS_LEFT}, + * {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_FORWARD}, + * {@link View#FOCUS_BACKWARD}, + * {@link View#ACCESSIBILITY_FOCUS_IN}, + * {@link View#ACCESSIBILITY_FOCUS_OUT}, + * {@link View#ACCESSIBILITY_FOCUS_FORWARD}, + * {@link View#ACCESSIBILITY_FOCUS_BACKWARD}, + * {@link View#ACCESSIBILITY_FOCUS_UP}, + * {@link View#ACCESSIBILITY_FOCUS_RIGHT}, + * {@link View#ACCESSIBILITY_FOCUS_DOWN}, + * {@link View#ACCESSIBILITY_FOCUS_LEFT}. + * + * @return The node info for the view that can take accessibility focus. + */ + public AccessibilityNodeInfo focusSearch(int direction) { + enforceSealed(); + if (!canPerformRequestOverConnection(mSourceNodeId)) { + return null; + } + return AccessibilityInteractionClient.getInstance().focusSearch(mConnectionId, mWindowId, + mSourceNodeId, direction); + } /** * Gets the id of the window from which the info comes from. @@ -642,6 +723,31 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Gets whether this node is accessibility focused. + * + * @return True if the node is accessibility focused. + */ + public boolean isAccessibilityFocused() { + return getBooleanProperty(PROPERTY_ACCESSIBILITY_FOCUSED); + } + + /** + * Sets whether this node is accessibility focused. + * <p> + * <strong>Note:</strong> 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 accessibility focused. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setAccessibilityFocused(boolean focused) { + setBooleanProperty(PROPERTY_ACCESSIBILITY_FOCUSED, focused); + } + + /** * Gets whether this node is selected. * * @return True if the node is selected. diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java b/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java index dfbfc70..d2609bb 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java @@ -18,6 +18,7 @@ package android.view.accessibility; import android.util.Log; import android.util.LongSparseArray; +import android.util.SparseLongArray; /** * Simple cache for AccessibilityNodeInfos. The cache is mapping an @@ -54,20 +55,25 @@ public class AccessibilityNodeInfoCache { * @param event An event. */ public void onAccessibilityEvent(AccessibilityEvent event) { - final int eventType = event.getEventType(); - switch (eventType) { - case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: - case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: - case AccessibilityEvent.TYPE_VIEW_SCROLLED: - clear(); - break; - case AccessibilityEvent.TYPE_VIEW_FOCUSED: - case AccessibilityEvent.TYPE_VIEW_SELECTED: - case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: - case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: - final long accessibilityNodeId = event.getSourceNodeId(); - remove(accessibilityNodeId); - break; + if (ENABLED) { + final int eventType = event.getEventType(); + switch (eventType) { + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: { + clear(); + } break; + case AccessibilityEvent.TYPE_VIEW_FOCUSED: + case AccessibilityEvent.TYPE_VIEW_SELECTED: + case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: + case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: { + final long accessibilityNodeId = event.getSourceNodeId(); + remove(accessibilityNodeId); + } break; + case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: + case AccessibilityEvent.TYPE_VIEW_SCROLLED: { + final long accessibilityNodeId = event.getSourceNodeId(); + clearSubTree(accessibilityNodeId); + } break; + } } } @@ -167,4 +173,23 @@ public class AccessibilityNodeInfoCache { } } } + + /** + * Clears a subtree rooted at the node with the given id. + * + * @param rootNodeId The root id. + */ + private void clearSubTree(long rootNodeId) { + AccessibilityNodeInfo current = mCacheImpl.get(rootNodeId); + if (current == null) { + return; + } + mCacheImpl.remove(rootNodeId); + SparseLongArray childNodeIds = current.getChildNodeIds(); + final int childCount = childNodeIds.size(); + for (int i = 0; i < childCount; i++) { + final long childNodeId = childNodeIds.valueAt(i); + clearSubTree(childNodeId); + } + } } diff --git a/core/java/android/view/accessibility/AccessibilityNodeProvider.java b/core/java/android/view/accessibility/AccessibilityNodeProvider.java index 5890417..19e35dd 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeProvider.java +++ b/core/java/android/view/accessibility/AccessibilityNodeProvider.java @@ -87,6 +87,7 @@ public abstract class AccessibilityNodeProvider { * @return A populated {@link AccessibilityNodeInfo} for a virtual descendant or the * host View. * + * @see View#createAccessibilityNodeInfo() * @see AccessibilityNodeInfo */ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { @@ -102,6 +103,7 @@ public abstract class AccessibilityNodeProvider { * @param virtualViewId A client defined virtual view id. * @return True if the action was performed. * + * @see View#performAccessibilityAction(int) * @see #createAccessibilityNodeInfo(int) * @see AccessibilityNodeInfo */ @@ -127,4 +129,58 @@ public abstract class AccessibilityNodeProvider { int virtualViewId) { return null; } + + /** + * Finds the accessibility focused {@link AccessibilityNodeInfo}. The search is + * relative to the virtual view, i.e. a descendant of the host View, with the + * given <code>virtualViewId</code> or the host View itself + * <code>virtualViewId</code> equals to {@link View#NO_ID}. + * + * <strong>Note:</strong> Normally the system is responsible to transparently find + * accessibility focused view starting from a given root but for virtual view + * hierarchies it is a responsibility of this provider's implementor to find + * the accessibility focused virtual view. + * + * @param virtualViewId A client defined virtual view id which defined + * the root of the tree in which to perform the search. + * @return A list of node info. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfo + */ + public AccessibilityNodeInfo findAccessibilitiyFocus(int virtualViewId) { + return null; + } + + /** + * Finds {@link AccessibilityNodeInfo} to take accessibility focus in the given + * <code>direction</code>. The search is relative to the virtual view, i.e. a + * descendant of the host View, with the given <code>virtualViewId</code> or + * the host View itself <code>virtualViewId</code> equals to {@link View#NO_ID}. + * + * <strong>Note:</strong> Normally the system is responsible to transparently find + * the next view to take accessibility focus but for virtual view hierarchies + * it is a responsibility of this provider's implementor to compute the next + * focusable. + * + * @param direction The direction in which to search for a focus candidate. + * Values are + * {@link View#ACCESSIBILITY_FOCUS_IN}, + * {@link View#ACCESSIBILITY_FOCUS_OUT}, + * {@link View#ACCESSIBILITY_FOCUS_FORWARD}, + * {@link View#ACCESSIBILITY_FOCUS_BACKWARD}, + * {@link View#ACCESSIBILITY_FOCUS_UP}, + * {@link View#ACCESSIBILITY_FOCUS_DOWN}, + * {@link View#ACCESSIBILITY_FOCUS_LEFT}, + * {@link View#ACCESSIBILITY_FOCUS_RIGHT}. + * @param virtualViewId A client defined virtual view id which defined + * the root of the tree in which to perform the search. + * @return A list of node info. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfo + */ + public AccessibilityNodeInfo accessibilityFocusSearch(int direction, int virtualViewId) { + return null; + } } diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java index d25b3db..78a7d46 100644 --- a/core/java/android/view/accessibility/AccessibilityRecord.java +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -62,6 +62,7 @@ public class AccessibilityRecord { private static final int PROPERTY_PASSWORD = 0x00000004; private static final int PROPERTY_FULL_SCREEN = 0x00000080; private static final int PROPERTY_SCROLLABLE = 0x00000100; + private static final int PROPERTY_IMPORTANT_FOR_ACCESSIBILITY = 0x00000200; private static final int GET_SOURCE_PREFETCH_FLAGS = AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS @@ -77,7 +78,7 @@ public class AccessibilityRecord { private boolean mIsInPool; boolean mSealed; - int mBooleanProperties; + int mBooleanProperties = PROPERTY_IMPORTANT_FOR_ACCESSIBILITY; int mCurrentItemIndex = UNDEFINED; int mItemCount = UNDEFINED; int mFromIndex = UNDEFINED; @@ -134,6 +135,8 @@ public class AccessibilityRecord { */ public void setSource(View root, int virtualDescendantId) { enforceNotSealed(); + final boolean important = (root != null) ? root.isImportantForAccessibility() : true; + setBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY, important); mSourceWindowId = (root != null) ? root.getAccessibilityWindowId() : UNDEFINED; final int rootViewId = (root != null) ? root.getAccessibilityViewId() : UNDEFINED; mSourceNodeId = AccessibilityNodeInfo.makeNodeId(rootViewId, virtualDescendantId); @@ -274,6 +277,23 @@ public class AccessibilityRecord { } /** + * Gets if the source is important for accessibility. + * + * <strong>Note:</strong> Used only internally to determine whether + * to deliver the event to a given accessibility service since some + * services may want to regard all views for accessibility while others + * may want to regard only the important views for accessibility. + * + * @return True if the source is important for accessibility, + * false otherwise. + * + * @hide + */ + public boolean isImportantForAccessibility() { + return getBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY); + } + + /** * Gets the number of items that can be visited. * * @return The number of items. @@ -755,7 +775,7 @@ public class AccessibilityRecord { */ void clear() { mSealed = false; - mBooleanProperties = 0; + mBooleanProperties = PROPERTY_IMPORTANT_FOR_ACCESSIBILITY; mCurrentItemIndex = UNDEFINED; mItemCount = UNDEFINED; mFromIndex = UNDEFINED; diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl index fc3651c..8182d29 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -28,18 +28,26 @@ import android.view.accessibility.IAccessibilityInteractionConnectionCallback; oneway interface IAccessibilityInteractionConnection { void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int prefetchFlags, - int interrogatingPid, long interrogatingTid); + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, + long interrogatingTid); void findAccessibilityNodeInfoByViewId(long accessibilityNodeId, int id, int interactionId, - IAccessibilityInteractionConnectionCallback callback, - int interrogatingPid, long interrogatingTid); + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, + long interrogatingTid); void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, + long interrogatingTid); + + void findFocus(long accessibilityNodeId, int interactionId, int focusType, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, + long interrogatingTid); + + void focusSearch(long accessibilityNodeId, int interactionId, int direction, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid); void performAccessibilityAction(long accessibilityNodeId, int action, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int interrogatingPid, + IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid); } diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 320c75d..5b5134a 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -19,7 +19,7 @@ package android.view.accessibility; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.IAccessibilityServiceConnection; -import android.accessibilityservice.IEventListener; +import android.accessibilityservice.IAccessibilityServiceClient; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityInteractionConnection; @@ -49,7 +49,8 @@ interface IAccessibilityManager { void removeAccessibilityInteractionConnection(IWindow windowToken); - void registerUiTestAutomationService(IEventListener listener, in AccessibilityServiceInfo info); + void registerUiTestAutomationService(IAccessibilityServiceClient client, + in AccessibilityServiceInfo info); - void unregisterUiTestAutomationService(IEventListener listener); + void unregisterUiTestAutomationService(IAccessibilityServiceClient client); } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 057aabe..e68049c 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -2062,6 +2062,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te child = mAdapter.getView(position, scrapView, this); + if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.BIND_VIEW, position, getChildCount()); @@ -2082,6 +2086,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } else { child = mAdapter.getView(position, null, this); + + if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java index efdfae3..f279f8e 100644 --- a/core/java/android/widget/AbsSpinner.java +++ b/core/java/android/widget/AbsSpinner.java @@ -191,6 +191,10 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { if (view == null) { // Make a new one view = mAdapter.getView(selectedPosition, null, this); + + if (view.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } if (view != null) { diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index 97a864c..1a2231e 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -24,14 +24,15 @@ import android.util.AttributeSet; import android.util.SparseArray; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; +import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; - /** * An AdapterView is a view whose children are determined by an {@link Adapter}. * @@ -232,6 +233,11 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { public AdapterView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } /** @@ -643,6 +649,11 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { public void setEmptyView(View emptyView) { mEmptyView = emptyView; + // If not explicitly specified this view is important for accessibility. + if (emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + final T adapter = getAdapter(); final boolean empty = ((adapter == null) || adapter.isEmpty()); updateEmptyStatus(empty); @@ -846,12 +857,14 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { } } else { fireOnSelected(); + performAccessibilityActionsOnSelected(); } } } void selectionChanged() { - if (mOnItemSelectedListener != null) { + if (mOnItemSelectedListener != null + || AccessibilityManager.getInstance(mContext).isEnabled()) { if (mInLayout || mBlockLayoutRequests) { // If we are in a layout traversal, defer notification // by posting. This ensures that the view tree is @@ -863,20 +876,16 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { post(mSelectionNotifier); } else { fireOnSelected(); + performAccessibilityActionsOnSelected(); } } - - // we fire selection events here not in View - if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - } } private void fireOnSelected() { - if (mOnItemSelectedListener == null) + if (mOnItemSelectedListener == null) { return; - - int selection = this.getSelectedItemPosition(); + } + final int selection = getSelectedItemPosition(); if (selection >= 0) { View v = getSelectedView(); mOnItemSelectedListener.onItemSelected(this, v, selection, @@ -886,6 +895,17 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { } } + private void performAccessibilityActionsOnSelected() { + if (!AccessibilityManager.getInstance(mContext).isEnabled()) { + return; + } + final int position = getSelectedItemPosition(); + if (position >= 0) { + // we fire selection events here not in View + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + } + @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { View selectedView = getSelectedView(); @@ -936,6 +956,24 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { event.setItemCount(getCount()); } + /** + * @hide + */ + @Override + public boolean onRequestAccessibilityFocusFromHover(float x, float y) { + // We prefer to five focus to the child instead of this view. + // Usually the children are not actionable for accessibility, + // and they will not take accessibility focus, so we give it. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (isTransformedTouchPointInView(x, y, child, null)) { + return child.requestAccessibilityFocus(); + } + } + return super.onRequestAccessibilityFocusFromHover(x, y); + } + private boolean isScrollableForAccessibility() { T adapter = getAdapter(); if (adapter != null) { @@ -1012,6 +1050,9 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { mNeedSync = false; checkSelectionChanged(); } + + //TODO: Hmm, we do not know the old state so this is sub-optimal + notifyAccessibilityStateChanged(); } void checkSelectionChanged() { diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java index bb00049..c557963 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -414,6 +414,10 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> // get the fresh child from the adapter final View updatedChild = mAdapter.getView(modulo(i, adapterCount), null, this); + if (updatedChild.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + updatedChild.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + if (mViewsMap.containsKey(index)) { final FrameLayout fl = (FrameLayout) mViewsMap.get(index).view; // add the new child to the frame, if it exists diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index c5066b6..108b720 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -279,8 +279,13 @@ public class DatePicker extends FrameLayout { // re-order the number spinners to match the current date format reorderSpinners(); - // set content descriptions + // accessibility setContentDescriptions(); + + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } /** diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 91e2e49..6c7ea67 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -105,11 +105,11 @@ public class ImageView extends View { super(context); initImageView(); } - + public ImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } - + public ImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initImageView(); diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index b2321d9..992849d 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -112,8 +112,7 @@ public class NumberPicker extends LinearLayout { private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; /** - * The duration of scrolling to the next/previous value while snapping to - * a given position. + * The duration of scrolling while snapping to a given position. */ private static final int SNAP_SCROLL_DURATION = 300; @@ -680,6 +679,11 @@ public class NumberPicker extends LinearLayout { mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f)); updateInputTextView(); + + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } @Override diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 3b0fb36..37d9db7 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -1105,6 +1105,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setLongClickable(longClickable); if (mEditor != null) mEditor.prepareCursorControllers(); + + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } private void setTypefaceByIndex(int typefaceIndex, int styleIndex) { diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index bc88b62..18f7a91 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -251,6 +251,11 @@ public class TimePicker extends FrameLayout { // set the content descriptions setContentDescriptions(); + + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } @Override diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java index 8c05459..7a6f7d9 100644 --- a/core/java/com/android/internal/widget/ActionBarView.java +++ b/core/java/com/android/internal/widget/ActionBarView.java @@ -246,6 +246,10 @@ public class ActionBarView extends AbsActionBarView { mHomeLayout.setOnClickListener(mUpClickListener); mHomeLayout.setClickable(true); mHomeLayout.setFocusable(true); + + if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } } @Override diff --git a/core/res/res/drawable-nodpi/view_accessibility_focused.9.png b/core/res/res/drawable-nodpi/view_accessibility_focused.9.png Binary files differnew file mode 100644 index 0000000..f03f575 --- /dev/null +++ b/core/res/res/drawable-nodpi/view_accessibility_focused.9.png diff --git a/core/res/res/raw/accessibility_gestures.bin b/core/res/res/raw/accessibility_gestures.bin Binary files differnew file mode 100644 index 0000000..1f95e56 --- /dev/null +++ b/core/res/res/raw/accessibility_gestures.bin diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index aabe407..de24d10 100755 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -835,6 +835,10 @@ <!-- Reference to the Pointer style --> <attr name="pointerStyle" format="reference" /> + + <!-- The drawable for accessibility focused views. --> + <attr name="accessibilityFocusedDrawable" format="reference" /> + </declare-styleable> <!-- **************************************************************** --> @@ -2123,6 +2127,20 @@ layoutDirection is LTR, and ALIGN_LEFT otherwise --> <enum name="viewEnd" value="6" /> </attr> + + <!-- Controls how this View is important for accessibility which is if it fires + accessibility events and if it is reported to accessibility services that + query the screen. Note: While not recommended, an accessibility service may + decide to ignore this attribute and operate on all views in the view tree. --> + <attr name="importantForAccessibility" format="integer"> + <!-- The system determines whether the view is important for accessibility (recommended). --> + <enum name="auto" value="0" /> + <!-- The view is important for accessibility. --> + <enum name="yes" value="1" /> + <!-- The view is not important for accessibility. --> + <enum name="no" value="2" /> + </attr> + </declare-styleable> <!-- Attributes that can be used with a {@link android.view.ViewGroup} or any @@ -2445,6 +2463,8 @@ <attr name="accessibilityFlags"> <!-- Has flag {@link android.accessibilityservice.AccessibilityServiceInfo#DEFAULT} --> <flag name="flagDefault" value="0x00000001" /> + <!-- Has flag {@link android.accessibilityservice.AccessibilityServiceInfo#INCLUDE_NOT_IMPORTANT_VIEWS} --> + <flag name="flagIncludeNotImportantViews" value="0x00000002" /> </attr> <!-- Component name of an activity that allows the user to modify the settings for this service. This setting cannot be changed at runtime. --> @@ -4317,6 +4337,7 @@ <li>"state_hovered" <li>"state_drag_can_accept" <li>"state_drag_hovered" + <li>"state_accessibility_focused" </ul> --> <declare-styleable name="DrawableStates"> <!-- State value for {@link android.graphics.drawable.StateListDrawable StateListDrawable}, @@ -4377,6 +4398,9 @@ indicating that a drag operation (for which the Drawable's view is a valid recipient) is currently positioned over the Drawable. --> <attr name="state_drag_hovered" format="boolean" /> + <!-- State for {@link android.graphics.drawable.StateListDrawable StateListDrawable} + indicating that a View has accessibility focus. --> + <attr name="state_accessibility_focused" format="boolean" /> </declare-styleable> <declare-styleable name="ViewDrawableStates"> <attr name="state_pressed" /> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 2006548..4a5e442 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -226,6 +226,7 @@ <java-symbol type="attr" name="windowFixedWidthMinor" /> <java-symbol type="attr" name="windowFixedHeightMajor" /> <java-symbol type="attr" name="windowFixedHeightMinor" /> + <java-symbol type="attr" name="accessibilityFocusedDrawable"/> <java-symbol type="bool" name="action_bar_embed_tabs" /> <java-symbol type="bool" name="action_bar_expanded_action_views_exclusive" /> @@ -1103,6 +1104,7 @@ <java-symbol type="xml" name="time_zones_by_country" /> <java-symbol type="xml" name="sms_short_codes" /> + <java-symbol type="raw" name="accessibility_gestures" /> <java-symbol type="raw" name="incognito_mode_start_page" /> <java-symbol type="raw" name="loaderror" /> <java-symbol type="raw" name="nodomain" /> @@ -3583,7 +3585,11 @@ <public type="attr" name="layout_marginEnd"/> <public type="attr" name="kcm"/> + <public type="attr" name="parentActivityName" /> <public type="attr" name="supportsSentenceSpellCheck" /> + + <public type="attr" name="importantForAccessibility"/> + </resources> diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml index 7e06e24..71738ad 100644 --- a/core/res/res/values/themes.xml +++ b/core/res/res/values/themes.xml @@ -366,6 +366,9 @@ please see themes_device_defaults.xml. <!-- Pointer style --> <item name="pointerStyle">@android:style/Pointer</item> + + <!-- Accessibility focused drawable. --> + <item name="accessibilityFocusedDrawable">@android:drawable/view_accessibility_focused</item> </style> <!-- Variant of {@link #Theme} with no title bar --> diff --git a/core/tests/coretests/src/android/widget/focus/ListOfButtonsTest.java b/core/tests/coretests/src/android/widget/focus/ListOfButtonsTest.java index 3dba4e5..1968a32 100644 --- a/core/tests/coretests/src/android/widget/focus/ListOfButtonsTest.java +++ b/core/tests/coretests/src/android/widget/focus/ListOfButtonsTest.java @@ -19,7 +19,7 @@ package android.widget.focus; import android.widget.focus.ListOfButtons; import com.android.frameworks.coretests.R; -import android.test.ActivityInstrumentationTestCase; +import android.test.ActivityInstrumentationTestCase2; import android.test.suitebuilder.annotation.MediumTest; import android.widget.ListAdapter; import android.widget.Button; @@ -31,7 +31,7 @@ import android.view.View; * Tests that focus works as expected when navigating into and out of * a {@link ListView} that has buttons in it. */ -public class ListOfButtonsTest extends ActivityInstrumentationTestCase<ListOfButtons> { +public class ListOfButtonsTest extends ActivityInstrumentationTestCase2<ListOfButtons> { private ListAdapter mListAdapter; private Button mButtonAtTop; @@ -39,7 +39,7 @@ public class ListOfButtonsTest extends ActivityInstrumentationTestCase<ListOfBut private ListView mListView; public ListOfButtonsTest() { - super("com.android.frameworks.coretests", ListOfButtons.class); + super(ListOfButtons.class); } @Override @@ -47,6 +47,7 @@ public class ListOfButtonsTest extends ActivityInstrumentationTestCase<ListOfBut super.setUp(); ListOfButtons a = getActivity(); + getInstrumentation().waitForIdleSync(); mListAdapter = a.getListAdapter(); mButtonAtTop = (Button) a.findViewById(R.id.button); mListView = a.getListView(); diff --git a/services/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/java/com/android/server/accessibility/AccessibilityInputFilter.java index 31aa21e..889fbe4 100644 --- a/services/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -16,6 +16,7 @@ package com.android.server.accessibility; +import com.android.server.accessibility.TouchExplorer.GestureListener; import com.android.server.input.InputFilter; import android.content.Context; @@ -36,6 +37,8 @@ public class AccessibilityInputFilter extends InputFilter { private final Context mContext; + private final GestureListener mGestureListener; + /** * This is an interface for explorers that take a {@link MotionEvent} * stream and perform touch exploration of the screen content. @@ -64,11 +67,13 @@ public class AccessibilityInputFilter extends InputFilter { } private TouchExplorer mTouchExplorer; + private int mTouchscreenSourceDeviceId; - public AccessibilityInputFilter(Context context) { + public AccessibilityInputFilter(Context context, GestureListener gestureListener) { super(context.getMainLooper()); mContext = context; + mGestureListener = gestureListener; } @Override @@ -76,7 +81,7 @@ public class AccessibilityInputFilter extends InputFilter { if (DEBUG) { Slog.d(TAG, "Accessibility input filter installed."); } - mTouchExplorer = new TouchExplorer(this, mContext); + mTouchExplorer = new TouchExplorer(this, mContext, mGestureListener); super.onInstalled(); } diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java index c99aa02..754a4dd 100644 --- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -16,11 +16,14 @@ package com.android.server.accessibility; +import static android.accessibilityservice.AccessibilityServiceInfo.DEFAULT; +import static android.accessibilityservice.AccessibilityServiceInfo.INCLUDE_NOT_IMPORTANT_VIEWS; + import android.Manifest; import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.IAccessibilityServiceClient; import android.accessibilityservice.IAccessibilityServiceConnection; -import android.accessibilityservice.IEventListener; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -35,6 +38,7 @@ import android.database.ContentObserver; import android.graphics.Rect; import android.net.Uri; import android.os.Binder; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Message; @@ -55,8 +59,7 @@ import android.view.accessibility.IAccessibilityManager; import android.view.accessibility.IAccessibilityManagerClient; import com.android.internal.content.PackageMonitor; -import com.android.internal.os.HandlerCaller; -import com.android.internal.os.HandlerCaller.SomeArgs; +import com.android.server.accessibility.TouchExplorer.GestureListener; import com.android.server.wm.WindowManagerService; import org.xmlpull.v1.XmlPullParserException; @@ -80,7 +83,7 @@ import java.util.Set; * @hide */ public class AccessibilityManagerService extends IAccessibilityManager.Stub - implements HandlerCaller.Callback { + implements GestureListener { private static final boolean DEBUG = false; @@ -93,12 +96,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private static final int OWN_PROCESS_ID = android.os.Process.myPid(); - private static final int DO_SET_SERVICE_INFO = 10; - private static int sNextWindowId; - final HandlerCaller mCaller; - final Context mContext; final Object mLock = new Object(); @@ -152,7 +151,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub int eventType = message.arg1; synchronized (mLock) { - notifyEventListenerLocked(service, eventType); + notifyAccessibilityEventLocked(service, eventType); } } }; @@ -165,7 +164,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub public AccessibilityManagerService(Context context) { mContext = context; mPackageManager = mContext.getPackageManager(); - mCaller = new HandlerCaller(context, this); mWindowManagerService = (WindowManagerService) ServiceManager.getService( Context.WINDOW_SERVICE); mSecurityPolicy = new SecurityPolicy(); @@ -395,32 +393,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - public void executeMessage(Message message) { - switch (message.what) { - case DO_SET_SERVICE_INFO: { - SomeArgs arguments = ((SomeArgs) message.obj); - - AccessibilityServiceInfo info = (AccessibilityServiceInfo) arguments.arg1; - Service service = (Service) arguments.arg2; - - synchronized (mLock) { - // If the XML manifest had data to configure the service its info - // should be already set. In such a case update only the dynamically - // configurable properties. - AccessibilityServiceInfo oldInfo = service.mAccessibilityServiceInfo; - if (oldInfo != null) { - oldInfo.updateDynamicallyConfigurableProperties(info); - service.setDynamicallyConfigurableProperties(oldInfo); - } else { - service.setDynamicallyConfigurableProperties(info); - } - } - } return; - default: - Slog.w(LOG_TAG, "Unknown message type: " + message.what); - } - } - public int addAccessibilityInteractionConnection(IWindow windowToken, IAccessibilityInteractionConnection connection) throws RemoteException { synchronized (mLock) { @@ -455,7 +427,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - public void registerUiTestAutomationService(IEventListener listener, + public void registerUiTestAutomationService(IAccessibilityServiceClient serviceClient, AccessibilityServiceInfo accessibilityServiceInfo) { mSecurityPolicy.enforceCallingPermission(Manifest.permission.RETRIEVE_WINDOW_CONTENT, FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE); @@ -480,18 +452,45 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } // Hook the automation service up. mUiAutomationService = new Service(componentName, accessibilityServiceInfo, true); - mUiAutomationService.onServiceConnected(componentName, listener.asBinder()); + mUiAutomationService.onServiceConnected(componentName, serviceClient.asBinder()); } - public void unregisterUiTestAutomationService(IEventListener listener) { + public void unregisterUiTestAutomationService(IAccessibilityServiceClient serviceClient) { synchronized (mLock) { // Automation service is not bound, so pretend it died to perform clean up. - if (mUiAutomationService != null) { + if (mUiAutomationService != null + && mUiAutomationService.mServiceInterface == serviceClient) { mUiAutomationService.binderDied(); } } } + @Override + public void onGesture(int gestureId) { + synchronized (mLock) { + final boolean dispatched = notifyGestureLocked(gestureId, false); + if (!dispatched) { + notifyGestureLocked(gestureId, true); + } + } + } + + private boolean notifyGestureLocked(int gestureId, boolean isDefault) { + final int serviceCount = mServices.size(); + for (int i = 0; i < serviceCount; i++) { + Service service = mServices.get(i); + if (service.mIsDefault == isDefault) { + try { + service.mServiceInterface.onGesture(gestureId); + return true; + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error dispatching gesture."); + } + } + } + return false; + } + /** * Removes an AccessibilityInteractionConnection. * @@ -588,13 +587,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } /** - * Notifies a service for a scheduled event given the event type. + * Notifies an accessibility service client for a scheduled event given the event type. * - * @param service The service. + * @param service The service client. * @param eventType The type of the event to dispatch. */ - private void notifyEventListenerLocked(Service service, int eventType) { - IEventListener listener = service.mServiceInterface; + private void notifyAccessibilityEventLocked(Service service, int eventType) { + IAccessibilityServiceClient listener = service.mServiceInterface; // If the service died/was disabled while the message for dispatching // the accessibility event was propagating the listener may be null. @@ -699,6 +698,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return false; } + if (!event.isImportantForAccessibility() + && !service.mIncludeNotImportantViews) { + return false; + } + int eventType = event.getEventType(); if ((service.mEventTypes & eventType) != eventType) { return false; @@ -864,7 +868,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (!mHasInputFilter) { mHasInputFilter = true; if (mInputFilter == null) { - mInputFilter = new AccessibilityInputFilter(mContext); + mInputFilter = new AccessibilityInputFilter(mContext, this); } mWindowManagerService.setInputFilter(mInputFilter); } @@ -942,7 +946,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub IBinder mService; - IEventListener mServiceInterface; + IAccessibilityServiceClient mServiceInterface; int mEventTypes; @@ -952,6 +956,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub boolean mIsDefault; + boolean mIncludeNotImportantViews; + long mNotificationTimeout; ComponentName mComponentName; @@ -983,6 +989,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mContext, 0, new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS), 0)); } else { mCanRetrieveScreenContent = true; + mIncludeNotImportantViews = true; } setDynamicallyConfigurableProperties(accessibilityServiceInfo); } @@ -995,7 +1002,17 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mPackageNames.addAll(Arrays.asList(packageNames)); } mNotificationTimeout = info.notificationTimeout; - mIsDefault = (info.flags & AccessibilityServiceInfo.DEFAULT) != 0; + mIsDefault = (info.flags & DEFAULT) != 0; + + final int targetSdkVersion = + info.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion; + // TODO: Uncomment this line and remove the line below when JellyBean + // SDK version is finalized. + // if (targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN) { + if (targetSdkVersion > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + mIncludeNotImportantViews = + (info.flags & INCLUDE_NOT_IMPORTANT_VIEWS) != 0; + } synchronized (mLock) { tryAddServiceLocked(this); @@ -1043,13 +1060,33 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return (mEventTypes != 0 && mFeedbackType != 0 && mService != null); } + @Override + public AccessibilityServiceInfo getServiceInfo() { + synchronized (mLock) { + return mAccessibilityServiceInfo; + } + } + + @Override public void setServiceInfo(AccessibilityServiceInfo info) { - mCaller.obtainMessageOO(DO_SET_SERVICE_INFO, info, this).sendToTarget(); + synchronized (mLock) { + // If the XML manifest had data to configure the service its info + // should be already set. In such a case update only the dynamically + // configurable properties. + AccessibilityServiceInfo oldInfo = mAccessibilityServiceInfo; + if (oldInfo != null) { + oldInfo.updateDynamicallyConfigurableProperties(info); + setDynamicallyConfigurableProperties(oldInfo); + } else { + setDynamicallyConfigurableProperties(info); + } + } } + @Override public void onServiceConnected(ComponentName componentName, IBinder service) { mService = service; - mServiceInterface = IEventListener.Stub.asInterface(service); + mServiceInterface = IAccessibilityServiceClient.Stub.asInterface(service); try { mServiceInterface.setConnection(this, mId); synchronized (mLock) { @@ -1060,6 +1097,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } + @Override public float findAccessibilityNodeInfoByViewId(int accessibilityWindowId, long accessibilityNodeId, int viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) @@ -1078,11 +1116,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } } + final int flags = (mIncludeNotImportantViews) ? + AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0; final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); try { connection.findAccessibilityNodeInfoByViewId(accessibilityNodeId, viewId, - interactionId, callback, interrogatingPid, interrogatingTid); + interactionId, callback, flags, interrogatingPid, interrogatingTid); } catch (RemoteException re) { if (DEBUG) { Slog.e(LOG_TAG, "Error findAccessibilityNodeInfoByViewId()."); @@ -1093,6 +1133,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return getCompatibilityScale(resolvedWindowId); } + @Override public float findAccessibilityNodeInfosByText(int accessibilityWindowId, long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) @@ -1112,11 +1153,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } } + final int flags = (mIncludeNotImportantViews) ? + AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0; final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); try { connection.findAccessibilityNodeInfosByText(accessibilityNodeId, text, - interactionId, callback, interrogatingPid, interrogatingTid); + interactionId, callback, flags, interrogatingPid, interrogatingTid); } catch (RemoteException re) { if (DEBUG) { Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfosByText()"); @@ -1127,11 +1170,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return getCompatibilityScale(resolvedWindowId); } + @Override public float findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, long accessibilityNodeId, int interactionId, - IAccessibilityInteractionConnectionCallback callback, long interrogatingTid, - int prefetchFlags) - throws RemoteException { + IAccessibilityInteractionConnectionCallback callback, int flags, + long interrogatingTid) throws RemoteException { final int resolvedWindowId = resolveAccessibilityWindowId(accessibilityWindowId); IAccessibilityInteractionConnection connection = null; synchronized (mLock) { @@ -1147,11 +1190,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } } + final int allFlags = flags | ((mIncludeNotImportantViews) ? + AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0); final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); try { connection.findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId, - interactionId, callback, prefetchFlags, interrogatingPid, interrogatingTid); + interactionId, callback, allFlags, interrogatingPid, interrogatingTid); } catch (RemoteException re) { if (DEBUG) { Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()"); @@ -1162,6 +1207,81 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return getCompatibilityScale(resolvedWindowId); } + @Override + public float findFocus(int accessibilityWindowId, long accessibilityNodeId, + int focusType, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId = resolveAccessibilityWindowId(accessibilityWindowId); + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + final boolean permissionGranted = + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); + if (!permissionGranted) { + return 0; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return 0; + } + } + } + final int flags = (mIncludeNotImportantViews) ? + AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0; + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + try { + connection.findFocus(accessibilityNodeId, interactionId, focusType, callback, + flags, interrogatingPid, interrogatingTid); + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling findAccessibilityFocus()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return getCompatibilityScale(resolvedWindowId); + } + + @Override + public float focusSearch(int accessibilityWindowId, long accessibilityNodeId, + int direction, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId = resolveAccessibilityWindowId(accessibilityWindowId); + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + final boolean permissionGranted = + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); + if (!permissionGranted) { + return 0; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return 0; + } + } + } + final int flags = (mIncludeNotImportantViews) ? + AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0; + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + try { + connection.focusSearch(accessibilityNodeId, interactionId, direction, callback, + flags, interrogatingPid, interrogatingTid); + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling accessibilityFocusSearch()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return getCompatibilityScale(resolvedWindowId); + } + + @Override public boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, int action, int interactionId, IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) { @@ -1179,11 +1299,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } } + final int flags = (mIncludeNotImportantViews) ? + AccessibilityNodeInfo.INCLUDE_NOT_IMPORTANT_VIEWS : 0; final int interrogatingPid = Binder.getCallingPid(); final long identityToken = Binder.clearCallingIdentity(); try { connection.performAccessibilityAction(accessibilityNodeId, action, interactionId, - callback, interrogatingPid, interrogatingTid); + callback, flags, interrogatingPid, interrogatingTid); } catch (RemoteException re) { if (DEBUG) { Slog.e(LOG_TAG, "Error calling performAccessibilityAction()"); @@ -1263,22 +1385,35 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } final class SecurityPolicy { - private static final int VALID_ACTIONS = AccessibilityNodeInfo.ACTION_FOCUS - | AccessibilityNodeInfo.ACTION_CLEAR_FOCUS | AccessibilityNodeInfo.ACTION_SELECT - | AccessibilityNodeInfo.ACTION_CLEAR_SELECTION; + private static final int VALID_ACTIONS = + AccessibilityNodeInfo.ACTION_CLICK + | AccessibilityNodeInfo.ACTION_FOCUS + | AccessibilityNodeInfo.ACTION_CLEAR_FOCUS + | AccessibilityNodeInfo.ACTION_SELECT + | AccessibilityNodeInfo.ACTION_CLEAR_SELECTION + | AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS + | AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS; private static final int RETRIEVAL_ALLOWING_EVENT_TYPES = - AccessibilityEvent.TYPE_VIEW_CLICKED | AccessibilityEvent.TYPE_VIEW_FOCUSED - | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT - | AccessibilityEvent.TYPE_VIEW_LONG_CLICKED | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED - | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED | AccessibilityEvent.TYPE_VIEW_SELECTED + AccessibilityEvent.TYPE_VIEW_CLICKED + | AccessibilityEvent.TYPE_VIEW_FOCUSED + | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER + | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT + | AccessibilityEvent.TYPE_VIEW_LONG_CLICKED + | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED + | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + | AccessibilityEvent.TYPE_VIEW_SELECTED | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED - | AccessibilityEvent.TYPE_VIEW_SCROLLED; + | AccessibilityEvent.TYPE_VIEW_SCROLLED + | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED + | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED; private static final int RETRIEVAL_ALLOWING_WINDOW_CHANGE_EVENT_TYPES = - AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER - | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT; + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER + | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT + | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED; private int mRetrievalAlowingWindowId; diff --git a/services/java/com/android/server/accessibility/TouchExplorer.java b/services/java/com/android/server/accessibility/TouchExplorer.java index d07aa7a..5d01c77 100644 --- a/services/java/com/android/server/accessibility/TouchExplorer.java +++ b/services/java/com/android/server/accessibility/TouchExplorer.java @@ -20,17 +20,25 @@ import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATI import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START; import android.content.Context; +import android.gesture.Gesture; +import android.gesture.GestureLibraries; +import android.gesture.GestureLibrary; +import android.gesture.GesturePoint; +import android.gesture.GestureStroke; +import android.gesture.Prediction; import android.os.Handler; import android.util.Slog; import android.view.MotionEvent; +import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.view.WindowManagerPolicy; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; -import com.android.server.accessibility.AccessibilityInputFilter.Explorer; import com.android.server.input.InputFilter; +import com.android.internal.R; +import java.util.ArrayList; import java.util.Arrays; /** @@ -54,34 +62,24 @@ import java.util.Arrays; * * @hide */ -public class TouchExplorer implements Explorer { +public class TouchExplorer { + private static final boolean DEBUG = false; // Tag for logging received events. - private static final String LOG_TAG_RECEIVED = "TouchExplorer-RECEIVED"; - // Tag for logging injected events. - private static final String LOG_TAG_INJECTED = "TouchExplorer-INJECTED"; - // Tag for logging the current state. - private static final String LOG_TAG_STATE = "TouchExplorer-STATE"; + private static final String LOG_TAG = "TouchExplorer"; // States this explorer can be in. private static final int STATE_TOUCH_EXPLORING = 0x00000001; private static final int STATE_DRAGGING = 0x00000002; private static final int STATE_DELEGATING = 0x00000004; - - // Invalid pointer ID. - private static final int INVALID_POINTER_ID = -1; + private static final int STATE_GESTURE_DETECTING = 0x00000005; // The time slop in milliseconds for activating an item after it has // been touch explored. Tapping on an item within this slop will perform // a click and tapping and holding down a long press. private static final long ACTIVATION_TIME_SLOP = 2000; - // This constant captures the current implementation detail that - // pointer IDs are between 0 and 31 inclusive (subject to change). - // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) - private static final int MAX_POINTER_COUNT = 32; - // The minimum of the cosine between the vectors of two moving // pointers so they can be considered moving in the same direction. private static final float MAX_DRAGGING_ANGLE_COS = 0.525321989f; // cos(pi/4) @@ -92,6 +90,14 @@ public class TouchExplorer implements Explorer { // Constant referring to the ids bits of all pointers. private static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF; + // This constant captures the current implementation detail that + // pointer IDs are between 0 and 31 inclusive (subject to change). + // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) + public static final int MAX_POINTER_COUNT = 32; + + // Invalid pointer ID. + public static final int INVALID_POINTER_ID = -1; + // Temporary array for storing pointer IDs. private final int[] mTempPointerIds = new int[MAX_POINTER_COUNT]; @@ -103,10 +109,6 @@ public class TouchExplorer implements Explorer { // which delegates event processing to this touch explorer. private final InputFilter mInputFilter; - // Helper class for tracking pointers on the screen, for example which - // pointers are down, which are active, etc. - private final PointerTracker mPointerTracker; - // Handle to the accessibility manager for firing accessibility events // announcing touch exploration gesture start and end. private final AccessibilityManager mAccessibilityManager; @@ -132,21 +134,48 @@ public class TouchExplorer implements Explorer { // Command for delayed sending of a long press. private final PerformLongPressDelayed mPerformLongPressDelayed; + private VelocityTracker mVelocityTracker; + + private final ReceivedPointerTracker mReceivedPointerTracker; + + private final InjectedPointerTracker mInjectedPointerTracker; + + private final GestureListener mGestureListener; + + /** + * Callback for gesture detection. + */ + public interface GestureListener { + + /** + * Called when a given gesture was performed. + * + * @param gestureId The gesture id. + */ + public void onGesture(int gestureId); + } + /** * Creates a new instance. * * @param inputFilter The input filter associated with this explorer. * @param context A context handle for accessing resources. */ - public TouchExplorer(InputFilter inputFilter, Context context) { + public TouchExplorer(InputFilter inputFilter, Context context, + GestureListener gestureListener) { + mGestureListener = gestureListener; + mReceivedPointerTracker = new ReceivedPointerTracker(context); + mInjectedPointerTracker = new InjectedPointerTracker(); mInputFilter = inputFilter; mTouchExplorationTapSlop = - ViewConfiguration.get(context).getScaledTouchExplorationTapSlop(); - mPointerTracker = new PointerTracker(context); + ViewConfiguration.get(context).getScaledTouchExploreTapSlop(); mHandler = new Handler(context.getMainLooper()); mSendHoverDelayed = new SendHoverDelayed(); mPerformLongPressDelayed = new PerformLongPressDelayed(); mAccessibilityManager = AccessibilityManager.getInstance(context); + mGestureLibrary = GestureLibraries.fromRawResource(context, R.raw.accessibility_gestures); + mGestureLibrary.setOrientationStyle(4); + mGestureLibrary.load(); } public void clear(MotionEvent event, int policyFlags) { @@ -154,18 +183,14 @@ public class TouchExplorer implements Explorer { clear(); } - /** - * {@inheritDoc} - */ public void onMotionEvent(MotionEvent event, int policyFlags) { if (DEBUG) { - Slog.d(LOG_TAG_RECEIVED, "Received event: " + event + ", policyFlags=0x" + Slog.d(LOG_TAG, "Received event: " + event + ", policyFlags=0x" + Integer.toHexString(policyFlags)); - Slog.d(LOG_TAG_STATE, getStateSymbolicName(mCurrentState)); + Slog.d(LOG_TAG, getStateSymbolicName(mCurrentState)); } - // Keep track of the pointers's state. - mPointerTracker.onReceivedMotionEvent(event); + mReceivedPointerTracker.onMotionEvent(event); switch(mCurrentState) { case STATE_TOUCH_EXPLORING: { @@ -177,9 +202,11 @@ public class TouchExplorer implements Explorer { case STATE_DELEGATING: { handleMotionEventStateDelegating(event, policyFlags); } break; - default: { + case STATE_GESTURE_DETECTING: { + handleMotionEventGestureDetecting(event, policyFlags); + } break; + default: throw new IllegalStateException("Illegal state: " + mCurrentState); - } } } @@ -190,8 +217,14 @@ public class TouchExplorer implements Explorer { * @param policyFlags The policy flags associated with the event. */ private void handleMotionEventStateTouchExploring(MotionEvent event, int policyFlags) { - PointerTracker pointerTracker = mPointerTracker; - final int activePointerCount = pointerTracker.getActivePointerCount(); + ReceivedPointerTracker receivedTracker = mReceivedPointerTracker; + InjectedPointerTracker injectedTracker = mInjectedPointerTracker; + final int activePointerCount = receivedTracker.getActivePointerCount(); + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(event); switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: @@ -205,9 +238,9 @@ public class TouchExplorer implements Explorer { mSendHoverDelayed.remove(); mPerformLongPressDelayed.remove(); // Send a hover for every finger down so the user gets feedback. - final int pointerId = pointerTracker.getPrimaryActivePointerId(); + final int pointerId = receivedTracker.getPrimaryActivePointerId(); final int pointerIdBits = (1 << pointerId); - final int lastAction = pointerTracker.getLastInjectedHoverAction(); + final int lastAction = injectedTracker.getLastInjectedHoverAction(); // Deliver hover enter with a delay to have a change to detect // whether the user actually starts a scrolling gesture. @@ -232,7 +265,7 @@ public class TouchExplorer implements Explorer { // If the down is in the time slop => schedule a long press. final long pointerDownTime = - pointerTracker.getReceivedPointerDownTime(pointerId); + receivedTracker.getReceivedPointerDownTime(pointerId); final long lastExploreTime = mLastTouchExploreEvent.getEventTime(); final long deltaTimeExplore = pointerDownTime - lastExploreTime; if (deltaTimeExplore <= ACTIVATION_TIME_SLOP) { @@ -247,7 +280,7 @@ public class TouchExplorer implements Explorer { } } break; case MotionEvent.ACTION_MOVE: { - final int pointerId = pointerTracker.getPrimaryActivePointerId(); + final int pointerId = receivedTracker.getPrimaryActivePointerId(); final int pointerIndex = event.findPointerIndex(pointerId); final int pointerIdBits = (1 << pointerId); switch (activePointerCount) { @@ -258,13 +291,27 @@ public class TouchExplorer implements Explorer { // Detect touch exploration gesture start by having one active pointer // that moved more than a given distance. if (!mTouchExploreGestureInProgress) { - final float deltaX = pointerTracker.getReceivedPointerDownX(pointerId) + final float deltaX = receivedTracker.getReceivedPointerDownX(pointerId) - event.getX(pointerIndex); - final float deltaY = pointerTracker.getReceivedPointerDownY(pointerId) + final float deltaY = receivedTracker.getReceivedPointerDownY(pointerId) - event.getY(pointerIndex); final double moveDelta = Math.hypot(deltaX, deltaY); if (moveDelta > mTouchExplorationTapSlop) { + + mVelocityTracker.computeCurrentVelocity(1000); + final float maxAbsVelocity = Math.max( + Math.abs(mVelocityTracker.getXVelocity(pointerId)), + Math.abs(mVelocityTracker.getYVelocity(pointerId))); + // TODO: Tune the velocity cut off and add a constant. + if (maxAbsVelocity > 1000) { + clear(event, policyFlags); + mCurrentState = STATE_GESTURE_DETECTING; + event.setAction(MotionEvent.ACTION_DOWN); + handleMotionEventGestureDetecting(event, policyFlags); + return; + } + mTouchExploreGestureInProgress = true; sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_START); // Make sure the scheduled down/move event is sent. @@ -272,7 +319,7 @@ public class TouchExplorer implements Explorer { mPerformLongPressDelayed.remove(); // If we have transitioned to exploring state from another one // we need to send a hover enter event here. - final int lastAction = mPointerTracker.getLastInjectedHoverAction(); + final int lastAction = injectedTracker.getLastInjectedHoverAction(); if (lastAction == MotionEvent.ACTION_HOVER_EXIT) { sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER, pointerIdBits, policyFlags); @@ -355,12 +402,12 @@ public class TouchExplorer implements Explorer { } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: { - final int pointerId = pointerTracker.getLastReceivedUpPointerId(); + final int pointerId = receivedTracker.getLastReceivedUpPointerId(); final int pointerIdBits = (1 << pointerId); switch (activePointerCount) { case 0: { // If the pointer that went up was not active we have nothing to do. - if (!pointerTracker.wasLastReceivedUpPointerActive()) { + if (!receivedTracker.wasLastReceivedUpPointerActive()) { break; } @@ -381,7 +428,7 @@ public class TouchExplorer implements Explorer { if (mLastTouchExploreEvent != null) { // If the down was not in the time slop => nothing else to do. final long eventTime = - pointerTracker.getLastReceivedUpPointerDownTime(); + receivedTracker.getLastReceivedUpPointerDownTime(); final long exploreTime = mLastTouchExploreEvent.getEventTime(); final long deltaTime = eventTime - exploreTime; if (deltaTime > ACTIVATION_TIME_SLOP) { @@ -422,14 +469,22 @@ public class TouchExplorer implements Explorer { } } break; } + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + mVelocityTracker = null; + } } break; case MotionEvent.ACTION_CANCEL: { mSendHoverDelayed.remove(); mPerformLongPressDelayed.remove(); - final int pointerId = pointerTracker.getPrimaryActivePointerId(); + final int pointerId = receivedTracker.getPrimaryActivePointerId(); final int pointerIdBits = (1 << pointerId); ensureHoverExitSent(event, pointerIdBits, policyFlags); clear(); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + mVelocityTracker = null; + } } break; } } @@ -455,7 +510,7 @@ public class TouchExplorer implements Explorer { sendDownForAllActiveNotInjectedPointers(event, policyFlags); } break; case MotionEvent.ACTION_MOVE: { - final int activePointerCount = mPointerTracker.getActivePointerCount(); + final int activePointerCount = mReceivedPointerTracker.getActivePointerCount(); switch (activePointerCount) { case 1: { // do nothing @@ -487,7 +542,7 @@ public class TouchExplorer implements Explorer { } } break; case MotionEvent.ACTION_POINTER_UP: { - final int activePointerCount = mPointerTracker.getActivePointerCount(); + final int activePointerCount = mReceivedPointerTracker.getActivePointerCount(); switch (activePointerCount) { case 1: { // Send an event to the end of the drag gesture. @@ -525,7 +580,8 @@ public class TouchExplorer implements Explorer { case MotionEvent.ACTION_MOVE: { // Check whether some other pointer became active because they have moved // a given distance and if such exist send them to the view hierarchy - final int notInjectedCount = mPointerTracker.getNotInjectedActivePointerCount(); + final int notInjectedCount = getNotInjectedActivePointerCount( + mReceivedPointerTracker, mInjectedPointerTracker); if (notInjectedCount > 0) { MotionEvent prototype = MotionEvent.obtain(event); sendDownForAllActiveNotInjectedPointers(prototype, policyFlags); @@ -533,7 +589,7 @@ public class TouchExplorer implements Explorer { } break; case MotionEvent.ACTION_POINTER_UP: { // No active pointers => go to initial state. - if (mPointerTracker.getActivePointerCount() == 0) { + if (mReceivedPointerTracker.getActivePointerCount() == 0) { mCurrentState = STATE_TOUCH_EXPLORING; } } break; @@ -545,6 +601,72 @@ public class TouchExplorer implements Explorer { sendMotionEventStripInactivePointers(event, policyFlags); } + private float mPreviousX; + private float mPreviousY; + + private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); + + private static final int TOUCH_TOLERANCE = 3; + private static final float MIN_PREDICTION_SCORE = 2.0f; + + private GestureLibrary mGestureLibrary; + + private void handleMotionEventGestureDetecting(MotionEvent event, int policyFlags) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + final float x = event.getX(); + final float y = event.getY(); + mPreviousX = x; + mPreviousY = y; + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + } break; + case MotionEvent.ACTION_MOVE: { + final float x = event.getX(); + final float y = event.getY(); + final float dX = Math.abs(x - mPreviousX); + final float dY = Math.abs(y - mPreviousY); + if (dX >= TOUCH_TOLERANCE || dY >= TOUCH_TOLERANCE) { + mPreviousX = x; + mPreviousY = y; + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + } + } break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: { + float x = event.getX(); + float y = event.getY(); + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + + Gesture gesture = new Gesture(); + gesture.addStroke(new GestureStroke(mStrokeBuffer)); + + ArrayList<Prediction> predictions = mGestureLibrary.recognize(gesture); + if (!predictions.isEmpty()) { + Prediction bestPrediction = predictions.get(0); + if (bestPrediction.score >= MIN_PREDICTION_SCORE) { + if (DEBUG) { + Slog.i(LOG_TAG, "gesture: " + bestPrediction.name + " score: " + + bestPrediction.score); + } + try { + final int gestureId = Integer.parseInt(bestPrediction.name); + mGestureListener.onGesture(gestureId); + } catch (NumberFormatException nfe) { + Slog.w(LOG_TAG, "Non numeric gesture id:" + bestPrediction.name); + } + } + } + + mStrokeBuffer.clear(); + mCurrentState = STATE_TOUCH_EXPLORING; + } break; + case MotionEvent.ACTION_CANCEL: { + mStrokeBuffer.clear(); + mCurrentState = STATE_TOUCH_EXPLORING; + } break; + } + } + /** * Sends down events to the view hierarchy for all active pointers which are * not already being delivered i.e. pointers that are not yet injected. @@ -553,14 +675,15 @@ public class TouchExplorer implements Explorer { * @param policyFlags The policy flags associated with the event. */ private void sendDownForAllActiveNotInjectedPointers(MotionEvent prototype, int policyFlags) { - final PointerTracker pointerTracker = mPointerTracker; + ReceivedPointerTracker receivedPointers = mReceivedPointerTracker; + InjectedPointerTracker injectedPointers = mInjectedPointerTracker; int pointerIdBits = 0; final int pointerCount = prototype.getPointerCount(); // Find which pointers are already injected. for (int i = 0; i < pointerCount; i++) { final int pointerId = prototype.getPointerId(i); - if (pointerTracker.isInjectedPointerDown(pointerId)) { + if (injectedPointers.isInjectedPointerDown(pointerId)) { pointerIdBits |= (1 << pointerId); } } @@ -569,11 +692,11 @@ public class TouchExplorer implements Explorer { for (int i = 0; i < pointerCount; i++) { final int pointerId = prototype.getPointerId(i); // Skip inactive pointers. - if (!pointerTracker.isActivePointer(pointerId)) { + if (!receivedPointers.isActivePointer(pointerId)) { continue; } // Do not send event for already delivered pointers. - if (pointerTracker.isInjectedPointerDown(pointerId)) { + if (injectedPointers.isInjectedPointerDown(pointerId)) { continue; } pointerIdBits |= (1 << pointerId); @@ -590,7 +713,7 @@ public class TouchExplorer implements Explorer { * @param policyFlags The policy flags associated with the event. */ private void ensureHoverExitSent(MotionEvent prototype, int pointerIdBits, int policyFlags) { - final int lastAction = mPointerTracker.getLastInjectedHoverAction(); + final int lastAction = mInjectedPointerTracker.getLastInjectedHoverAction(); if (lastAction != MotionEvent.ACTION_HOVER_EXIT) { sendMotionEvent(prototype, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits, policyFlags); @@ -605,13 +728,13 @@ public class TouchExplorer implements Explorer { * @param policyFlags The policy flags associated with the event. */ private void sendUpForInjectedDownPointers(MotionEvent prototype, int policyFlags) { - final PointerTracker pointerTracker = mPointerTracker; + final InjectedPointerTracker injectedTracked = mInjectedPointerTracker; int pointerIdBits = 0; final int pointerCount = prototype.getPointerCount(); for (int i = 0; i < pointerCount; i++) { final int pointerId = prototype.getPointerId(i); // Skip non injected down pointers. - if (!pointerTracker.isInjectedPointerDown(pointerId)) { + if (!injectedTracked.isInjectedPointerDown(pointerId)) { continue; } pointerIdBits |= (1 << pointerId); @@ -627,18 +750,18 @@ public class TouchExplorer implements Explorer { * @param policyFlags The policy flags associated with the event. */ private void sendMotionEventStripInactivePointers(MotionEvent prototype, int policyFlags) { - PointerTracker pointerTracker = mPointerTracker; + ReceivedPointerTracker receivedTracker = mReceivedPointerTracker; // All pointers active therefore we just inject the event as is. - if (prototype.getPointerCount() == pointerTracker.getActivePointerCount()) { + if (prototype.getPointerCount() == receivedTracker.getActivePointerCount()) { sendMotionEvent(prototype, prototype.getAction(), ALL_POINTER_ID_BITS, policyFlags); return; } // No active pointers and the one that just went up was not // active, therefore we have nothing to do. - if (pointerTracker.getActivePointerCount() == 0 - && !pointerTracker.wasLastReceivedUpPointerActive()) { + if (receivedTracker.getActivePointerCount() == 0 + && !receivedTracker.wasLastReceivedUpPointerActive()) { return; } @@ -647,7 +770,7 @@ public class TouchExplorer implements Explorer { final int actionMasked = prototype.getActionMasked(); final int actionPointerId = prototype.getPointerId(prototype.getActionIndex()); if (actionMasked != MotionEvent.ACTION_MOVE) { - if (!pointerTracker.isActiveOrWasLastActiveUpPointer(actionPointerId)) { + if (!receivedTracker.isActiveOrWasLastActiveUpPointer(actionPointerId)) { return; } } @@ -658,7 +781,7 @@ public class TouchExplorer implements Explorer { final int pointerCount = prototype.getPointerCount(); for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) { final int pointerId = prototype.getPointerId(pointerIndex); - if (pointerTracker.isActiveOrWasLastActiveUpPointer(pointerId)) { + if (receivedTracker.isActiveOrWasLastActiveUpPointer(pointerId)) { pointerIdBits |= (1 << pointerId); } } @@ -700,19 +823,20 @@ public class TouchExplorer implements Explorer { if (action == MotionEvent.ACTION_DOWN) { event.setDownTime(event.getEventTime()); } else { - event.setDownTime(mPointerTracker.getLastInjectedDownEventTime()); + event.setDownTime(mInjectedPointerTracker.getLastInjectedDownEventTime()); } if (DEBUG) { - Slog.d(LOG_TAG_INJECTED, "Injecting event: " + event + ", policyFlags=0x" + Slog.d(LOG_TAG, "Injecting event: " + event + ", policyFlags=0x" + Integer.toHexString(policyFlags)); } // Make sure that the user will see the event. policyFlags |= WindowManagerPolicy.FLAG_PASS_TO_USER; - mPointerTracker.onInjectedMotionEvent(event); mInputFilter.sendInputEvent(event, policyFlags); + mInjectedPointerTracker.onMotionEvent(event); + if (event != prototype) { event.recycle(); } @@ -730,9 +854,9 @@ public class TouchExplorer implements Explorer { switch (actionMasked) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: { - PointerTracker pointerTracker = mPointerTracker; + InjectedPointerTracker injectedTracker = mInjectedPointerTracker; // Compute the action based on how many down pointers are injected. - if (pointerTracker.getInjectedPointerDownCount() == 0) { + if (injectedTracker.getInjectedPointerDownCount() == 0) { return MotionEvent.ACTION_DOWN; } else { return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) @@ -740,9 +864,9 @@ public class TouchExplorer implements Explorer { } } case MotionEvent.ACTION_POINTER_UP: { - PointerTracker pointerTracker = mPointerTracker; + InjectedPointerTracker injectedTracker = mInjectedPointerTracker; // Compute the action based on how many down pointers are injected. - if (pointerTracker.getInjectedPointerDownCount() == 1) { + if (injectedTracker.getInjectedPointerDownCount() == 1) { return MotionEvent.ACTION_UP; } else { return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) @@ -761,9 +885,9 @@ public class TouchExplorer implements Explorer { * @return True if the gesture is a dragging one. */ private boolean isDraggingGesture(MotionEvent event) { - PointerTracker pointerTracker = mPointerTracker; + ReceivedPointerTracker receivedTracker = mReceivedPointerTracker; int[] pointerIds = mTempPointerIds; - pointerTracker.populateActivePointerIds(pointerIds); + receivedTracker.populateActivePointerIds(pointerIds); final int firstPtrIndex = event.findPointerIndex(pointerIds[0]); final int secondPtrIndex = event.findPointerIndex(pointerIds[1]); @@ -775,9 +899,9 @@ public class TouchExplorer implements Explorer { // Check if the pointers are moving in the same direction. final float firstDeltaX = - firstPtrX - pointerTracker.getReceivedPointerDownX(firstPtrIndex); + firstPtrX - receivedTracker.getReceivedPointerDownX(firstPtrIndex); final float firstDeltaY = - firstPtrY - pointerTracker.getReceivedPointerDownY(firstPtrIndex); + firstPtrY - receivedTracker.getReceivedPointerDownY(firstPtrIndex); if (firstDeltaX == 0 && firstDeltaY == 0) { return true; @@ -791,9 +915,9 @@ public class TouchExplorer implements Explorer { (firstMagnitude > 0) ? firstDeltaY / firstMagnitude : firstDeltaY; final float secondDeltaX = - secondPtrX - pointerTracker.getReceivedPointerDownX(secondPtrIndex); + secondPtrX - receivedTracker.getReceivedPointerDownX(secondPtrIndex); final float secondDeltaY = - secondPtrY - pointerTracker.getReceivedPointerDownY(secondPtrIndex); + secondPtrY - receivedTracker.getReceivedPointerDownY(secondPtrIndex); if (secondDeltaX == 0 && secondDeltaY == 0) { return true; @@ -832,7 +956,8 @@ public class TouchExplorer implements Explorer { public void clear() { mSendHoverDelayed.remove(); mPerformLongPressDelayed.remove(); - mPointerTracker.clear(); + mReceivedPointerTracker.clear(); + mInjectedPointerTracker.clear(); mLastTouchExploreEvent = null; mCurrentState = STATE_TOUCH_EXPLORING; mTouchExploreGestureInProgress = false; @@ -853,27 +978,253 @@ public class TouchExplorer implements Explorer { return "STATE_DRAGGING"; case STATE_DELEGATING: return "STATE_DELEGATING"; + case STATE_GESTURE_DETECTING: + return "STATE_GESTURE_DETECTING"; default: throw new IllegalArgumentException("Unknown state: " + state); } } /** - * Helper class for tracking pointers and more specifically which of - * them are currently down, which are active, and which are delivered - * to the view hierarchy. The enclosing {@link TouchExplorer} uses the - * pointer state reported by this class to perform touch exploration. - * <p> - * The main purpose of this class is to allow the touch explorer to - * disregard pointers put down by accident by the user and not being - * involved in the interaction. For example, a blind user grabs the - * device with her left hand such that she touches the screen and she - * uses her right hand's index finger to explore the screen content. - * In this scenario the touches generated by the left hand are to be - * ignored. + * @return The number of non injected active pointers. */ - class PointerTracker { - private static final String LOG_TAG = "PointerTracker"; + private int getNotInjectedActivePointerCount(ReceivedPointerTracker receivedTracker, + InjectedPointerTracker injectedTracker) { + final int pointerState = receivedTracker.getActivePointers() + & ~injectedTracker.getInjectedPointersDown(); + return Integer.bitCount(pointerState); + } + + /** + * Class for delayed sending of long press. + */ + private final class PerformLongPressDelayed implements Runnable { + private MotionEvent mEvent; + private int mPolicyFlags; + + public void post(MotionEvent prototype, int policyFlags, long delay) { + mEvent = MotionEvent.obtain(prototype); + mPolicyFlags = policyFlags; + mHandler.postDelayed(this, delay); + } + + public void remove() { + if (isPenidng()) { + mHandler.removeCallbacks(this); + clear(); + } + } + + private boolean isPenidng() { + return (mEvent != null); + } + + @Override + public void run() { + mCurrentState = STATE_DELEGATING; + // Make sure the scheduled hover exit is delivered. + mSendHoverDelayed.remove(); + final int pointerId = mReceivedPointerTracker.getPrimaryActivePointerId(); + final int pointerIdBits = (1 << pointerId); + ensureHoverExitSent(mEvent, pointerIdBits, mPolicyFlags); + + sendDownForAllActiveNotInjectedPointers(mEvent, mPolicyFlags); + mTouchExploreGestureInProgress = false; + mLastTouchExploreEvent = null; + clear(); + } + + private void clear() { + if (!isPenidng()) { + return; + } + mEvent.recycle(); + mEvent = null; + mPolicyFlags = 0; + } + } + + /** + * Class for delayed sending of hover events. + */ + private final class SendHoverDelayed implements Runnable { + private MotionEvent mEvent; + private int mAction; + private int mPointerIdBits; + private int mPolicyFlags; + + public void post(MotionEvent prototype, int action, int pointerIdBits, int policyFlags, + long delay) { + remove(); + mEvent = MotionEvent.obtain(prototype); + mAction = action; + mPointerIdBits = pointerIdBits; + mPolicyFlags = policyFlags; + mHandler.postDelayed(this, delay); + } + + public void remove() { + mHandler.removeCallbacks(this); + clear(); + } + + private boolean isPenidng() { + return (mEvent != null); + } + + private void clear() { + if (!isPenidng()) { + return; + } + mEvent.recycle(); + mEvent = null; + mAction = 0; + mPointerIdBits = -1; + mPolicyFlags = 0; + } + + public void forceSendAndRemove() { + if (isPenidng()) { + run(); + remove(); + } + } + + public void run() { + if (DEBUG) { + if (mAction == MotionEvent.ACTION_HOVER_ENTER) { + Slog.d(LOG_TAG, "Injecting: " + MotionEvent.ACTION_HOVER_ENTER); + } else if (mAction == MotionEvent.ACTION_HOVER_MOVE) { + Slog.d(LOG_TAG, "Injecting: MotionEvent.ACTION_HOVER_MOVE"); + } else if (mAction == MotionEvent.ACTION_HOVER_EXIT) { + Slog.d(LOG_TAG, "Injecting: MotionEvent.ACTION_HOVER_EXIT"); + } + } + + sendMotionEvent(mEvent, mAction, mPointerIdBits, mPolicyFlags); + clear(); + } + } + + @Override + public String toString() { + return LOG_TAG; + } + + class InjectedPointerTracker { + private static final String LOG_TAG_INJECTED_POINTER_TRACKER = "InjectedPointerTracker"; + + // Keep track of which pointers sent to the system are down. + private int mInjectedPointersDown; + + // The time of the last injected down. + private long mLastInjectedDownEventTime; + + // The action of the last injected hover event. + private int mLastInjectedHoverEventAction = MotionEvent.ACTION_HOVER_EXIT; + + /** + * Processes an injected {@link MotionEvent} event. + * + * @param event The event to process. + */ + public void onMotionEvent(MotionEvent event) { + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + final int pointerId = event.getPointerId(event.getActionIndex()); + final int pointerFlag = (1 << pointerId); + mInjectedPointersDown |= pointerFlag; + mLastInjectedDownEventTime = event.getDownTime(); + } break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: { + final int pointerId = event.getPointerId(event.getActionIndex()); + final int pointerFlag = (1 << pointerId); + mInjectedPointersDown &= ~pointerFlag; + if (mInjectedPointersDown == 0) { + mLastInjectedDownEventTime = 0; + } + } break; + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_HOVER_EXIT: { + mLastInjectedHoverEventAction = event.getActionMasked(); + } break; + } + if (DEBUG) { + Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer: " + toString()); + } + } + + /** + * Clears the internals state. + */ + public void clear() { + mInjectedPointersDown = 0; + } + + /** + * @return The time of the last injected down event. + */ + public long getLastInjectedDownEventTime() { + return mLastInjectedDownEventTime; + } + + /** + * @return The number of down pointers injected to the view hierarchy. + */ + public int getInjectedPointerDownCount() { + return Integer.bitCount(mInjectedPointersDown); + } + + /** + * @return The bits of the injected pointers that are down. + */ + public int getInjectedPointersDown() { + return mInjectedPointersDown; + } + + /** + * Whether an injected pointer is down. + * + * @param pointerId The unique pointer id. + * @return True if the pointer is down. + */ + public boolean isInjectedPointerDown(int pointerId) { + final int pointerFlag = (1 << pointerId); + return (mInjectedPointersDown & pointerFlag) != 0; + } + + /** + * @return The action of the last injected hover event. + */ + public int getLastInjectedHoverAction() { + return mLastInjectedHoverEventAction; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("========================="); + builder.append("\nDown pointers #"); + builder.append(Integer.bitCount(mInjectedPointersDown)); + builder.append(" [ "); + for (int i = 0; i < MAX_POINTER_COUNT; i++) { + if ((mInjectedPointersDown & i) != 0) { + builder.append(i); + builder.append(" "); + } + } + builder.append("]"); + builder.append("\n========================="); + return builder.toString(); + } + } + + class ReceivedPointerTracker { + private static final String LOG_TAG_RECEIVED_POINTER_TRACKER = "ReceivedPointerTracker"; // The coefficient by which to multiply // ViewConfiguration.#getScaledTouchSlop() @@ -902,26 +1253,19 @@ public class TouchExplorer implements Explorer { // Flag indicating that there is at least one active pointer moving. private boolean mHasMovingActivePointer; - // Keep track of which pointers sent to the system are down. - private int mInjectedPointersDown; - // Keep track of the last up pointer data. private long mLastReceivedUpPointerDownTime; private int mLastReceivedUpPointerId; private boolean mLastReceivedUpPointerActive; - - // The time of the last injected down. - private long mLastInjectedDownEventTime; - - // The action of the last injected hover event. - private int mLastInjectedHoverEventAction = MotionEvent.ACTION_HOVER_EXIT; + private float mLastReceivedUpPointerDownX; + private float mLastReceivedUpPointerDownY; /** * Creates a new instance. * * @param context Context for looking up resources. */ - public PointerTracker(Context context) { + public ReceivedPointerTracker(Context context) { mThresholdActivePointer = ViewConfiguration.get(context).getScaledTouchSlop() * COEFFICIENT_ACTIVE_POINTER; } @@ -937,10 +1281,11 @@ public class TouchExplorer implements Explorer { mActivePointers = 0; mPrimaryActivePointerId = 0; mHasMovingActivePointer = false; - mInjectedPointersDown = 0; mLastReceivedUpPointerDownTime = 0; mLastReceivedUpPointerId = 0; mLastReceivedUpPointerActive = false; + mLastReceivedUpPointerDownX = 0; + mLastReceivedUpPointerDownY = 0; } /** @@ -948,12 +1293,10 @@ public class TouchExplorer implements Explorer { * * @param event The event to process. */ - public void onReceivedMotionEvent(MotionEvent event) { + public void onMotionEvent(MotionEvent event) { final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: { - // New gesture so restart tracking injected down pointers. - mInjectedPointersDown = 0; handleReceivedPointerDown(event.getActionIndex(), event); } break; case MotionEvent.ACTION_POINTER_DOWN: { @@ -970,47 +1313,22 @@ public class TouchExplorer implements Explorer { } break; } if (DEBUG) { - Slog.i(LOG_TAG, "Received pointer: " + toString()); + Slog.i(LOG_TAG_RECEIVED_POINTER_TRACKER, "Received pointer: " + toString()); } } /** - * Processes an injected {@link MotionEvent} event. - * - * @param event The event to process. + * @return The number of received pointers that are down. */ - public void onInjectedMotionEvent(MotionEvent event) { - final int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: { - handleInjectedPointerDown(event.getActionIndex(), event); - mLastInjectedDownEventTime = event.getDownTime(); - } break; - case MotionEvent.ACTION_POINTER_DOWN: { - handleInjectedPointerDown(event.getActionIndex(), event); - } break; - case MotionEvent.ACTION_UP: { - handleInjectedPointerUp(event.getActionIndex(), event); - } break; - case MotionEvent.ACTION_POINTER_UP: { - handleInjectedPointerUp(event.getActionIndex(), event); - } break; - case MotionEvent.ACTION_HOVER_ENTER: - case MotionEvent.ACTION_HOVER_MOVE: - case MotionEvent.ACTION_HOVER_EXIT: { - mLastInjectedHoverEventAction = event.getActionMasked(); - } break; - } - if (DEBUG) { - Slog.i(LOG_TAG, "Injected pointer: " + toString()); - } + public int getReceivedPointerDownCount() { + return Integer.bitCount(mReceivedPointersDown); } /** - * @return The number of received pointers that are down. + * @return The bits of the pointers that are active. */ - public int getReceivedPointerDownCount() { - return Integer.bitCount(mReceivedPointersDown); + public int getActivePointers() { + return mActivePointers; } /** @@ -1032,24 +1350,6 @@ public class TouchExplorer implements Explorer { } /** - * Whether an injected pointer is down. - * - * @param pointerId The unique pointer id. - * @return True if the pointer is down. - */ - public boolean isInjectedPointerDown(int pointerId) { - final int pointerFlag = (1 << pointerId); - return (mInjectedPointersDown & pointerFlag) != 0; - } - - /** - * @return The number of down pointers injected to the view hierarchy. - */ - public int getInjectedPointerDownCount() { - return Integer.bitCount(mInjectedPointersDown); - } - - /** * Whether an input pointer is active. * * @param pointerId The unique pointer id. @@ -1108,27 +1408,27 @@ public class TouchExplorer implements Explorer { return mLastReceivedUpPointerId; } + /** - * @return Whether the last received pointer that went up was active. + * @return The down X of the last received pointer that went up. */ - public boolean wasLastReceivedUpPointerActive() { - return mLastReceivedUpPointerActive; + public float getLastReceivedUpPointerDownX() { + return mLastReceivedUpPointerDownX; } /** - * @return The time of the last injected down event. + * @return The down Y of the last received pointer that went up. */ - public long getLastInjectedDownEventTime() { - return mLastInjectedDownEventTime; + public float getLastReceivedUpPointerDownY() { + return mLastReceivedUpPointerDownY; } /** - * @return The action of the last injected hover event. + * @return Whether the last received pointer that went up was active. */ - public int getLastInjectedHoverAction() { - return mLastInjectedHoverEventAction; + public boolean wasLastReceivedUpPointerActive() { + return mLastReceivedUpPointerActive; } - /** * Populates the active pointer IDs to the given array. * <p> @@ -1147,18 +1447,10 @@ public class TouchExplorer implements Explorer { } /** - * @return The number of non injected active pointers. - */ - public int getNotInjectedActivePointerCount() { - final int pointerState = mActivePointers & ~mInjectedPointersDown; - return Integer.bitCount(pointerState); - } - - /** * @param pointerId The unique pointer id. * @return Whether the pointer is active or was the last active than went up. */ - private boolean isActiveOrWasLastActiveUpPointer(int pointerId) { + public boolean isActiveOrWasLastActiveUpPointer(int pointerId) { return (isActivePointer(pointerId) || (mLastReceivedUpPointerId == pointerId && mLastReceivedUpPointerActive)); @@ -1177,6 +1469,8 @@ public class TouchExplorer implements Explorer { mLastReceivedUpPointerId = 0; mLastReceivedUpPointerDownTime = 0; mLastReceivedUpPointerActive = false; + mLastReceivedUpPointerDownX = 0; + mLastReceivedUpPointerDownX = 0; mReceivedPointersDown |= pointerFlag; mReceivedPointerDownX[pointerId] = event.getX(pointerIndex); @@ -1217,6 +1511,8 @@ public class TouchExplorer implements Explorer { mLastReceivedUpPointerId = pointerId; mLastReceivedUpPointerDownTime = getReceivedPointerDownTime(pointerId); mLastReceivedUpPointerActive = isActivePointer(pointerId); + mLastReceivedUpPointerDownX = mReceivedPointerDownX[pointerId]; + mLastReceivedUpPointerDownY = mReceivedPointerDownY[pointerId]; mReceivedPointersDown &= ~pointerFlag; mActivePointers &= ~pointerFlag; @@ -1233,33 +1529,6 @@ public class TouchExplorer implements Explorer { } /** - * Handles a injected pointer down event. - * - * @param pointerIndex The index of the pointer that has changed. - * @param event The event to be handled. - */ - private void handleInjectedPointerDown(int pointerIndex, MotionEvent event) { - final int pointerId = event.getPointerId(pointerIndex); - final int pointerFlag = (1 << pointerId); - mInjectedPointersDown |= pointerFlag; - } - - /** - * Handles a injected pointer up event. - * - * @param pointerIndex The index of the pointer that has changed. - * @param event The event to be handled. - */ - private void handleInjectedPointerUp(int pointerIndex, MotionEvent event) { - final int pointerId = event.getPointerId(pointerIndex); - final int pointerFlag = (1 << pointerId); - mInjectedPointersDown &= ~pointerFlag; - if (mInjectedPointersDown == 0) { - mLastInjectedDownEventTime = 0; - } - } - - /** * Detects the active pointers in an event. * * @param event The event to examine. @@ -1348,117 +1617,4 @@ public class TouchExplorer implements Explorer { return builder.toString(); } } - - /** - * Class for delayed sending of long press. - */ - private final class PerformLongPressDelayed implements Runnable { - private MotionEvent mEvent; - private int mPolicyFlags; - - public void post(MotionEvent prototype, int policyFlags, long delay) { - mEvent = MotionEvent.obtain(prototype); - mPolicyFlags = policyFlags; - mHandler.postDelayed(this, delay); - } - - public void remove() { - if (isPenidng()) { - mHandler.removeCallbacks(this); - clear(); - } - } - - private boolean isPenidng() { - return (mEvent != null); - } - - @Override - public void run() { - mCurrentState = STATE_DELEGATING; - // Make sure the scheduled hover exit is delivered. - mSendHoverDelayed.remove(); - final int pointerId = mPointerTracker.getPrimaryActivePointerId(); - final int pointerIdBits = (1 << pointerId); - ensureHoverExitSent(mEvent, pointerIdBits, mPolicyFlags); - - sendDownForAllActiveNotInjectedPointers(mEvent, mPolicyFlags); - mTouchExploreGestureInProgress = false; - mLastTouchExploreEvent = null; - clear(); - } - - private void clear() { - if (!isPenidng()) { - return; - } - mEvent.recycle(); - mEvent = null; - mPolicyFlags = 0; - } - } - - /** - * Class for delayed sending of hover events. - */ - private final class SendHoverDelayed implements Runnable { - private static final String LOG_TAG = "SendHoverEnterOrExitDelayed"; - - private MotionEvent mEvent; - private int mAction; - private int mPointerIdBits; - private int mPolicyFlags; - - public void post(MotionEvent prototype, int action, int pointerIdBits, int policyFlags, - long delay) { - remove(); - mEvent = MotionEvent.obtain(prototype); - mAction = action; - mPointerIdBits = pointerIdBits; - mPolicyFlags = policyFlags; - mHandler.postDelayed(this, delay); - } - - public void remove() { - mHandler.removeCallbacks(this); - clear(); - } - - private boolean isPenidng() { - return (mEvent != null); - } - - private void clear() { - if (!isPenidng()) { - return; - } - mEvent.recycle(); - mEvent = null; - mAction = 0; - mPointerIdBits = -1; - mPolicyFlags = 0; - } - - public void forceSendAndRemove() { - if (isPenidng()) { - run(); - remove(); - } - } - - public void run() { - if (DEBUG) { - if (mAction == MotionEvent.ACTION_HOVER_ENTER) { - Slog.d(LOG_TAG, "Injecting: " + MotionEvent.ACTION_HOVER_ENTER); - } else if (mAction == MotionEvent.ACTION_HOVER_MOVE) { - Slog.d(LOG_TAG, "Injecting: MotionEvent.ACTION_HOVER_MOVE"); - } else if (mAction == MotionEvent.ACTION_HOVER_EXIT) { - Slog.d(LOG_TAG, "Injecting: MotionEvent.ACTION_HOVER_EXIT"); - } - } - - sendMotionEvent(mEvent, mAction, mPointerIdBits, mPolicyFlags); - clear(); - } - } } |