Accessibility focus - framework
Usefulness: Keep track of the current user location in the screen when
traversing the it. Enabling structural and directional
navigation over all elements on the screen. This enables
blind users that know the application layout to efficiently
locate desired elements as opposed to try touch exploring the
region where the the element should be - very tedious.
Rationale: There are two ways to implement accessibility focus One is
to let accessibility services keep track of it since they
have access to the screen content, and another to let the view
hierarchy keep track of it. While the first approach would
require almost no work on our part it poses several challenges
which make it a sub-optimal choice. Having the accessibility focus
in the accessibility service would require that service to scrape
the window content every time it changes to sync the view tree
state and the accessibility focus location. Pretty much the service
will have to keep an off screen model of the screen content. This
could be quite challenging to get right and would incur performance
cost for the multiple IPCs to repeatedly fetch the screen content.
Further, keeping virtual accessibility focus (i.e. in the service)
would require sync of the input and accessibility focus. This could
be challenging to implement right as well. Also, having an unlimited
number of accessibility services we cannot guarantee that they will
have a proper implementation, if any, to allow users to perform structural
navigation of the screen content. Assuming two accessibility
services implement structural navigation via accessibility focus,
there is not guarantee that they will behave similarly by default,
i.e. provide some standard way to navigate the screen content.
Also feedback from experienced accessibility researchers, specifically
T.V Raman, provides evidence that having virtual accessibility focus
creates many issues and it is very hard to get right.
Therefore, keeping accessibility focus in the system will avoid
keeping an off-screen model in accessibility services, it will always
be in sync with the state of the view hierarchy and the input focus.
Also this will allow having a default behavior for traversing the
screen via this accessibility focus that is consistent in all
accessibility services. We provide accessibility services with APIs to
override this behavior but all of them will perform screen traversal
in a consistent way by default.
Behavior: If accessibility is enabled the accessibility focus is the leading one
and the input follows it. Putting accessibility focus on a view moves
the input focus there. Clearing the accessibility focus of a view, clears
the input focus of this view. If accessibility focus is on a view that
cannot take input focus, then no other view should have input focus.
In accessibility mode we initially give accessibility focus to the topmost
view and no view has input focus. This ensures consistent behavior accross
all apps. Note that accessibility focus can move hierarchically in the
view tree and having it at the root is better than putting it where the
input focus would be - at the first input focusable which could be at
an arbitrary depth in the view tree. By default not all views are reported
for accessibility, only the important ones. A view may be explicitly labeled
as important or not for accessibility, or the system determines which one
is such - default. Important views for accessibility are all views that are
not dumb layout managers used only to arrange their chidren. Since the same
content arrangement can be obtained via different combintation of layout
managers, such managers cannot be used to reliably determine the application
structure. For example, a user should see a list as a list view with several
list items and each list item as a text view and a button as opposed to seeing
all the layout managers used to arrange the list item's content.
By default only important for accessibility views are regared for accessibility
purposes. View not regarded for accessibility neither fire accessibility events,
nor are reported being on the screen. An accessibility service may request the
system to regard all views. If the target SDK of an accessibility services is
less than JellyBean, then all views are regarded for accessibility.
Note that an accessibility service that requires all view to be ragarded for
accessibility may put accessibility focus on any view. Hence, it may implement
any navigational paradigm if desired. Especially considering the fact that
the system is detecting some standard gestures and delegates their processing
to an accessibility service. The default implementation of an accessibility
services performs the defualt navigation.
bug:5932640
bug:5605641
Change-Id: Ieac461d480579d706a847b9325720cb254736ebe
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 @@
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 @@
* @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 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 @@
}
/**
+ * 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 @@
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 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 @@
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 @@
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 @@
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.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 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 @@
* <strong>Can be dynamically set at runtime.</strong>
* </p>
* @see #DEFAULT
+ * @see #INCLUDE_NOT_IMPORTANT_VIEWS
*/
public int flags;
@@ -561,6 +595,8 @@
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
similarity index 86%
rename from core/java/android/accessibilityservice/IEventListener.aidl
rename to 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.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 @@
* 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 @@
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 @@
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 @@
private volatile int mConnectionId = AccessibilityInteractionClient.NO_ID;
- private IEventListenerWrapper mListener;
+ private IAccessibilityServiceClientWrapper mListener;
private AccessibilityEvent mLastEvent;
@@ -133,7 +133,7 @@
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 @@
mLock.notifyAll();
}
}
+
+ @Override
+ public void onGesture(int gestureId) {
+ /* do nothing */
+ }
});
final IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
@@ -252,6 +257,7 @@
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 @@
private static ThreadLocal<FocusFinder> tlFocusFinder =
new ThreadLocal<FocusFinder>() {
-
+ @Override
protected FocusFinder initialValue() {
return new FocusFinder();
}
@@ -48,6 +50,10 @@
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 @@
* @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 @@
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);
+
+ ArrayList<View> focusables = mTempList;
+ focusables.clear();
+ root.addFocusables(focusables, direction);
+ if (focusables.isEmpty()) {
+ // The focus cannot change.
+ return null;
+ }
+
+ 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();
+ }
}
- private void setFocusTopLeft(ViewGroup root) {
+ /**
+ * 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.
+ */
+ 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);
+ }
+ }
+
+ 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);
+ }
+ 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;
+ }
+
+ 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;
+ }
+
+ 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 setFocusBottomRight(ViewGroup root) {
+ private void setFocusTopLeft(ViewGroup root) {
final int rootTop = root.getScrollY();
final int rootLeft = root.getScrollX();
mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
}
- /**
- * 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) {
- ArrayList<View> focusables = root.getFocusables(direction);
- if (focusables.isEmpty()) {
- // The focus cannot change.
- return null;
- }
-
- 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);
- }
-
- 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 null;
- }
-
+ 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 @@
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 @@
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 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 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 @@
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 @@
| 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 @@
*/
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 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 @@
// 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 @@
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 @@
onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();
+
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ notifyAccessibilityStateChanged();
+ }
}
}
@@ -4050,16 +4158,21 @@
}
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 @@
onFocusChanged(false, 0, null);
refreshDrawableState();
+
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ notifyAccessibilityStateChanged();
+ }
}
}
@@ -4127,7 +4244,10 @@
*/
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 void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
if (mAccessibilityDelegate != null) {
- mAccessibilityDelegate.sendAccessibilityEventUnchecked(this, event);
+ mAccessibilityDelegate.sendAccessibilityEventUnchecked(this, event);
} else {
sendAccessibilityEventUncheckedInternal(event);
}
@@ -4257,6 +4377,31 @@
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 @@
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 @@
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 @@
info.setClickable(isClickable());
info.setFocusable(isFocusable());
info.setFocused(isFocused());
+ info.setAccessibilityFocused(isAccessibilityFocused());
info.setSelected(isSelected());
info.setLongClickable(isLongClickable());
@@ -4597,10 +4742,11 @@
* 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 @@
* 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 @@
*
* @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 @@
}
/**
+ * 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 @@
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 @@
* @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 @@
}
/**
+ * 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 @@
// 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 @@
dispatchGenericMotionEventInternal(event);
return true;
}
+
return false;
}
@@ -6806,7 +7361,6 @@
*/
private boolean isHoverable() {
final int viewFlags = mViewFlags;
- //noinspection SimplifiableIfStatement
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return false;
}
@@ -7130,6 +7684,9 @@
*/
if (mParent != null) mParent.focusableViewAvailable(this);
}
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ notifyAccessibilityStateChanged();
+ }
}
if ((flags & VISIBILITY_MASK) == VISIBLE) {
@@ -7161,6 +7718,7 @@
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 @@
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 @@
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 @@
* @param canvas the canvas on which to draw the view
*/
protected void dispatchDraw(Canvas canvas) {
+
}
/**
@@ -10227,6 +10793,7 @@
resolvePadding();
resolveTextDirection();
resolveTextAlignment();
+ clearAccessibilityFocus();
if (isFocused()) {
InputMethodManager imm = InputMethodManager.peekInstance();
imm.focusIn(this);
@@ -10457,6 +11024,8 @@
resetResolvedLayoutDirection();
resetResolvedTextAlignment();
+ resetAccessibilityStateChanged();
+ clearAccessibilityFocus();
}
/**
@@ -13287,6 +13856,9 @@
invalidate(true);
refreshDrawableState();
dispatchSetSelected(selected);
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ notifyAccessibilityStateChanged();
+ }
}
}
@@ -13456,7 +14028,7 @@
position[1] += view.mTop;
viewParent = view.mParent;
- }
+ }
if (viewParent instanceof ViewRootImpl) {
// *cough*
@@ -16291,7 +16863,7 @@
/**
* 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 @@
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 @@
* 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 @@
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 @@
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 @@
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 @@
*
* @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 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.util.Predicate;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashSet;
/**
@@ -611,13 +612,13 @@
/**
* {@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 @@
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 @@
@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 @@
}
/**
+ * @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 @@
clearChildFocus(view);
ensureInputFocusOnFirstFocusable();
}
+
+ if (view.isAccessibilityFocused()) {
+ view.clearAccessibilityFocus();
+ }
}
/**
@@ -5622,4 +5678,218 @@
}
}
}
+
+ /**
+ * 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 @@
* 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.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.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.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.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 @@
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 @@
SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent;
- AccessibilityNodePrefetcher mAccessibilityNodePrefetcher;
+ HashSet<View> mTempHashSet;
private final int mDensity;
@@ -630,6 +628,10 @@
if (mAccessibilityManager.isEnabled()) {
mAccessibilityInteractionConnectionManager.ensureConnection();
}
+
+ if (view.getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
}
}
}
@@ -1418,6 +1420,8 @@
mView.draw(layerCanvas);
+ drawAccessibilityFocusedDrawableIfNeeded(layerCanvas);
+
mResizeBufferStartTime = SystemClock.uptimeMillis();
mResizeBufferDuration = mView.getResources().getInteger(
com.android.internal.R.integer.config_mediumAnimTime);
@@ -1712,7 +1716,7 @@
attachInfo.mTreeObserver.dispatchOnGlobalLayout();
if (AccessibilityManager.getInstance(host.mContext).isEnabled()) {
- postSendWindowContentChangedCallback();
+ postSendWindowContentChangedCallback(mView);
}
}
@@ -1880,6 +1884,7 @@
mResizePaint.setAlpha(mResizeAlpha);
canvas.drawHardwareLayer(mResizeBuffer, 0.0f, mHardwareYOffset, mResizePaint);
}
+ drawAccessibilityFocusedDrawableIfNeeded(canvas);
}
/**
@@ -2234,6 +2239,8 @@
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 @@
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 @@
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 @@
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 @@
/**
* 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 @@
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 @@
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 @@
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 @@
}
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 @@
// 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 @@
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 @@
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 @@
v, mTempRect);
}
if (v.requestFocus(direction, mTempRect)) {
- playSoundEffect(
- SoundEffectConstants.getContantForFocusDirection(direction));
+ playSoundEffect(SoundEffectConstants
+ .getContantForFocusDirection(direction));
finishInputEvent(q, true);
return;
}
@@ -3683,22 +3760,11 @@
+ " 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 @@
* 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 @@
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 @@
}
} else {
ensureNoConnection();
+ setAccessibilityFocusedHost(null);
}
}
@@ -4991,14 +5102,15 @@
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 @@
}
}
+ @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 @@
}
}
+ @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 @@
}
}
+ @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 @@
}
}
}
- }
- /**
- * 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);
+ 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 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 {
+ // We cannot make the call and notify the caller so it does not wait.
try {
- callback.setFindAccessibilityNodeInfoResult(info, interactionId);
+ callback.setFindAccessibilityNodeInfoResult(null, interactionId);
} catch (RemoteException re) {
- /* ignore - the other side will time out */
+ /* best effort - ignore */
}
}
}
-
- 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);
- } 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 {
- try {
- callback.setPerformAccessibilityActionResult(succeeded, interactionId);
- } catch (RemoteException re) {
- /* ignore - the other side will time out */
- }
- }
- }
-
- 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 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 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 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 @@
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 @@
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 @@
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 @@
}
/**
+ * 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 @@
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 @@
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 @@
* @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 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 @@
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 @@
(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 @@
}
/**
+ * 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 @@
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 @@
* @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 @@
}
}
}
+
+ /**
+ * 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 @@
* @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 @@
* @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 @@
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 @@
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 @@
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 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 @@
}
/**
+ * 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 @@
*/
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 @@
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 @@
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 @@
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 @@
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 @@
}
} 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 @@
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.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 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 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 @@
}
} 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 @@
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 @@
}
}
+ 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 @@
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 @@
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 @@
// 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 @@
// 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 91e2e497..6c7ea67 100644
--- a/core/java/android/widget/ImageView.java
+++ b/core/java/android/widget/ImageView.java
@@ -105,11 +105,11 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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
new file mode 100644
index 0000000..f03f575
--- /dev/null
+++ b/core/res/res/drawable-nodpi/view_accessibility_focused.9.png
Binary files differ
diff --git a/core/res/res/raw/accessibility_gestures.bin b/core/res/res/raw/accessibility_gestures.bin
new file mode 100644
index 0000000..1f95e56
--- /dev/null
+++ b/core/res/res/raw/accessibility_gestures.bin
Binary files differ
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 @@
<!-- 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 @@
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 @@
* 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 @@
private ListView mListView;
public ListOfButtonsTest() {
- super("com.android.frameworks.coretests", ListOfButtons.class);
+ super(ListOfButtons.class);
}
@Override
@@ -47,6 +47,7 @@
super.setUp();
ListOfButtons a = getActivity();
+ getInstrumentation().waitForIdleSync();
mListAdapter = a.getListAdapter();
mButtonAtTop = (Button) a.findViewById(R.id.button);
mListView = a.getListView();