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