summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSvetoslav Ganov <svetoslavganov@google.com>2011-04-04 16:09:08 -0700
committerSvetoslav Ganov <svetoslavganov@google.com>2011-04-21 18:33:52 -0700
commitac84d3ba81f08036308b17e1ab919e43987a3df5 (patch)
tree8bf9d7f0b110667e45186d90674465a63e868f26
parent1cc1a41b5a095c2eae556c9be0a7ad3f2fc5bfbb (diff)
downloadframeworks_base-ac84d3ba81f08036308b17e1ab919e43987a3df5.zip
frameworks_base-ac84d3ba81f08036308b17e1ab919e43987a3df5.tar.gz
frameworks_base-ac84d3ba81f08036308b17e1ab919e43987a3df5.tar.bz2
Touch exploration feature, event bubling, refactor
1. Added an Input Filter that interprets the touch screen motion events to perfrom accessibility exploration. One finger explores. Tapping within a given time and distance slop on the last exlopred location does click and long press, respectively. Two fingers close and in the same diretion drag. Multiple finglers or two fingers in different directions or two fingers too far away are delegated to the view hierarchy. Non moving fingers "accidentally grabbed the device for the scrren" are ignored. 2. Added accessibility events for hover enter, hover exit, touch exoloration gesture start, and end. Accessibility hover events are fired by the hover pipeline. An accessibility event is dispatched up the view tree and the topmost view fires it. Thus predecessors can augment the fired event. An accessibility event has several records and a predecessor can optionally modify, delete, and add such to the event. 3. Added onPopulateAccessibilityEvent and refactored the existing accessibility code to use it. 4. Added API for querying the currently enabled accessibility services by feedback type. Change-Id: Iec03c6c3fe298de3f14cb6efdbb9b198cd531a0c
-rw-r--r--api/current.txt88
-rw-r--r--core/java/android/view/InputEventConsistencyVerifier.java19
-rw-r--r--core/java/android/view/View.java44
-rw-r--r--core/java/android/view/ViewConfiguration.java22
-rw-r--r--core/java/android/view/ViewGroup.java54
-rw-r--r--core/java/android/view/ViewParent.java19
-rw-r--r--core/java/android/view/ViewRoot.java8
-rw-r--r--core/java/android/view/accessibility/AccessibilityEvent.java470
-rw-r--r--core/java/android/view/accessibility/AccessibilityManager.java36
-rw-r--r--core/java/android/view/accessibility/AccessibilityRecord.java415
-rw-r--r--core/java/android/view/accessibility/IAccessibilityManager.aidl2
-rw-r--r--core/java/android/widget/AbsListView.java12
-rw-r--r--core/java/android/widget/AdapterView.java28
-rw-r--r--core/java/android/widget/CheckedTextView.java9
-rw-r--r--core/java/android/widget/CompoundButton.java26
-rw-r--r--core/java/android/widget/DatePicker.java7
-rw-r--r--core/java/android/widget/ListView.java40
-rw-r--r--core/java/android/widget/ProgressBar.java10
-rw-r--r--core/java/android/widget/TabWidget.java15
-rw-r--r--core/java/android/widget/TextView.java5
-rw-r--r--core/java/android/widget/TimePicker.java5
-rw-r--r--services/java/com/android/server/accessibility/AccessibilityInputFilter.java65
-rw-r--r--services/java/com/android/server/accessibility/AccessibilityManagerService.java74
-rw-r--r--services/java/com/android/server/accessibility/TouchExplorer.java1540
-rw-r--r--services/java/com/android/server/wm/InputFilter.java6
25 files changed, 2530 insertions, 489 deletions
diff --git a/api/current.txt b/api/current.txt
index 0f5c98a..f26852d 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -21163,6 +21163,7 @@ package android.view {
method protected void onLayout(boolean, int, int, int, int);
method protected void onMeasure(int, int);
method protected void onOverScrolled(int, int, boolean, boolean);
+ method public void onPopulateAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
method protected void onRestoreInstanceState(android.os.Parcelable);
method protected android.os.Parcelable onSaveInstanceState();
method protected void onScrollChanged(int, int, int, int);
@@ -21574,6 +21575,7 @@ package android.view {
method public boolean onInterceptTouchEvent(android.view.MotionEvent);
method protected abstract void onLayout(boolean, int, int, int, int);
method protected boolean onRequestFocusInDescendants(int, android.graphics.Rect);
+ method public boolean onRequestSendAccessibilityEvent(android.view.View, android.view.accessibility.AccessibilityEvent);
method public void recomputeViewAttributes(android.view.View);
method public void removeAllViews();
method public void removeAllViewsInLayout();
@@ -21586,6 +21588,7 @@ package android.view {
method public void requestChildFocus(android.view.View, android.view.View);
method public boolean requestChildRectangleOnScreen(android.view.View, android.graphics.Rect, boolean);
method public void requestDisallowInterceptTouchEvent(boolean);
+ method public boolean requestSendAccessibilityEvent(android.view.View, android.view.accessibility.AccessibilityEvent);
method public void requestTransparentRegion(android.view.View);
method public void scheduleLayoutAnimation();
method public void setAddStatesFromChildren(boolean);
@@ -21672,6 +21675,7 @@ package android.view {
method public abstract boolean requestChildRectangleOnScreen(android.view.View, android.graphics.Rect, boolean);
method public abstract void requestDisallowInterceptTouchEvent(boolean);
method public abstract void requestLayout();
+ method public abstract boolean requestSendAccessibilityEvent(android.view.View, android.view.accessibility.AccessibilityEvent);
method public abstract void requestTransparentRegion(android.view.View);
method public abstract boolean showContextMenuForChild(android.view.View);
method public abstract android.view.ActionMode startActionModeForChild(android.view.View, android.view.ActionMode.Callback);
@@ -22017,53 +22021,32 @@ package android.view {
package android.view.accessibility {
- public final class AccessibilityEvent implements android.os.Parcelable {
+ public final class AccessibilityEvent extends android.view.accessibility.AccessibilityRecord implements android.os.Parcelable {
+ method public void appendRecord(android.view.accessibility.AccessibilityRecord);
method public int describeContents();
- method public int getAddedCount();
- method public java.lang.CharSequence getBeforeText();
- method public java.lang.CharSequence getClassName();
- method public java.lang.CharSequence getContentDescription();
- method public int getCurrentItemIndex();
method public long getEventTime();
method public int getEventType();
- method public int getFromIndex();
- method public int getItemCount();
method public java.lang.CharSequence getPackageName();
- method public android.os.Parcelable getParcelableData();
- method public int getRemovedCount();
- method public java.util.List<java.lang.CharSequence> getText();
+ method public android.view.accessibility.AccessibilityRecord getRecord(int);
+ method public int getRecordCount();
method public void initFromParcel(android.os.Parcel);
- method public boolean isChecked();
- method public boolean isEnabled();
- method public boolean isFullScreen();
- method public boolean isPassword();
method public static android.view.accessibility.AccessibilityEvent obtain(int);
method public static android.view.accessibility.AccessibilityEvent obtain();
- method public void recycle();
- method public void setAddedCount(int);
- method public void setBeforeText(java.lang.CharSequence);
- method public void setChecked(boolean);
- method public void setClassName(java.lang.CharSequence);
- method public void setContentDescription(java.lang.CharSequence);
- method public void setCurrentItemIndex(int);
- method public void setEnabled(boolean);
method public void setEventTime(long);
method public void setEventType(int);
- method public void setFromIndex(int);
- method public void setFullScreen(boolean);
- method public void setItemCount(int);
method public void setPackageName(java.lang.CharSequence);
- method public void setParcelableData(android.os.Parcelable);
- method public void setPassword(boolean);
- method public void setRemovedCount(int);
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator CREATOR;
field public static final int INVALID_POSITION = -1; // 0xffffffff
field public static final deprecated int MAX_TEXT_LENGTH = 500; // 0x1f4
field public static final int TYPES_ALL_MASK = -1; // 0xffffffff
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_CLICKED = 1; // 0x1
field public static final int TYPE_VIEW_FOCUSED = 8; // 0x8
+ field public static final int TYPE_VIEW_HOVER_ENTER = 128; // 0x80
+ field public static final int TYPE_VIEW_HOVER_EXIT = 256; // 0x100
field public static final int TYPE_VIEW_LONG_CLICKED = 2; // 0x2
field public static final int TYPE_VIEW_SELECTED = 4; // 0x4
field public static final int TYPE_VIEW_TEXT_CHANGED = 16; // 0x10
@@ -22077,11 +22060,58 @@ package android.view.accessibility {
public final class AccessibilityManager {
method public java.util.List<android.content.pm.ServiceInfo> getAccessibilityServiceList();
+ method public java.util.List<android.content.pm.ServiceInfo> getEnabledAccessibilityServiceList(int);
method public void interrupt();
method public boolean isEnabled();
method public void sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
}
+ public class AccessibilityRecord {
+ ctor protected AccessibilityRecord();
+ method protected void clear();
+ method public int getAddedCount();
+ method public java.lang.CharSequence getBeforeText();
+ method public boolean getBooleanProperty(int);
+ method public java.lang.CharSequence getClassName();
+ method public java.lang.CharSequence getContentDescription();
+ method public int getCurrentItemIndex();
+ method public int getFromIndex();
+ method public int getItemCount();
+ method public android.os.Parcelable getParcelableData();
+ method public int getRemovedCount();
+ method public java.util.List<java.lang.CharSequence> getText();
+ method public boolean isChecked();
+ method public boolean isEnabled();
+ method public boolean isFullScreen();
+ method public boolean isPassword();
+ method protected static android.view.accessibility.AccessibilityRecord obtain();
+ method public void recycle();
+ method public void setAddedCount(int);
+ method public void setBeforeText(java.lang.CharSequence);
+ method public void setChecked(boolean);
+ method public void setClassName(java.lang.CharSequence);
+ method public void setContentDescription(java.lang.CharSequence);
+ method public void setCurrentItemIndex(int);
+ method public void setEnabled(boolean);
+ method public void setFromIndex(int);
+ method public void setFullScreen(boolean);
+ method public void setItemCount(int);
+ method public void setParcelableData(android.os.Parcelable);
+ method public void setPassword(boolean);
+ method public void setRemovedCount(int);
+ field protected int mAddedCount;
+ field protected java.lang.CharSequence mBeforeText;
+ field protected int mBooleanProperties;
+ field protected java.lang.CharSequence mClassName;
+ field protected java.lang.CharSequence mContentDescription;
+ field protected int mCurrentItemIndex;
+ field protected int mFromIndex;
+ field protected int mItemCount;
+ field protected android.os.Parcelable mParcelableData;
+ field protected int mRemovedCount;
+ field protected final java.util.List mText;
+ }
+
}
package android.view.animation {
diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java
index b5ca2c2..e14b975 100644
--- a/core/java/android/view/InputEventConsistencyVerifier.java
+++ b/core/java/android/view/InputEventConsistencyVerifier.java
@@ -30,7 +30,6 @@ import android.util.Log;
* @hide
*/
public final class InputEventConsistencyVerifier {
- private static final String TAG = "InputEventConsistencyVerifier";
private static final boolean IS_ENG_BUILD = "eng".equals(Build.TYPE);
// The number of recent events to log when a problem is detected.
@@ -44,6 +43,11 @@ public final class InputEventConsistencyVerifier {
// Consistency verifier flags.
private final int mFlags;
+ // Tag for logging which a client can set to help distinguish the output
+ // from different verifiers since several can be active at the same time.
+ // If not provided defaults to the simple class name.
+ private final String mLogTag;
+
// The most recently checked event and the nesting level at which it was checked.
// This is only set when the verifier is called from a nesting level greater than 0
// so that the verifier can detect when it has been asked to verify the same event twice.
@@ -103,8 +107,19 @@ public final class InputEventConsistencyVerifier {
* @param flags Flags to the verifier, or 0 if none.
*/
public InputEventConsistencyVerifier(Object caller, int flags) {
+ this(caller, flags, InputEventConsistencyVerifier.class.getSimpleName());
+ }
+
+ /**
+ * Creates an input consistency verifier.
+ * @param caller The object to which the verifier is attached.
+ * @param flags Flags to the verifier, or 0 if none.
+ * @param logTag Tag for logging. If null defaults to the short class name.
+ */
+ public InputEventConsistencyVerifier(Object caller, int flags, String logTag) {
this.mCaller = caller;
this.mFlags = flags;
+ this.mLogTag = (logTag != null) ? logTag : "InputEventConsistencyVerifier";
}
/**
@@ -596,7 +611,7 @@ public final class InputEventConsistencyVerifier {
}
}
- Log.d(TAG, mViolationMessage.toString());
+ Log.d(mLogTag, mViolationMessage.toString());
mViolationMessage.setLength(0);
tainted = true;
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index fe8af19..2f519f4 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -3455,6 +3455,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
if (!isShown()) {
return;
}
+
+ // Populate these here since they are related to the View that
+ // sends the event and should not be modified while dispatching
+ // to descendants.
event.setClassName(getClass().getName());
event.setPackageName(getContext().getPackageName());
event.setEnabled(isEnabled());
@@ -3470,22 +3474,38 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
dispatchPopulateAccessibilityEvent(event);
- AccessibilityManager.getInstance(mContext).sendAccessibilityEvent(event);
+ // In the beginning we called #isShown(), so we know that getParent() is not null.
+ getParent().requestSendAccessibilityEvent(this, event);
}
/**
- * Dispatches an {@link AccessibilityEvent} to the {@link View} children
- * to be populated.
+ * Dispatches an {@link AccessibilityEvent} to the {@link View} children to be populated.
+ * This method first calls {@link #onPopulateAccessibilityEvent(AccessibilityEvent)}
+ * on this view allowing it to populate information about itself and also decide
+ * whether to intercept the population i.e. to prevent its children from populating
+ * the event.
*
* @param event The event.
*
* @return True if the event population was completed.
*/
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ onPopulateAccessibilityEvent(event);
return false;
}
/**
+ * Called from {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)}
+ * giving a chance to this View to populate the accessibility evnet with
+ * information about itself.
+ *
+ * @param event The accessibility event which to populate.
+ */
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+
+ }
+
+ /**
* Gets the {@link View} description. It briefly describes the view and is
* primarily used for accessibility support. Set this property to enable
* better accessibility support for your application. This is especially
@@ -5390,20 +5410,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
* to receive the hover event.
*/
public boolean onHoverEvent(MotionEvent event) {
- final int viewFlags = mViewFlags;
-
- if (((viewFlags & CLICKABLE) != CLICKABLE &&
- (viewFlags & LONG_CLICKABLE) != LONG_CLICKABLE)) {
- // Nothing to do if the view is not clickable.
- return false;
- }
-
- if ((viewFlags & ENABLED_MASK) == DISABLED) {
- // A disabled view that is clickable still consumes the hover events, it just doesn't
- // respond to them.
- return true;
- }
-
switch (event.getAction()) {
case MotionEvent.ACTION_HOVER_ENTER:
setHovered(true);
@@ -5414,7 +5420,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
break;
}
- return true;
+ return false;
}
/**
@@ -5436,11 +5442,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
if ((mPrivateFlags & HOVERED) == 0) {
mPrivateFlags |= HOVERED;
refreshDrawableState();
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
}
} else {
if ((mPrivateFlags & HOVERED) != 0) {
mPrivateFlags &= ~HOVERED;
refreshDrawableState();
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
}
}
}
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index 739758c..94eb429 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -19,7 +19,6 @@ package android.view;
import android.app.AppGlobals;
import android.content.Context;
import android.content.res.Configuration;
-import android.os.Bundle;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.SparseArray;
@@ -156,6 +155,13 @@ public class ViewConfiguration {
private static final int MAXIMUM_FLING_VELOCITY = 8000;
/**
+ * Distance between a touch up event denoting the end of a touch exploration
+ * 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;
+
+ /**
* The maximum size of View's drawing cache, expressed in bytes. This size
* should be at least equal to the size of the screen in ARGB888 format.
*/
@@ -185,6 +191,7 @@ public class ViewConfiguration {
private final int mTouchSlop;
private final int mPagingTouchSlop;
private final int mDoubleTapSlop;
+ private final int mScaledTouchExplorationTapSlop;
private final int mWindowTouchSlop;
private final int mMaximumDrawingCacheSize;
private final int mOverscrollDistance;
@@ -206,6 +213,7 @@ public class ViewConfiguration {
mTouchSlop = TOUCH_SLOP;
mPagingTouchSlop = PAGING_TOUCH_SLOP;
mDoubleTapSlop = DOUBLE_TAP_SLOP;
+ mScaledTouchExplorationTapSlop = TOUCH_EXPLORATION_TAP_SLOP;
mWindowTouchSlop = WINDOW_TOUCH_SLOP;
//noinspection deprecation
mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE;
@@ -242,6 +250,7 @@ public class ViewConfiguration {
mTouchSlop = (int) (sizeAndDensity * TOUCH_SLOP + 0.5f);
mPagingTouchSlop = (int) (sizeAndDensity * PAGING_TOUCH_SLOP + 0.5f);
mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f);
+ mScaledTouchExplorationTapSlop = (int) (density * TOUCH_EXPLORATION_TAP_SLOP + 0.5f);
mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f);
// Size of the screen in bytes, in ARGB_8888 format
@@ -444,6 +453,17 @@ public class ViewConfiguration {
}
/**
+ * @return Distance between a touch up event denoting the end of a touch exploration
+ * 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.
+ *
+ * @hide
+ */
+ public int getScaledTouchExplorationTapSlop() {
+ return mScaledTouchExplorationTapSlop;
+ }
+
+ /**
* @return Distance a touch must be outside the bounds of a window for it
* to be counted as outside the window for purposes of dismissing that
* window.
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 08daa28..7b404b4 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -586,6 +586,35 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
/**
* {@inheritDoc}
*/
+ public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
+ ViewParent parent = getParent();
+ if (parent == null) {
+ return false;
+ }
+ final boolean propagate = onRequestSendAccessibilityEvent(child, event);
+ if (!propagate) {
+ return false;
+ }
+ return parent.requestSendAccessibilityEvent(this, event);
+ }
+
+ /**
+ * Called when a child has requested sending an {@link AccessibilityEvent} and
+ * gives an opportunity to its parent to augment the event.
+ *
+ * @param child The child which requests sending the event.
+ * @param event The event to be sent.
+ * @return True if the event should be sent.
+ *
+ * @see #requestSendAccessibilityEvent(View, AccessibilityEvent)
+ */
+ public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
@Override
public boolean dispatchUnhandledMove(View focused, int direction) {
return mFocused != null &&
@@ -1216,9 +1245,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, mHoveredChild);
eventNoHistory.setAction(action);
-
mHoveredChild = null;
- } else if (action == MotionEvent.ACTION_HOVER_MOVE) {
+ } else {
// Pointer is still within the child.
handled |= dispatchTransformedGenericPointerEvent(event, mHoveredChild);
}
@@ -1278,6 +1306,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
return handled;
}
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ // Handle the event only if leaf. This guarantees that
+ // the leafs (or any custom class that returns true from
+ // this method) will get a change to process the hover.
+ if (getChildCount() == 0) {
+ return super.onHoverEvent(event);
+ }
+ return false;
+ }
+
private static MotionEvent obtainMotionEventNoHistoryOrSelf(MotionEvent event) {
if (event.getHistorySize() == 0) {
return event;
@@ -2091,11 +2130,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
- boolean populated = false;
+ // We first get a chance to populate the event.
+ onPopulateAccessibilityEvent(event);
+ // Let our children have a shot in populating the event.
for (int i = 0, count = getChildCount(); i < count; i++) {
- populated |= getChildAt(i).dispatchPopulateAccessibilityEvent(event);
+ boolean handled = getChildAt(i).dispatchPopulateAccessibilityEvent(event);
+ if (handled) {
+ return handled;
+ }
}
- return populated;
+ return false;
}
/**
diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java
index d7d4c3f..655df39 100644
--- a/core/java/android/view/ViewParent.java
+++ b/core/java/android/view/ViewParent.java
@@ -17,6 +17,7 @@
package android.view;
import android.graphics.Rect;
+import android.view.accessibility.AccessibilityEvent;
/**
* Defines the responsibilities for a class that will be a parent of a View.
@@ -222,4 +223,22 @@ public interface ViewParent {
*/
public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
boolean immediate);
+
+ /**
+ * Called by a child to request from its parent to send an {@link AccessibilityEvent}.
+ * The child has already populated a record for itself in the event and is delegating
+ * to its parent to send the event. The parent can optionally add a record for itself.
+ * <p>
+ * Note: An accessibility event is fired by an individual view which populates the
+ * event with a record for its state and requests from its parent to perform
+ * the sending. The parent can optionally add a record for itself before
+ * dispatching the request to its parent. A parent can also choose not to
+ * respect the request for sending the event. The accessibility event is sent
+ * by the topmost view in the view tree.
+ *
+ * @param child The child which requests sending the event.
+ * @param event The event to be sent.
+ * @return True if the event was sent.
+ */
+ public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event);
}
diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java
index 1a6bae7..06c33f6 100644
--- a/core/java/android/view/ViewRoot.java
+++ b/core/java/android/view/ViewRoot.java
@@ -3531,6 +3531,14 @@ public final class ViewRoot extends Handler implements ViewParent,
public void childDrawableStateChanged(View child) {
}
+ public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
+ if (mView == null) {
+ return false;
+ }
+ AccessibilityManager.getInstance(child.mContext).sendAccessibilityEvent(event);
+ return true;
+ }
+
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java
index 9af19b8..11c9392 100644
--- a/core/java/android/view/accessibility/AccessibilityEvent.java
+++ b/core/java/android/view/accessibility/AccessibilityEvent.java
@@ -21,13 +21,26 @@ import android.os.Parcelable;
import android.text.TextUtils;
import java.util.ArrayList;
-import java.util.List;
/**
* This class represents accessibility events that are sent by the system when
* something notable happens in the user interface. For example, when a
* {@link android.widget.Button} is clicked, a {@link android.view.View} is focused, etc.
* <p>
+ * An accessibility event is fired by an individual view which populates the event with
+ * a record for its state and requests from its parent to send the event to interested
+ * parties. The parent can optionally add a record for itself before dispatching a similar
+ * request to its parent. A parent can also choose not to respect the request for sending
+ * an event. The accessibility event is sent by the topmost view in the view tree.
+ * Therefore, an {@link android.accessibilityservice.AccessibilityService} can explore
+ * all records in an accessibility event to obtain more information about the context
+ * in which the event was fired.
+ * <p>
+ * A client can add, remove, and modify records. The getters and setters for individual
+ * properties operate on the current record which can be explicitly set by the client. By
+ * default current is the first record. Thus, querying a record would require setting
+ * it as the current one and interacting with the property getters and setters.
+ * <p>
* This class represents various semantically different accessibility event
* types. Each event type has associated a set of related properties. In other
* words, each event type is characterized via a subset of the properties exposed
@@ -145,7 +158,7 @@ import java.util.List;
* @see android.view.accessibility.AccessibilityManager
* @see android.accessibilityservice.AccessibilityService
*/
-public final class AccessibilityEvent implements Parcelable {
+public final class AccessibilityEvent extends AccessibilityRecord implements Parcelable {
/**
* Invalid selection/focus position.
@@ -207,6 +220,26 @@ public final class AccessibilityEvent implements Parcelable {
public static final int TYPE_NOTIFICATION_STATE_CHANGED = 0x00000040;
/**
+ * Represents the event of a hover enter over a {@link android.view.View}.
+ */
+ public static final int TYPE_VIEW_HOVER_ENTER = 0x00000080;
+
+ /**
+ * Represents the event of a hover exit over a {@link android.view.View}.
+ */
+ public static final int TYPE_VIEW_HOVER_EXIT = 0x00000100;
+
+ /**
+ * Represents the event of starting a touch exploration gesture.
+ */
+ public static final int TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200;
+
+ /**
+ * Represents the event of ending a touch exploration gesture.
+ */
+ public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400;
+
+ /**
* Mask for {@link AccessibilityEvent} all types.
*
* @see #TYPE_VIEW_CLICKED
@@ -219,116 +252,53 @@ public final class AccessibilityEvent implements Parcelable {
*/
public static final int TYPES_ALL_MASK = 0xFFFFFFFF;
- private static final int MAX_POOL_SIZE = 2;
+ private static final int MAX_POOL_SIZE = 10;
private static final Object mPoolLock = new Object();
private static AccessibilityEvent sPool;
private static int sPoolSize;
- private static final int CHECKED = 0x00000001;
- private static final int ENABLED = 0x00000002;
- private static final int PASSWORD = 0x00000004;
- private static final int FULL_SCREEN = 0x00000080;
-
private AccessibilityEvent mNext;
+ private boolean mIsInPool;
private int mEventType;
- private int mBooleanProperties;
- private int mCurrentItemIndex;
- private int mItemCount;
- private int mFromIndex;
- private int mAddedCount;
- private int mRemovedCount;
-
- private long mEventTime;
-
- private CharSequence mClassName;
private CharSequence mPackageName;
- private CharSequence mContentDescription;
- private CharSequence mBeforeText;
-
- private Parcelable mParcelableData;
-
- private final List<CharSequence> mText = new ArrayList<CharSequence>();
+ private long mEventTime;
- private boolean mIsInPool;
+ private final ArrayList<AccessibilityRecord> mRecords = new ArrayList<AccessibilityRecord>();
/*
* Hide constructor from clients.
*/
private AccessibilityEvent() {
- mCurrentItemIndex = INVALID_POSITION;
- }
-
- /**
- * Gets if the source is checked.
- *
- * @return True if the view is checked, false otherwise.
- */
- public boolean isChecked() {
- return getBooleanProperty(CHECKED);
- }
-
- /**
- * Sets if the source is checked.
- *
- * @param isChecked True if the view is checked, false otherwise.
- */
- public void setChecked(boolean isChecked) {
- setBooleanProperty(CHECKED, isChecked);
- }
- /**
- * Gets if the source is enabled.
- *
- * @return True if the view is enabled, false otherwise.
- */
- public boolean isEnabled() {
- return getBooleanProperty(ENABLED);
- }
-
- /**
- * Sets if the source is enabled.
- *
- * @param isEnabled True if the view is enabled, false otherwise.
- */
- public void setEnabled(boolean isEnabled) {
- setBooleanProperty(ENABLED, isEnabled);
- }
-
- /**
- * Gets if the source is a password field.
- *
- * @return True if the view is a password field, false otherwise.
- */
- public boolean isPassword() {
- return getBooleanProperty(PASSWORD);
}
/**
- * Sets if the source is a password field.
+ * Gets the number of records contained in the event.
*
- * @param isPassword True if the view is a password field, false otherwise.
+ * @return The number of records.
*/
- public void setPassword(boolean isPassword) {
- setBooleanProperty(PASSWORD, isPassword);
+ public int getRecordCount() {
+ return mRecords.size();
}
/**
- * Sets if the source is taking the entire screen.
+ * Appends an {@link AccessibilityRecord} to the end of event records.
*
- * @param isFullScreen True if the source is full screen, false otherwise.
+ * @param record The record to append.
*/
- public void setFullScreen(boolean isFullScreen) {
- setBooleanProperty(FULL_SCREEN, isFullScreen);
+ public void appendRecord(AccessibilityRecord record) {
+ mRecords.add(record);
}
/**
- * Gets if the source is taking the entire screen.
+ * Gets the records at a given index.
*
- * @return True if the source is full screen, false otherwise.
+ * @param index The index.
+ * @return The records at the specified index.
*/
- public boolean isFullScreen() {
- return getBooleanProperty(FULL_SCREEN);
+ public AccessibilityRecord getRecord(int index) {
+ return mRecords.get(index);
}
/**
@@ -350,96 +320,6 @@ public final class AccessibilityEvent implements Parcelable {
}
/**
- * Gets the number of items that can be visited.
- *
- * @return The number of items.
- */
- public int getItemCount() {
- return mItemCount;
- }
-
- /**
- * Sets the number of items that can be visited.
- *
- * @param itemCount The number of items.
- */
- public void setItemCount(int itemCount) {
- mItemCount = itemCount;
- }
-
- /**
- * Gets the index of the source in the list of items the can be visited.
- *
- * @return The current item index.
- */
- public int getCurrentItemIndex() {
- return mCurrentItemIndex;
- }
-
- /**
- * Sets the index of the source in the list of items that can be visited.
- *
- * @param currentItemIndex The current item index.
- */
- public void setCurrentItemIndex(int currentItemIndex) {
- mCurrentItemIndex = currentItemIndex;
- }
-
- /**
- * Gets the index of the first character of the changed sequence.
- *
- * @return The index of the first character.
- */
- public int getFromIndex() {
- return mFromIndex;
- }
-
- /**
- * Sets the index of the first character of the changed sequence.
- *
- * @param fromIndex The index of the first character.
- */
- public void setFromIndex(int fromIndex) {
- mFromIndex = fromIndex;
- }
-
- /**
- * Gets the number of added characters.
- *
- * @return The number of added characters.
- */
- public int getAddedCount() {
- return mAddedCount;
- }
-
- /**
- * Sets the number of added characters.
- *
- * @param addedCount The number of added characters.
- */
- public void setAddedCount(int addedCount) {
- mAddedCount = addedCount;
- }
-
- /**
- * Gets the number of removed characters.
- *
- * @return The number of removed characters.
- */
- public int getRemovedCount() {
- return mRemovedCount;
- }
-
- /**
- * Sets the number of removed characters.
- *
- * @param removedCount The number of removed characters.
- */
- public void setRemovedCount(int removedCount) {
- mRemovedCount = removedCount;
- }
-
- /**
* Gets the time in which this event was sent.
*
* @return The event time.
@@ -458,24 +338,6 @@ public final class AccessibilityEvent implements Parcelable {
}
/**
- * Gets the class name of the source.
- *
- * @return The class name.
- */
- public CharSequence getClassName() {
- return mClassName;
- }
-
- /**
- * Sets the class name of the source.
- *
- * @param className The lass name.
- */
- public void setClassName(CharSequence className) {
- mClassName = className;
- }
-
- /**
* Gets the package name of the source.
*
* @return The package name.
@@ -494,70 +356,6 @@ public final class AccessibilityEvent implements Parcelable {
}
/**
- * Gets the text of the event. The index in the list represents the priority
- * of the text. Specifically, the lower the index the higher the priority.
- *
- * @return The text.
- */
- public List<CharSequence> getText() {
- return mText;
- }
-
- /**
- * Sets the text before a change.
- *
- * @return The text before the change.
- */
- public CharSequence getBeforeText() {
- return mBeforeText;
- }
-
- /**
- * Sets the text before a change.
- *
- * @param beforeText The text before the change.
- */
- public void setBeforeText(CharSequence beforeText) {
- mBeforeText = beforeText;
- }
-
- /**
- * Gets the description of the source.
- *
- * @return The description.
- */
- public CharSequence getContentDescription() {
- return mContentDescription;
- }
-
- /**
- * Sets the description of the source.
- *
- * @param contentDescription The description.
- */
- public void setContentDescription(CharSequence contentDescription) {
- mContentDescription = contentDescription;
- }
-
- /**
- * Gets the {@link Parcelable} data.
- *
- * @return The parcelable data.
- */
- public Parcelable getParcelableData() {
- return mParcelableData;
- }
-
- /**
- * Sets the {@link Parcelable} data of the event.
- *
- * @param parcelableData The parcelable data.
- */
- public void setParcelableData(Parcelable parcelableData) {
- mParcelableData = parcelableData;
- }
-
- /**
* Returns a cached instance if such is available or a new one is
* instantiated with type property set.
*
@@ -595,11 +393,11 @@ public final class AccessibilityEvent implements Parcelable {
* <p>
* <b>Note: You must not touch the object after calling this function.</b>
*/
+ @Override
public void recycle() {
if (mIsInPool) {
return;
}
-
clear();
synchronized (mPoolLock) {
if (sPoolSize <= MAX_POOL_SIZE) {
@@ -614,44 +412,15 @@ public final class AccessibilityEvent implements Parcelable {
/**
* Clears the state of this instance.
*/
- private void clear() {
+ @Override
+ protected void clear() {
+ super.clear();
mEventType = 0;
- mBooleanProperties = 0;
- mCurrentItemIndex = INVALID_POSITION;
- mItemCount = 0;
- mFromIndex = 0;
- mAddedCount = 0;
- mRemovedCount = 0;
- mEventTime = 0;
- mClassName = null;
mPackageName = null;
- mContentDescription = null;
- mBeforeText = null;
- mParcelableData = null;
- mText.clear();
- }
-
- /**
- * Gets the value of a boolean property.
- *
- * @param property The property.
- * @return The value.
- */
- private boolean getBooleanProperty(int property) {
- return (mBooleanProperties & property) == property;
- }
-
- /**
- * Sets a boolean property.
- *
- * @param property The property.
- * @param value The value.
- */
- private void setBooleanProperty(int property, boolean value) {
- if (value) {
- mBooleanProperties |= property;
- } else {
- mBooleanProperties &= ~property;
+ mEventTime = 0;
+ while (!mRecords.isEmpty()) {
+ AccessibilityRecord record = mRecords.remove(0);
+ record.recycle();
}
}
@@ -662,38 +431,82 @@ public final class AccessibilityEvent implements Parcelable {
*/
public void initFromParcel(Parcel parcel) {
mEventType = parcel.readInt();
- mBooleanProperties = parcel.readInt();
- mCurrentItemIndex = parcel.readInt();
- mItemCount = parcel.readInt();
- mFromIndex = parcel.readInt();
- mAddedCount = parcel.readInt();
- mRemovedCount = parcel.readInt();
- mEventTime = parcel.readLong();
- mClassName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
- mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
- mBeforeText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
- mParcelableData = parcel.readParcelable(null);
- parcel.readList(mText, null);
+ mEventTime = parcel.readLong();
+ readAccessibilityRecordFromParcel(this, parcel);
+
+ // Read the records.
+ final int recordCount = parcel.readInt();
+ for (int i = 0; i < recordCount; i++) {
+ AccessibilityRecord record = AccessibilityRecord.obtain();
+ readAccessibilityRecordFromParcel(record, parcel);
+ mRecords.add(record);
+ }
}
+ /**
+ * Reads an {@link AccessibilityRecord} from a parcel.
+ *
+ * @param record The record to initialize.
+ * @param parcel The parcel to read from.
+ */
+ private void readAccessibilityRecordFromParcel(AccessibilityRecord record,
+ Parcel parcel) {
+ record.mBooleanProperties = parcel.readInt();
+ record.mCurrentItemIndex = parcel.readInt();
+ record.mItemCount = parcel.readInt();
+ record.mFromIndex = parcel.readInt();
+ record.mAddedCount = parcel.readInt();
+ record.mRemovedCount = parcel.readInt();
+ record.mClassName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ record.mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ record.mBeforeText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ record.mParcelableData = parcel.readParcelable(null);
+ parcel.readList(record.mText, null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeInt(mEventType);
- parcel.writeInt(mBooleanProperties);
- parcel.writeInt(mCurrentItemIndex);
- parcel.writeInt(mItemCount);
- parcel.writeInt(mFromIndex);
- parcel.writeInt(mAddedCount);
- parcel.writeInt(mRemovedCount);
- parcel.writeLong(mEventTime);
- TextUtils.writeToParcel(mClassName, parcel, 0);
TextUtils.writeToParcel(mPackageName, parcel, 0);
- TextUtils.writeToParcel(mContentDescription, parcel, 0);
- TextUtils.writeToParcel(mBeforeText, parcel, 0);
- parcel.writeParcelable(mParcelableData, flags);
- parcel.writeList(mText);
+ parcel.writeLong(mEventTime);
+ writeAccessibilityRecordToParcel(this, parcel, flags);
+
+ // Write the records.
+ final int recordCount = getRecordCount();
+ parcel.writeInt(recordCount);
+ for (int i = 0; i < recordCount; i++) {
+ AccessibilityRecord record = mRecords.get(i);
+ writeAccessibilityRecordToParcel(record, parcel, flags);
+ }
+ }
+
+ /**
+ * Writes an {@link AccessibilityRecord} to a parcel.
+ *
+ * @param record The record to write.
+ * @param parcel The parcel to which to write.
+ */
+ private void writeAccessibilityRecordToParcel(AccessibilityRecord record, Parcel parcel,
+ int flags) {
+ parcel.writeInt(record.mBooleanProperties);
+ parcel.writeInt(record.mCurrentItemIndex);
+ parcel.writeInt(record.mItemCount);
+ parcel.writeInt(record.mFromIndex);
+ parcel.writeInt(record.mAddedCount);
+ parcel.writeInt(record.mRemovedCount);
+ TextUtils.writeToParcel(record.mClassName, parcel, flags);
+ TextUtils.writeToParcel(record.mContentDescription, parcel, flags);
+ TextUtils.writeToParcel(record.mBeforeText, parcel, flags);
+ parcel.writeParcelable(record.mParcelableData, flags);
+ parcel.writeList(record.mText);
}
+ /**
+ * {@inheritDoc}
+ */
public int describeContents() {
return 0;
}
@@ -701,24 +514,21 @@ public final class AccessibilityEvent implements Parcelable {
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
- builder.append(super.toString());
builder.append("; EventType: " + mEventType);
builder.append("; EventTime: " + mEventTime);
- builder.append("; ClassName: " + mClassName);
builder.append("; PackageName: " + mPackageName);
- builder.append("; Text: " + mText);
- builder.append("; ContentDescription: " + mContentDescription);
- builder.append("; ItemCount: " + mItemCount);
- builder.append("; CurrentItemIndex: " + mCurrentItemIndex);
- builder.append("; IsEnabled: " + isEnabled());
- builder.append("; IsPassword: " + isPassword());
- builder.append("; IsChecked: " + isChecked());
- builder.append("; IsFullScreen: " + isFullScreen());
- builder.append("; BeforeText: " + mBeforeText);
- builder.append("; FromIndex: " + mFromIndex);
- builder.append("; AddedCount: " + mAddedCount);
- builder.append("; RemovedCount: " + mRemovedCount);
- builder.append("; ParcelableData: " + mParcelableData);
+ builder.append(" \n{\n");
+ builder.append(super.toString());
+ builder.append("\n");
+ for (int i = 0; i < mRecords.size(); i++) {
+ AccessibilityRecord record = mRecords.get(i);
+ builder.append(" Record ");
+ builder.append(i);
+ builder.append(":");
+ builder.append(record.toString());
+ builder.append("\n");
+ }
+ builder.append("}\n");
return builder.toString();
}
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 22cb0d4..dd77193 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -16,6 +16,8 @@
package android.view.accessibility;
+import android.accessibilityservice.AccessibilityService;
+import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
import android.content.pm.ServiceInfo;
import android.os.Binder;
@@ -44,6 +46,8 @@ import java.util.List;
* @see android.content.Context#getSystemService
*/
public final class AccessibilityManager {
+ private static final boolean DEBUG = false;
+
private static final String LOG_TAG = "AccessibilityManager";
static final Object sInstanceSync = new Object();
@@ -164,7 +168,7 @@ public final class AccessibilityManager {
long identityToken = Binder.clearCallingIdentity();
doRecycle = mService.sendAccessibilityEvent(event);
Binder.restoreCallingIdentity(identityToken);
- if (false) {
+ if (DEBUG) {
Log.i(LOG_TAG, event + " sent");
}
} catch (RemoteException re) {
@@ -185,7 +189,7 @@ public final class AccessibilityManager {
}
try {
mService.interrupt();
- if (false) {
+ if (DEBUG) {
Log.i(LOG_TAG, "Requested interrupt from all services");
}
} catch (RemoteException re) {
@@ -202,7 +206,33 @@ public final class AccessibilityManager {
List<ServiceInfo> services = null;
try {
services = mService.getAccessibilityServiceList();
- if (false) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
+ }
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
+ }
+ return Collections.unmodifiableList(services);
+ }
+
+ /**
+ * Returns the {@link ServiceInfo}s of the enabled accessibility services
+ * for a given feedback type.
+ *
+ * @param feedbackType The type of feedback.
+ * @return An unmodifiable list with {@link ServiceInfo}s.
+ *
+ * @see AccessibilityServiceInfo#FEEDBACK_AUDIBLE
+ * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC
+ * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN
+ * @see AccessibilityServiceInfo#FEEDBACK_VISUAL
+ * @see AccessibilityServiceInfo#FEEDBACK_GENERIC
+ */
+ public List<ServiceInfo> getEnabledAccessibilityServiceList(int feedbackType) {
+ List<ServiceInfo> services = null;
+ try {
+ services = mService.getEnabledAccessibilityServiceList(feedbackType);
+ if (DEBUG) {
Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
}
} catch (RemoteException re) {
diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java
new file mode 100644
index 0000000..e095f43
--- /dev/null
+++ b/core/java/android/view/accessibility/AccessibilityRecord.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.accessibility;
+
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a record in an accessibility event. This class encapsulates
+ * the information for a {@link android.view.View}. Note that not all properties
+ * are applicable to all view types. For detailed information please refer to
+ * {@link AccessibilityEvent}.
+ *
+ * @see AccessibilityEvent
+ */
+public class AccessibilityRecord {
+
+ private static final int INVALID_POSITION = -1;
+
+ private static final int PROPERTY_CHECKED = 0x00000001;
+ private static final int PROPERTY_ENABLED = 0x00000002;
+ private static final int PROPERTY_PASSWORD = 0x00000004;
+ private static final int PROPERTY_FULL_SCREEN = 0x00000080;
+
+ private static final int MAX_POOL_SIZE = 10;
+ private static final Object mPoolLock = new Object();
+ private static AccessibilityRecord sPool;
+ private static int sPoolSize;
+
+ private AccessibilityRecord mNext;
+ private boolean mIsInPool;
+
+ protected int mBooleanProperties;
+ protected int mCurrentItemIndex;
+ protected int mItemCount;
+ protected int mFromIndex;
+ protected int mAddedCount;
+ protected int mRemovedCount;
+
+ protected CharSequence mClassName;
+ protected CharSequence mContentDescription;
+ protected CharSequence mBeforeText;
+ protected Parcelable mParcelableData;
+
+ protected final List<CharSequence> mText = new ArrayList<CharSequence>();
+
+ /*
+ * Hide constructor.
+ */
+ protected AccessibilityRecord() {
+
+ }
+
+ /**
+ * Gets if the source is checked.
+ *
+ * @return True if the view is checked, false otherwise.
+ */
+ public boolean isChecked() {
+ return getBooleanProperty(PROPERTY_CHECKED);
+ }
+
+ /**
+ * Sets if the source is checked.
+ *
+ * @param isChecked True if the view is checked, false otherwise.
+ */
+ public void setChecked(boolean isChecked) {
+ setBooleanProperty(PROPERTY_CHECKED, isChecked);
+ }
+
+ /**
+ * Gets if the source is enabled.
+ *
+ * @return True if the view is enabled, false otherwise.
+ */
+ public boolean isEnabled() {
+ return getBooleanProperty(PROPERTY_ENABLED);
+ }
+
+ /**
+ * Sets if the source is enabled.
+ *
+ * @param isEnabled True if the view is enabled, false otherwise.
+ */
+ public void setEnabled(boolean isEnabled) {
+ setBooleanProperty(PROPERTY_ENABLED, isEnabled);
+ }
+
+ /**
+ * Gets if the source is a password field.
+ *
+ * @return True if the view is a password field, false otherwise.
+ */
+ public boolean isPassword() {
+ return getBooleanProperty(PROPERTY_PASSWORD);
+ }
+
+ /**
+ * Sets if the source is a password field.
+ *
+ * @param isPassword True if the view is a password field, false otherwise.
+ */
+ public void setPassword(boolean isPassword) {
+ setBooleanProperty(PROPERTY_PASSWORD, isPassword);
+ }
+
+ /**
+ * Sets if the source is taking the entire screen.
+ *
+ * @param isFullScreen True if the source is full screen, false otherwise.
+ */
+ public void setFullScreen(boolean isFullScreen) {
+ setBooleanProperty(PROPERTY_FULL_SCREEN, isFullScreen);
+ }
+
+ /**
+ * Gets if the source is taking the entire screen.
+ *
+ * @return True if the source is full screen, false otherwise.
+ */
+ public boolean isFullScreen() {
+ return getBooleanProperty(PROPERTY_FULL_SCREEN);
+ }
+
+ /**
+ * Gets the number of items that can be visited.
+ *
+ * @return The number of items.
+ */
+ public int getItemCount() {
+ return mItemCount;
+ }
+
+ /**
+ * Sets the number of items that can be visited.
+ *
+ * @param itemCount The number of items.
+ */
+ public void setItemCount(int itemCount) {
+ mItemCount = itemCount;
+ }
+
+ /**
+ * Gets the index of the source in the list of items the can be visited.
+ *
+ * @return The current item index.
+ */
+ public int getCurrentItemIndex() {
+ return mCurrentItemIndex;
+ }
+
+ /**
+ * Sets the index of the source in the list of items that can be visited.
+ *
+ * @param currentItemIndex The current item index.
+ */
+ public void setCurrentItemIndex(int currentItemIndex) {
+ mCurrentItemIndex = currentItemIndex;
+ }
+
+ /**
+ * Gets the index of the first character of the changed sequence.
+ *
+ * @return The index of the first character.
+ */
+ public int getFromIndex() {
+ return mFromIndex;
+ }
+
+ /**
+ * Sets the index of the first character of the changed sequence.
+ *
+ * @param fromIndex The index of the first character.
+ */
+ public void setFromIndex(int fromIndex) {
+ mFromIndex = fromIndex;
+ }
+
+ /**
+ * Gets the number of added characters.
+ *
+ * @return The number of added characters.
+ */
+ public int getAddedCount() {
+ return mAddedCount;
+ }
+
+ /**
+ * Sets the number of added characters.
+ *
+ * @param addedCount The number of added characters.
+ */
+ public void setAddedCount(int addedCount) {
+ mAddedCount = addedCount;
+ }
+
+ /**
+ * Gets the number of removed characters.
+ *
+ * @return The number of removed characters.
+ */
+ public int getRemovedCount() {
+ return mRemovedCount;
+ }
+
+ /**
+ * Sets the number of removed characters.
+ *
+ * @param removedCount The number of removed characters.
+ */
+ public void setRemovedCount(int removedCount) {
+ mRemovedCount = removedCount;
+ }
+
+ /**
+ * Gets the class name of the source.
+ *
+ * @return The class name.
+ */
+ public CharSequence getClassName() {
+ return mClassName;
+ }
+
+ /**
+ * Sets the class name of the source.
+ *
+ * @param className The lass name.
+ */
+ public void setClassName(CharSequence className) {
+ mClassName = className;
+ }
+
+ /**
+ * Gets the text of the event. The index in the list represents the priority
+ * of the text. Specifically, the lower the index the higher the priority.
+ *
+ * @return The text.
+ */
+ public List<CharSequence> getText() {
+ return mText;
+ }
+
+ /**
+ * Sets the text before a change.
+ *
+ * @return The text before the change.
+ */
+ public CharSequence getBeforeText() {
+ return mBeforeText;
+ }
+
+ /**
+ * Sets the text before a change.
+ *
+ * @param beforeText The text before the change.
+ */
+ public void setBeforeText(CharSequence beforeText) {
+ mBeforeText = beforeText;
+ }
+
+ /**
+ * Gets the description of the source.
+ *
+ * @return The description.
+ */
+ public CharSequence getContentDescription() {
+ return mContentDescription;
+ }
+
+ /**
+ * Sets the description of the source.
+ *
+ * @param contentDescription The description.
+ */
+ public void setContentDescription(CharSequence contentDescription) {
+ mContentDescription = contentDescription;
+ }
+
+ /**
+ * Gets the {@link Parcelable} data.
+ *
+ * @return The parcelable data.
+ */
+ public Parcelable getParcelableData() {
+ return mParcelableData;
+ }
+
+ /**
+ * Sets the {@link Parcelable} data of the event.
+ *
+ * @param parcelableData The parcelable data.
+ */
+ public void setParcelableData(Parcelable parcelableData) {
+ mParcelableData = parcelableData;
+ }
+
+ /**
+ * Gets the value of a boolean property.
+ *
+ * @param property The property.
+ * @return The value.
+ */
+ public boolean getBooleanProperty(int property) {
+ return (mBooleanProperties & property) == property;
+ }
+
+ /**
+ * Sets a boolean property.
+ *
+ * @param property The property.
+ * @param value The value.
+ */
+ private void setBooleanProperty(int property, boolean value) {
+ if (value) {
+ mBooleanProperties |= property;
+ } else {
+ mBooleanProperties &= ~property;
+ }
+ }
+
+ /**
+ * Returns a cached instance if such is available or a new one is
+ * instantiated.
+ *
+ * @return An instance.
+ */
+ protected static AccessibilityRecord obtain() {
+ synchronized (mPoolLock) {
+ if (sPool != null) {
+ AccessibilityRecord record = sPool;
+ sPool = sPool.mNext;
+ sPoolSize--;
+ record.mNext = null;
+ record.mIsInPool = false;
+ return record;
+ }
+ return new AccessibilityRecord();
+ }
+ }
+
+ /**
+ * Return an instance back to be reused.
+ * <p>
+ * <b>Note: You must not touch the object after calling this function.</b>
+ */
+ public void recycle() {
+ if (mIsInPool) {
+ return;
+ }
+ clear();
+ synchronized (mPoolLock) {
+ if (sPoolSize <= MAX_POOL_SIZE) {
+ mNext = sPool;
+ sPool = this;
+ mIsInPool = true;
+ sPoolSize++;
+ }
+ }
+ }
+
+ /**
+ * Clears the state of this instance.
+ */
+ protected void clear() {
+ mBooleanProperties = 0;
+ mCurrentItemIndex = INVALID_POSITION;
+ mItemCount = 0;
+ mFromIndex = 0;
+ mAddedCount = 0;
+ mRemovedCount = 0;
+ mClassName = null;
+ mContentDescription = null;
+ mBeforeText = null;
+ mParcelableData = null;
+ mText.clear();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(" [ ClassName: " + mClassName);
+ builder.append("; Text: " + mText);
+ builder.append("; ContentDescription: " + mContentDescription);
+ builder.append("; ItemCount: " + mItemCount);
+ builder.append("; CurrentItemIndex: " + mCurrentItemIndex);
+ builder.append("; IsEnabled: " + getBooleanProperty(PROPERTY_ENABLED));
+ builder.append("; IsPassword: " + getBooleanProperty(PROPERTY_PASSWORD));
+ builder.append("; IsChecked: " + getBooleanProperty(PROPERTY_CHECKED));
+ builder.append("; IsFullScreen: " + getBooleanProperty(PROPERTY_FULL_SCREEN));
+ builder.append("; BeforeText: " + mBeforeText);
+ builder.append("; FromIndex: " + mFromIndex);
+ builder.append("; AddedCount: " + mAddedCount);
+ builder.append("; RemovedCount: " + mRemovedCount);
+ builder.append("; ParcelableData: " + mParcelableData);
+ builder.append(" ]");
+ return builder.toString();
+ }
+}
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 7633569..aaaae32 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -35,5 +35,7 @@ interface IAccessibilityManager {
List<ServiceInfo> getAccessibilityServiceList();
+ List<ServiceInfo> getEnabledAccessibilityServiceList(int feedbackType);
+
void interrupt();
}
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 6cb5c35..d63d421 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -55,6 +55,7 @@ import android.view.ViewConfiguration;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@@ -2556,6 +2557,17 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
}
@Override
+ public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
+ // Add a record for ourselves as well.
+ AccessibilityEvent record = AccessibilityEvent.obtain();
+ // Set the class since it is not populated in #dispatchPopulateAccessibilityEvent
+ record.setClassName(getClass().getName());
+ child.dispatchPopulateAccessibilityEvent(record);
+ event.appendRecord(record);
+ return true;
+ }
+
+ @Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return false;
}
diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java
index f16efbd..060f1a9 100644
--- a/core/java/android/widget/AdapterView.java
+++ b/core/java/android/widget/AdapterView.java
@@ -876,7 +876,6 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup {
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
- boolean populated = false;
// This is an exceptional case which occurs when a window gets the
// focus and sends a focus event via its focused child to announce
// current focus/selection. AdapterView fires selection but not focus
@@ -885,22 +884,27 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup {
event.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED);
}
- // we send selection events only from AdapterView to avoid
- // generation of such event for each child
+ // We first get a chance to populate the event.
+ onPopulateAccessibilityEvent(event);
+
+ // We send selection events only from AdapterView to avoid
+ // generation of such event for each child.
View selectedView = getSelectedView();
if (selectedView != null) {
- populated = selectedView.dispatchPopulateAccessibilityEvent(event);
+ return selectedView.dispatchPopulateAccessibilityEvent(event);
}
- if (!populated) {
- if (selectedView != null) {
- event.setEnabled(selectedView.isEnabled());
- }
- event.setItemCount(getCount());
- event.setCurrentItemIndex(getSelectedItemPosition());
- }
+ return false;
+ }
- return populated;
+ @Override
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ View selectedView = getSelectedView();
+ if (selectedView != null) {
+ event.setEnabled(selectedView.isEnabled());
+ }
+ event.setItemCount(getCount());
+ event.setCurrentItemIndex(getSelectedItemPosition());
}
@Override
diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java
index bf63607..bd595a5 100644
--- a/core/java/android/widget/CheckedTextView.java
+++ b/core/java/android/widget/CheckedTextView.java
@@ -199,11 +199,8 @@ public class CheckedTextView extends TextView implements Checkable {
}
@Override
- public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
- boolean populated = super.dispatchPopulateAccessibilityEvent(event);
- if (!populated) {
- event.setChecked(mChecked);
- }
- return populated;
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEvent(event);
+ event.setChecked(mChecked);
}
}
diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java
index 0df45cc..f050d41 100644
--- a/core/java/android/widget/CompoundButton.java
+++ b/core/java/android/widget/CompoundButton.java
@@ -208,22 +208,18 @@ public abstract class CompoundButton extends Button implements Checkable {
}
@Override
- public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
- boolean populated = super.dispatchPopulateAccessibilityEvent(event);
-
- if (!populated) {
- int resourceId = 0;
- if (mChecked) {
- resourceId = R.string.accessibility_compound_button_selected;
- } else {
- resourceId = R.string.accessibility_compound_button_unselected;
- }
- String state = getResources().getString(resourceId);
- event.getText().add(state);
- event.setChecked(mChecked);
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEvent(event);
+
+ int resourceId = 0;
+ if (mChecked) {
+ resourceId = R.string.accessibility_compound_button_selected;
+ } else {
+ resourceId = R.string.accessibility_compound_button_unselected;
}
-
- return populated;
+ String state = getResources().getString(resourceId);
+ event.getText().add(state);
+ event.setChecked(mChecked);
}
@Override
diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java
index 7210e21..30fb927 100644
--- a/core/java/android/widget/DatePicker.java
+++ b/core/java/android/widget/DatePicker.java
@@ -353,13 +353,14 @@ public class DatePicker extends FrameLayout {
}
@Override
- public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
- int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEvent(event);
+
+ final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY
| DateUtils.FORMAT_SHOW_YEAR;
String selectedDateUtterance = DateUtils.formatDateTime(mContext,
mCurrentDate.getTimeInMillis(), flags);
event.getText().add(selectedDateUtterance);
- return true;
}
/**
diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java
index d76a956..5618dbe 100644
--- a/core/java/android/widget/ListView.java
+++ b/core/java/android/widget/ListView.java
@@ -1998,36 +1998,32 @@ public class ListView extends AbsListView {
}
@Override
- public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
- boolean populated = super.dispatchPopulateAccessibilityEvent(event);
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEvent(event);
// If the item count is less than 15 then subtract disabled items from the count and
// position. Otherwise ignore disabled items.
- if (!populated) {
- int itemCount = 0;
- int currentItemIndex = getSelectedItemPosition();
-
- ListAdapter adapter = getAdapter();
- if (adapter != null) {
- final int count = adapter.getCount();
- if (count < 15) {
- for (int i = 0; i < count; i++) {
- if (adapter.isEnabled(i)) {
- itemCount++;
- } else if (i <= currentItemIndex) {
- currentItemIndex--;
- }
+ int itemCount = 0;
+ int currentItemIndex = getSelectedItemPosition();
+
+ ListAdapter adapter = getAdapter();
+ if (adapter != null) {
+ final int count = adapter.getCount();
+ if (count < 15) {
+ for (int i = 0; i < count; i++) {
+ if (adapter.isEnabled(i)) {
+ itemCount++;
+ } else if (i <= currentItemIndex) {
+ currentItemIndex--;
}
- } else {
- itemCount = count;
}
+ } else {
+ itemCount = count;
}
-
- event.setItemCount(itemCount);
- event.setCurrentItemIndex(currentItemIndex);
}
- return populated;
+ event.setItemCount(itemCount);
+ event.setCurrentItemIndex(currentItemIndex);
}
/**
diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java
index 8db34d9..96d41a0 100644
--- a/core/java/android/widget/ProgressBar.java
+++ b/core/java/android/widget/ProgressBar.java
@@ -1027,12 +1027,10 @@ public class ProgressBar extends View {
}
@Override
- public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
- if (!super.dispatchPopulateAccessibilityEvent(event)) {
- event.setItemCount(mMax);
- event.setCurrentItemIndex(mProgress);
- }
- return true;
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEvent(event);
+ event.setItemCount(mMax);
+ event.setCurrentItemIndex(mProgress);
}
/**
diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java
index 6f76dd0..31ec785 100644
--- a/core/java/android/widget/TabWidget.java
+++ b/core/java/android/widget/TabWidget.java
@@ -427,12 +427,19 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener {
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
- event.setItemCount(getTabCount());
- event.setCurrentItemIndex(mSelectedTab);
+ onPopulateAccessibilityEvent(event);
+ // Dispatch only to the selected tab.
if (mSelectedTab != -1) {
- getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event);
+ return getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event);
}
- return true;
+ return false;
+ }
+
+ @Override
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEvent(event);
+ event.setItemCount(getTabCount());
+ event.setCurrentItemIndex(mSelectedTab);
}
/**
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 537709d..51b3bb4 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -7896,9 +7896,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
@Override
- public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
if (!isShown()) {
- return false;
+ return;
}
final boolean isPassword = hasPasswordTransformationMethod();
@@ -7914,7 +7914,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
} else {
event.setPassword(isPassword);
}
- return false;
}
void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java
index 029d690..423e735 100644
--- a/core/java/android/widget/TimePicker.java
+++ b/core/java/android/widget/TimePicker.java
@@ -409,7 +409,9 @@ public class TimePicker extends FrameLayout {
}
@Override
- public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEvent(event);
+
int flags = DateUtils.FORMAT_SHOW_TIME;
if (mIs24HourView) {
flags |= DateUtils.FORMAT_24HOUR;
@@ -421,7 +423,6 @@ public class TimePicker extends FrameLayout {
String selectedDateUtterance = DateUtils.formatDateTime(mContext,
mTempCalendar.getTimeInMillis(), flags);
event.getText().add(selectedDateUtterance);
- return true;
}
private void updateHourControl() {
diff --git a/services/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/java/com/android/server/accessibility/AccessibilityInputFilter.java
index ced8feb..8ba0a0b 100644
--- a/services/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -20,8 +20,8 @@ import com.android.server.wm.InputFilter;
import android.content.Context;
import android.util.Slog;
+import android.view.InputDevice;
import android.view.InputEvent;
-import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.WindowManagerPolicy;
@@ -32,10 +32,35 @@ import android.view.WindowManagerPolicy;
*/
public class AccessibilityInputFilter extends InputFilter {
private static final String TAG = "AccessibilityInputFilter";
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
private final Context mContext;
+ /**
+ * This is an interface for explorers that take a {@link MotionEvent}
+ * stream and perform touch exploration of the screen content.
+ */
+ public interface Explorer {
+ /**
+ * Handles a {@link MotionEvent}.
+ *
+ * @param event The event to handle.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ public void onMotionEvent(MotionEvent event, int policyFlags);
+
+ /**
+ * Requests that the explorer clears its internal state.
+ *
+ * @param event The last received event.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ public void clear(MotionEvent event, int policyFlags);
+ }
+
+ private TouchExplorer mTouchExplorer;
+ private int mTouchscreenSourceDeviceId;
+
public AccessibilityInputFilter(Context context) {
super(context.getMainLooper());
mContext = context;
@@ -60,27 +85,27 @@ public class AccessibilityInputFilter extends InputFilter {
@Override
public void onInputEvent(InputEvent event, int policyFlags) {
if (DEBUG) {
- Slog.d(TAG, "Accessibility input filter received input event: "
- + event + ", policyFlags=0x" + Integer.toHexString(policyFlags));
+ Slog.d(TAG, "Received event: " + event + ", policyFlags=0x"
+ + Integer.toHexString(policyFlags));
}
-
- // To prove that this is working as intended, we will silently transform
- // Q key presses into non-repeating Z's as part of this stub implementation.
- // TODO: Replace with the real thing.
- if (event instanceof KeyEvent) {
- final KeyEvent keyEvent = (KeyEvent)event;
- if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_Q) {
- if (keyEvent.getRepeatCount() == 0) {
- sendInputEvent(new KeyEvent(keyEvent.getDownTime(), keyEvent.getEventTime(),
- keyEvent.getAction(), KeyEvent.KEYCODE_Z, keyEvent.getRepeatCount(),
- keyEvent.getMetaState(), keyEvent.getDeviceId(), keyEvent.getScanCode(),
- keyEvent.getFlags(), keyEvent.getSource()),
- policyFlags | WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT);
+ if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
+ MotionEvent motionEvent = (MotionEvent) event;
+ int deviceId = event.getDeviceId();
+ if (mTouchscreenSourceDeviceId != deviceId) {
+ mTouchscreenSourceDeviceId = deviceId;
+ if (mTouchExplorer != null) {
+ mTouchExplorer.clear(motionEvent, policyFlags);
+ } else {
+ mTouchExplorer = new TouchExplorer(this, mContext);
}
- return;
}
+ if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) != 0) {
+ mTouchExplorer.onMotionEvent(motionEvent, policyFlags);
+ } else {
+ mTouchExplorer.clear(motionEvent, policyFlags);
+ }
+ } else {
+ super.onInputEvent(event, policyFlags);
}
-
- super.onInputEvent(event, policyFlags);
}
}
diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
index 7a483aa..1ad8047 100644
--- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -56,6 +56,7 @@ import android.view.accessibility.IAccessibilityManagerClient;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -74,6 +75,8 @@ import java.util.Set;
public class AccessibilityManagerService extends IAccessibilityManager.Stub
implements HandlerCaller.Callback {
+ private static final boolean DEBUG = false;
+
private static final String LOG_TAG = "AccessibilityManagerService";
private static int sIdCounter = 0;
@@ -102,6 +105,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private final SimpleStringSplitter mStringColonSplitter = new SimpleStringSplitter(':');
+ private final SparseArray<List<ServiceInfo>> mFeedbackTypeToEnabledServicesMap =
+ new SparseArray<List<ServiceInfo>>();
+
private PackageManager mPackageManager;
private int mHandledFeedbackTypes = 0;
@@ -211,7 +217,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
manageServicesLocked();
- updateInputFilterLocked();
}
return;
@@ -252,7 +257,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
unbindAllServicesLocked();
}
updateClientsLocked();
- updateInputFilterLocked();
}
}
});
@@ -300,6 +304,16 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
}
+ public List<ServiceInfo> getEnabledAccessibilityServiceList(int feedbackType) {
+ synchronized (mLock) {
+ List<ServiceInfo> enabledServices = mFeedbackTypeToEnabledServicesMap.get(feedbackType);
+ if (enabledServices == null) {
+ return Collections.emptyList();
+ }
+ return enabledServices;
+ }
+ }
+
public void interrupt() {
synchronized (mLock) {
for (int i = 0, count = mServices.size(); i < count; i++) {
@@ -339,6 +353,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
service.mNotificationTimeout = info.notificationTimeout;
service.mIsDefault = (info.flags & AccessibilityServiceInfo.DEFAULT) != 0;
+
+ updateStateOnEnabledService(service);
}
return;
default:
@@ -449,7 +465,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
try {
listener.onAccessibilityEvent(event);
- if (false) {
+ if (DEBUG) {
Slog.i(LOG_TAG, "Event " + event + " sent to " + listener);
}
} catch (RemoteException re) {
@@ -469,10 +485,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
* @return True if the service was removed, false otherwise.
*/
private boolean removeDeadServiceLocked(Service service) {
- if (false) {
+ if (DEBUG) {
Slog.i(LOG_TAG, "Dead service " + service.mService + " removed");
}
mHandler.removeMessages(service.mId);
+ updateStateOnDisabledService(service);
return mServices.remove(service);
}
@@ -593,7 +610,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
if (isEnabled) {
if (enabledServices.contains(componentName)) {
if (service == null) {
- service = new Service(componentName);
+ service = new Service(componentName, intalledService);
}
service.bind();
} else if (!enabledServices.contains(componentName)) {
@@ -644,6 +661,47 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
/**
+ * Updates the set of enabled services for a given feedback type and
+ * if more than one of them provides spoken feedback enables touch
+ * exploration.
+ *
+ * @param service An enable service.
+ */
+ private void updateStateOnEnabledService(Service service) {
+ int feedbackType = service.mFeedbackType;
+ List<ServiceInfo> enabledServices = mFeedbackTypeToEnabledServicesMap.get(feedbackType);
+ if (enabledServices == null) {
+ enabledServices = new ArrayList<ServiceInfo>();
+ mFeedbackTypeToEnabledServicesMap.put(feedbackType, enabledServices);
+ }
+ enabledServices.add(service.mServiceInfo);
+
+ // We enable touch exploration if at least one
+ // enabled service provides spoken feedback.
+ if (enabledServices.size() > 0
+ && service.mFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) {
+ updateClientsLocked();
+ updateInputFilterLocked();
+ }
+ }
+
+ private void updateStateOnDisabledService(Service service) {
+ List<ServiceInfo> enabledServices =
+ mFeedbackTypeToEnabledServicesMap.get(service.mFeedbackType);
+ if (enabledServices == null) {
+ return;
+ }
+ enabledServices.remove(service.mServiceInfo);
+ // We disable touch exploration if no
+ // enabled service provides spoken feedback.
+ if (enabledServices.isEmpty()
+ && service.mFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) {
+ updateClientsLocked();
+ updateInputFilterLocked();
+ }
+ }
+
+ /**
* This class represents an accessibility service. It stores all per service
* data required for the service management, provides API for starting/stopping the
* service and is responsible for adding/removing the service in the data structures
@@ -654,6 +712,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
class Service extends IAccessibilityServiceConnection.Stub implements ServiceConnection {
int mId = 0;
+ ServiceInfo mServiceInfo;
+
IBinder mService;
IEventListener mServiceInterface;
@@ -678,9 +738,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
final SparseArray<AccessibilityEvent> mPendingEvents =
new SparseArray<AccessibilityEvent>();
- Service(ComponentName componentName) {
+ Service(ComponentName componentName, ServiceInfo serviceInfo) {
mId = sIdCounter++;
mComponentName = componentName;
+ mServiceInfo = serviceInfo;
mIntent = new Intent().setComponent(mComponentName);
mIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
com.android.internal.R.string.accessibility_binding_label);
@@ -712,6 +773,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
mContext.unbindService(this);
mComponentNameToServiceMap.remove(mComponentName);
mServices.remove(this);
+ updateStateOnDisabledService(this);
return true;
}
return false;
diff --git a/services/java/com/android/server/accessibility/TouchExplorer.java b/services/java/com/android/server/accessibility/TouchExplorer.java
new file mode 100644
index 0000000..4ba6060
--- /dev/null
+++ b/services/java/com/android/server/accessibility/TouchExplorer.java
@@ -0,0 +1,1540 @@
+/*
+ ** Copyright 2011, The Android Open Source Project
+ **
+ ** Licensed under the Apache License, Version 2.0 (the "License");
+ ** you may not use this file except in compliance with the License.
+ ** You may obtain a copy of the License at
+ **
+ ** http://www.apache.org/licenses/LICENSE-2.0
+ **
+ ** Unless required by applicable law or agreed to in writing, software
+ ** distributed under the License is distributed on an "AS IS" BASIS,
+ ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ** See the License for the specific language governing permissions and
+ ** limitations under the License.
+ */
+
+package com.android.server.accessibility;
+
+import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END;
+import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START;
+
+import com.android.server.accessibility.AccessibilityInputFilter.Explorer;
+import com.android.server.wm.InputFilter;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.WindowManagerPolicy;
+import android.view.MotionEvent.PointerCoords;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import java.util.Arrays;
+
+/**
+ * This class is a strategy for performing touch exploration. It
+ * transforms the motion event stream by modifying, adding, replacing,
+ * and consuming certain events. The interaction model is:
+ *
+ * <ol>
+ * <li>1. One finger moving around performs touch exploration.</li>
+ * <li>2. Two close fingers moving in the same direction perform a drag.</li>
+ * <li>3. Multi-finger gestures are delivered to view hierarchy.</li>
+ * <li>4. Pointers that have not moved more than a specified distance after they
+ * went down are considered inactive.</li>
+ * <li>5. Two fingers moving too far from each other or in different directions
+ * are considered a multi-finger gesture.</li>
+ * <li>6. Tapping on the last touch explored location within given time and
+ * distance slop performs a click.</li>
+ * <li>7. Tapping and holding for a while on the last touch explored location within
+ * given time and distance slop performs a long press.</li>
+ * <ol>
+ *
+ * @hide
+ */
+public class TouchExplorer implements Explorer {
+ 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";
+
+ // 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;
+
+ // Human readable symbolic names for the states of the explorer.
+ private static final SparseArray<String> sStateSymbolicNames = new SparseArray<String>();
+ static {
+ SparseArray<String> symbolicNames = sStateSymbolicNames;
+ symbolicNames.append(STATE_TOUCH_EXPLORING, "STATE_TOUCH_EXPLORING");
+ symbolicNames.append(STATE_DRAGGING, "STATE_DRAGING");
+ symbolicNames.append(STATE_DELEGATING, "STATE_DELEGATING");
+ }
+
+ // Invalid pointer ID.
+ private static final int INVALID_POINTER_ID = -1;
+
+ // The coefficient by which to multiply
+ // ViewConfiguration.#getScaledTouchExplorationTapSlop()
+ // to compute #mDraggingDistance.
+ private static final int COEFFICIENT_DRAGGING_DISTANCE = 2;
+
+ // 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 MIN_ANGLE_COS = 0.866025404f; // cos(pi/6)
+
+ // The delay for sending a hover enter event.
+ private static final long DELAY_SEND_HOVER_MOVE = 200;
+
+ // Temporary array for storing pointer IDs.
+ private final int[] mTempPointerIds = new int[MAX_POINTER_COUNT];
+
+ // Temporary array for mapping new to old pointer IDs while filtering inactive pointers.
+ private final int [] mTempNewToOldPointerIndexMap = new int[MAX_POINTER_COUNT];
+
+ // Temporary array for storing PointerCoords
+ private final PointerCoords[] mTempPointerCoords= new PointerCoords[MAX_POINTER_COUNT];
+
+ // The maximal distance between two pointers so they are
+ // considered to be performing a drag operation.
+ private final float mDraggingDistance;
+
+ // The distance from the last touch explored location tapping within
+ // which would perform a click and tapping and holding a long press.
+ private final int mTouchExplorationTapSlop;
+
+ // Context handle for accessing resources.
+ private final Context mContext;
+
+ // The InputFilter this tracker is associated with i.e. the filter
+ // 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;
+
+ // The last event that was received while performing touch exploration.
+ private MotionEvent mLastTouchExploreEvent;
+
+ // The current state of the touch explorer.
+ private int mCurrentState = STATE_TOUCH_EXPLORING;
+
+ // Flag whether a touch exploration gesture is in progress.
+ private boolean mTouchExploreGestureInProgress;
+
+ // The ID of the pointer used for dragging.
+ private int mDraggingPointerId;
+
+ // Handler for performing asynchronous operations.
+ private final Handler mHandler;
+
+ // Command for delayed sending of a hover event.
+ private final SendHoverDelayed mSendHoverDelayed;
+
+ /**
+ * 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) {
+ mInputFilter = inputFilter;
+ mTouchExplorationTapSlop =
+ ViewConfiguration.get(context).getScaledTouchExplorationTapSlop();
+ mDraggingDistance = mTouchExplorationTapSlop * COEFFICIENT_DRAGGING_DISTANCE;
+ mPointerTracker = new PointerTracker(context);
+ mContext = context;
+ mHandler = new Handler(context.getMainLooper());
+ mSendHoverDelayed = new SendHoverDelayed();
+ mAccessibilityManager = AccessibilityManager.getInstance(context);
+
+ // Populate the temporary array with PointerCorrds to be reused.
+ for (int i = 0, count = mTempPointerCoords.length; i < count; i++) {
+ mTempPointerCoords[i] = new PointerCoords();
+ }
+ }
+
+ public void clear(MotionEvent event, int policyFlags) {
+ sendUpForInjectedDownPointers(event, policyFlags);
+ clear();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onMotionEvent(MotionEvent event, int policyFlags) {
+ if (DEBUG) {
+ Slog.d(LOG_TAG_RECEIVED, "Received event: " + event + ", policyFlags=0x"
+ + Integer.toHexString(policyFlags));
+ Slog.d(LOG_TAG_STATE, sStateSymbolicNames.get(mCurrentState));
+ }
+
+ // Keep track of the pointers's state.
+ mPointerTracker.onReceivedMotionEvent(event);
+
+ switch(mCurrentState) {
+ case STATE_TOUCH_EXPLORING: {
+ handleMotionEventStateTouchExploring(event, policyFlags);
+ } break;
+ case STATE_DRAGGING: {
+ handleMotionEventStateDragging(event, policyFlags);
+ } break;
+ case STATE_DELEGATING: {
+ handleMotionEventStateDelegating(event, policyFlags);
+ } break;
+ default: {
+ throw new IllegalStateException("Illegal state: " + mCurrentState);
+ }
+ }
+ }
+
+ /**
+ * Handles a motion event in touch exploring state.
+ *
+ * @param event The event to be handled.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void handleMotionEventStateTouchExploring(MotionEvent event, int policyFlags) {
+ PointerTracker pointerTracker = mPointerTracker;
+ final int activePointerCount = pointerTracker.getActivePointerCount();
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ // Send a hover for every finger down so the user gets feedback
+ // where she is currently touching.
+ mSendHoverDelayed.forceSendAndRemove();
+ mSendHoverDelayed.post(event, MotionEvent.ACTION_HOVER_ENTER, 0, policyFlags,
+ DELAY_SEND_HOVER_MOVE);
+ } break;
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ switch (activePointerCount) {
+ case 0: {
+ throw new IllegalStateException("The must always be one active pointer in"
+ + "touch exploring state!");
+ }
+ case 1: {
+ // Schedule a hover event which will lead to firing an
+ // accessibility event from the hovered view.
+ mSendHoverDelayed.remove();
+ final int pointerId = pointerTracker.getPrimaryActivePointerId();
+ final int pointerIndex = event.findPointerIndex(pointerId);
+ final int lastAction = pointerTracker.getLastInjectedHoverAction();
+ // If a schedules hover enter for another pointer is delivered we send move.
+ final int action = (lastAction == MotionEvent.ACTION_HOVER_ENTER)
+ ? MotionEvent.ACTION_HOVER_MOVE
+ : MotionEvent.ACTION_HOVER_ENTER;
+ mSendHoverDelayed.post(event, action, pointerIndex, policyFlags,
+ DELAY_SEND_HOVER_MOVE);
+
+ if (mLastTouchExploreEvent == null) {
+ break;
+ }
+
+ // If more pointers down on the screen since the last touch
+ // exploration we discard the last cached touch explore event.
+ if (event.getPointerCount() != mLastTouchExploreEvent.getPointerCount()) {
+ mLastTouchExploreEvent = null;
+ }
+ } break;
+ default: {
+ /* do nothing - let the code for ACTION_MOVE decide what to do */
+ } break;
+ }
+ } break;
+ case MotionEvent.ACTION_MOVE: {
+ switch (activePointerCount) {
+ case 0: {
+ /* do nothing - no active pointers so we swallow the event */
+ } break;
+ case 1: {
+ final int pointerId = pointerTracker.getPrimaryActivePointerId();
+ final int pointerIndex = event.findPointerIndex(pointerId);
+
+ // 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)
+ - event.getX(pointerIndex);
+ final float deltaY = pointerTracker.getReceivedPointerDownY(pointerId)
+ - event.getY(pointerIndex);
+ final double moveDelta = Math.hypot(deltaX, deltaY);
+
+ if (moveDelta > mTouchExplorationTapSlop) {
+ mTouchExploreGestureInProgress = true;
+ sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_START);
+ // Make sure the scheduled down/move event is sent.
+ mSendHoverDelayed.forceSendAndRemove();
+ sendHoverEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIndex,
+ policyFlags);
+ }
+ } else {
+ // Touch exploration gesture in progress so send a hover event.
+ sendHoverEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIndex,
+ policyFlags);
+ }
+
+ // Detect long press on the last touch explored position.
+ if (!mTouchExploreGestureInProgress && mLastTouchExploreEvent != null) {
+
+ // If the down was not in the time slop => nothing else to do.
+ final long pointerDownTime =
+ pointerTracker.getReceivedPointerDownTime(pointerId);
+ final long lastExploreTime = mLastTouchExploreEvent.getEventTime();
+ final long deltaTimeExplore = pointerDownTime - lastExploreTime;
+ if (deltaTimeExplore > ACTIVATION_TIME_SLOP) {
+ mLastTouchExploreEvent = null;
+ break;
+ }
+
+ // If the pointer moved more than the tap slop => nothing else to do.
+ final float deltaX = mLastTouchExploreEvent.getX(pointerIndex)
+ - event.getX(pointerIndex);
+ final float deltaY = mLastTouchExploreEvent.getY(pointerIndex)
+ - event.getY(pointerIndex);
+ final float moveDelta = (float) Math.hypot(deltaX, deltaY);
+ if (moveDelta > mTouchExplorationTapSlop) {
+ mLastTouchExploreEvent = null;
+ break;
+ }
+
+ // If down for long enough we get a long press.
+ final long deltaTimeMove = event.getEventTime() - pointerDownTime;
+ if (deltaTimeMove > ViewConfiguration.getLongPressTimeout()) {
+ mCurrentState = STATE_DELEGATING;
+ // Make sure the scheduled hover exit is delivered.
+ mSendHoverDelayed.forceSendAndRemove();
+ sendDownForAllActiveNotInjectedPointers(event, policyFlags);
+ sendMotionEvent(event, policyFlags);
+ mTouchExploreGestureInProgress = false;
+ mLastTouchExploreEvent = null;
+ }
+ }
+ } break;
+ case 2: {
+ // Make sure the scheduled hover enter is delivered.
+ mSendHoverDelayed.forceSendAndRemove();
+ // We want to no longer hover over the location so subsequent
+ // touch at the same spot will generate a hover enter.
+ final int pointerId = pointerTracker.getPrimaryActivePointerId();
+ final int pointerIndex = event.findPointerIndex(pointerId);
+ sendHoverEvent(event, MotionEvent.ACTION_HOVER_EXIT, pointerIndex,
+ policyFlags);
+
+ if (isDraggingGesture(event)) {
+ // Two pointers moving in the same direction within
+ // a given distance perform a drag.
+ mCurrentState = STATE_DRAGGING;
+ if (mTouchExploreGestureInProgress) {
+ sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END);
+ mTouchExploreGestureInProgress = false;
+ }
+ mDraggingPointerId = pointerTracker.getPrimaryActivePointerId();
+ sendDragEvent(event, MotionEvent.ACTION_DOWN, policyFlags);
+ } else {
+ // Two pointers moving arbitrary are delegated to the view hierarchy.
+ mCurrentState = STATE_DELEGATING;
+ if (mTouchExploreGestureInProgress) {
+ sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END);
+ mTouchExploreGestureInProgress = false;
+ }
+ sendDownForAllActiveNotInjectedPointers(event, policyFlags);
+ }
+ } break;
+ default: {
+ // Make sure the scheduled hover enter is delivered.
+ mSendHoverDelayed.forceSendAndRemove();
+ // We want to no longer hover over the location so subsequent
+ // touch at the same spot will generate a hover enter.
+ final int pointerId = pointerTracker.getPrimaryActivePointerId();
+ final int pointerIndex = event.findPointerIndex(pointerId);
+ sendHoverEvent(event, MotionEvent.ACTION_HOVER_EXIT, pointerIndex,
+ policyFlags);
+
+ // More than two pointers are delegated to the view hierarchy.
+ mCurrentState = STATE_DELEGATING;
+ mSendHoverDelayed.remove();
+ if (mTouchExploreGestureInProgress) {
+ sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END);
+ mTouchExploreGestureInProgress = false;
+ }
+ sendDownForAllActiveNotInjectedPointers(event, policyFlags);
+ }
+ }
+ } break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP: {
+ switch (activePointerCount) {
+ case 0: {
+ // If the pointer that went up was not active we have nothing to do.
+ if (!pointerTracker.wasLastReceivedUpPointerActive()) {
+ break;
+ }
+
+ // If touch exploring announce the end of the gesture.
+ if (mTouchExploreGestureInProgress) {
+ sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END);
+ mTouchExploreGestureInProgress = false;
+ }
+
+ // Detect whether to activate i.e. click on the last explored location.
+ if (mLastTouchExploreEvent != null) {
+ final int pointerId = pointerTracker.getLastReceivedUpPointerId();
+
+ // If the down was not in the time slop => nothing else to do.
+ final long eventTime =
+ pointerTracker.getLastReceivedUpPointerDownTime();
+ final long exploreTime = mLastTouchExploreEvent.getEventTime();
+ final long deltaTime = eventTime - exploreTime;
+ if (deltaTime > ACTIVATION_TIME_SLOP) {
+ mSendHoverDelayed.forceSendAndRemove();
+ scheduleHoverExit(event, policyFlags);
+ mLastTouchExploreEvent = MotionEvent.obtain(event);
+ break;
+ }
+
+ // If the pointer moved more than the tap slop => nothing else to do.
+ final int pointerIndex = event.findPointerIndex(pointerId);
+ final float deltaX = pointerTracker.getLastReceivedUpPointerDownX()
+ - event.getX(pointerIndex);
+ final float deltaY = pointerTracker.getLastReceivedUpPointerDownY()
+ - event.getY(pointerIndex);
+ final float deltaMove = (float) Math.hypot(deltaX, deltaY);
+ if (deltaMove > mTouchExplorationTapSlop) {
+ mSendHoverDelayed.forceSendAndRemove();
+ scheduleHoverExit(event, policyFlags);
+ mLastTouchExploreEvent = MotionEvent.obtain(event);
+ break;
+ }
+
+ // All preconditions are met, so click the last explored location.
+ mSendHoverDelayed.forceSendAndRemove();
+ sendActionDownAndUp(mLastTouchExploreEvent, policyFlags);
+ mLastTouchExploreEvent = null;
+ } else {
+ mSendHoverDelayed.forceSendAndRemove();
+ scheduleHoverExit(event, policyFlags);
+ mLastTouchExploreEvent = MotionEvent.obtain(event);
+ }
+ } break;
+ }
+ } break;
+ case MotionEvent.ACTION_CANCEL: {
+ final int lastAction = pointerTracker.getLastInjectedHoverAction();
+ if (lastAction != MotionEvent.ACTION_HOVER_EXIT) {
+ final int pointerId = pointerTracker.getPrimaryActivePointerId();
+ final int pointerIndex = event.findPointerIndex(pointerId);
+ sendHoverEvent(event, MotionEvent.ACTION_HOVER_EXIT, pointerIndex,
+ policyFlags);
+ }
+ clear();
+ } break;
+ }
+ }
+
+ /**
+ * Handles a motion event in dragging state.
+ *
+ * @param event The event to be handled.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void handleMotionEventStateDragging(MotionEvent event, int policyFlags) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ throw new IllegalStateException("Dragging state can be reached only if two "
+ + "pointers are already down");
+ }
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ // We are in dragging state so we have two pointers and another one
+ // goes down => delegate the three pointers to the view hierarchy
+ mCurrentState = STATE_DELEGATING;
+ sendDragEvent(event, MotionEvent.ACTION_UP, policyFlags);
+ sendDownForAllActiveNotInjectedPointers(event, policyFlags);
+ } break;
+ case MotionEvent.ACTION_MOVE: {
+ final int activePointerCount = mPointerTracker.getActivePointerCount();
+ switch (activePointerCount) {
+ case 2: {
+ if (isDraggingGesture(event)) {
+ // If still dragging send a drag event.
+ sendDragEvent(event, MotionEvent.ACTION_MOVE, policyFlags);
+ } else {
+ // The two pointers are moving either in different directions or
+ // no close enough => delegate the gesture to the view hierarchy.
+ mCurrentState = STATE_DELEGATING;
+ // Send an event to the end of the drag gesture.
+ sendDragEvent(event, MotionEvent.ACTION_UP, policyFlags);
+ // Deliver all active pointers to the view hierarchy.
+ sendDownForAllActiveNotInjectedPointers(event, policyFlags);
+ }
+ } break;
+ default: {
+ mCurrentState = STATE_DELEGATING;
+ // Send an event to the end of the drag gesture.
+ sendDragEvent(event, MotionEvent.ACTION_UP, policyFlags);
+ // Deliver all active pointers to the view hierarchy.
+ sendDownForAllActiveNotInjectedPointers(event, policyFlags);
+ }
+ }
+ } break;
+ case MotionEvent.ACTION_POINTER_UP: {
+ mCurrentState = STATE_TOUCH_EXPLORING;
+ // Send an event to the end of the drag gesture.
+ sendDragEvent(event, MotionEvent.ACTION_UP, policyFlags);
+ } break;
+ case MotionEvent.ACTION_CANCEL: {
+ clear();
+ } break;
+ }
+ }
+
+ /**
+ * Handles a motion event in delegating state.
+ *
+ * @param event The event to be handled.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ public void handleMotionEventStateDelegating(MotionEvent event, int policyFlags) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ throw new IllegalStateException("Delegating state can only be reached if "
+ + "there is at least one pointer down!");
+ }
+ case MotionEvent.ACTION_UP: {
+ mCurrentState = STATE_TOUCH_EXPLORING;
+ } break;
+ 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();
+ if (notInjectedCount > 0) {
+ sendDownForAllActiveNotInjectedPointers(event, policyFlags);
+ }
+ } break;
+ case MotionEvent.ACTION_POINTER_UP: {
+ // No active pointers => go to initial state.
+ if (mPointerTracker.getActivePointerCount() == 0) {
+ mCurrentState = STATE_TOUCH_EXPLORING;
+ }
+ } break;
+ case MotionEvent.ACTION_CANCEL: {
+ clear();
+ } break;
+ }
+ // Deliver the event striping out inactive pointers.
+ sendMotionEventStripInactivePointers(event, policyFlags);
+ }
+
+ /**
+ * Schedules a hover up event so subsequent poking on the same location after
+ * the scheduled delay will perform exploration.
+ *
+ * @param prototype The prototype from which to create the injected events.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void scheduleHoverExit(MotionEvent prototype,
+ int policyFlags) {
+ final int pointerId = mPointerTracker.getLastReceivedUpPointerId();
+ final int pointerIndex = prototype.findPointerIndex(pointerId);
+ // We want to no longer hover over the location so subsequent
+ // touch at the same spot will generate a hover enter.
+ mSendHoverDelayed.post(prototype, MotionEvent.ACTION_HOVER_EXIT,
+ pointerIndex, policyFlags, ACTIVATION_TIME_SLOP);
+ }
+
+ /**
+ * 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.
+ *
+ * @param prototype The prototype from which to create the injected events.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendDownForAllActiveNotInjectedPointers(MotionEvent prototype, int policyFlags) {
+ PointerCoords[] pointerCoords = mTempPointerCoords;
+ PointerTracker pointerTracker = mPointerTracker;
+ int[] pointerIds = mTempPointerIds;
+ int pointerDataIndex = 0;
+
+ final int pinterCount = prototype.getPointerCount();
+ for (int i = 0; i < pinterCount; i++) {
+ final int pointerId = prototype.getPointerId(i);
+
+ // Skip inactive pointers.
+ if (!pointerTracker.isActivePointer(pointerId)) {
+ continue;
+ }
+ // Skip already delivered pointers.
+ if (pointerTracker.isInjectedPointerDown(pointerId)) {
+ continue;
+ }
+
+ // Populate and inject an event for the current pointer.
+ pointerIds[pointerDataIndex] = pointerId;
+ prototype.getPointerCoords(i, pointerCoords[pointerDataIndex]);
+
+ final long downTime = pointerTracker.getLastInjectedDownEventTime();
+ final int action = computeInjectionAction(MotionEvent.ACTION_DOWN, pointerDataIndex);
+ final int pointerCount = pointerDataIndex + 1;
+ final long pointerDownTime = SystemClock.uptimeMillis();
+
+ MotionEvent event = MotionEvent.obtain(downTime, pointerDownTime,
+ action, pointerCount, pointerIds, pointerCoords, prototype.getMetaState(),
+ prototype.getXPrecision(), prototype.getYPrecision(), prototype.getDeviceId(),
+ prototype.getEdgeFlags(), prototype.getSource(), prototype.getFlags());
+ sendMotionEvent(event, policyFlags);
+ event.recycle();
+
+ pointerDataIndex++;
+ }
+ }
+
+ /**
+ * Sends up events to the view hierarchy for all active pointers which are
+ * already being delivered i.e. pointers that are injected.
+ *
+ * @param prototype The prototype from which to create the injected events.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendUpForInjectedDownPointers(MotionEvent prototype, int policyFlags) {
+ PointerTracker pointerTracker = mPointerTracker;
+ PointerCoords[] pointerCoords = mTempPointerCoords;
+ int[] pointerIds = mTempPointerIds;
+ int pointerDataIndex = 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)) {
+ continue;
+ }
+
+ // Populate and inject event.
+ pointerIds[pointerDataIndex] = pointerId;
+ prototype.getPointerCoords(i, pointerCoords[pointerDataIndex]);
+
+ final long downTime = pointerTracker.getLastInjectedDownEventTime();
+ final int action = computeInjectionAction(MotionEvent.ACTION_UP, pointerDataIndex);
+ final int newPointerCount = pointerDataIndex + 1;
+ final long eventTime = SystemClock.uptimeMillis();
+
+ MotionEvent event = MotionEvent.obtain(downTime, eventTime, action,
+ newPointerCount, pointerIds, pointerCoords, prototype.getMetaState(),
+ prototype.getXPrecision(), prototype.getYPrecision(), prototype.getDeviceId(),
+ prototype.getEdgeFlags(), prototype.getSource(), prototype.getFlags());
+
+ sendMotionEvent(event, policyFlags);
+ event.recycle();
+
+ pointerDataIndex++;
+ }
+ }
+
+ /**
+ * Sends a motion event by first stripping the inactive pointers.
+ *
+ * @param prototype The prototype from which to create the injected event.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendMotionEventStripInactivePointers(MotionEvent prototype, int policyFlags) {
+ PointerTracker pointerTracker = mPointerTracker;
+
+ // All pointers active therefore we just inject the event as is.
+ if (prototype.getPointerCount() == pointerTracker.getActivePointerCount()) {
+ sendMotionEvent(prototype, 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()) {
+ return;
+ }
+
+ // Filter out inactive pointers from the event and inject it.
+ PointerCoords[] pointerCoords = mTempPointerCoords;
+ int[] pointerIds = mTempPointerIds;
+ int [] newToOldPointerIndexMap = mTempNewToOldPointerIndexMap;
+ int newPointerIndex = 0;
+ int actionIndex = prototype.getActionIndex();
+
+ final int oldPointerCount = prototype.getPointerCount();
+ for (int oldPointerIndex = 0; oldPointerIndex < oldPointerCount; oldPointerIndex++) {
+ final int pointerId = prototype.getPointerId(oldPointerIndex);
+
+ // If the pointer is inactive or the pointer that just went up
+ // was inactive we strip the pointer data from the event.
+ if (!pointerTracker.isActiveOrWasLastActiveUpPointer(pointerId)) {
+ if (oldPointerIndex <= prototype.getActionIndex()) {
+ actionIndex--;
+ }
+ continue;
+ }
+
+ newToOldPointerIndexMap[newPointerIndex] = oldPointerIndex;
+ pointerIds[newPointerIndex] = pointerId;
+ prototype.getPointerCoords(oldPointerIndex, pointerCoords[newPointerIndex]);
+
+ newPointerIndex++;
+ }
+
+ // If we skipped all pointers => nothing to do.
+ if (newPointerIndex == 0) {
+ return;
+ }
+
+ // Populate and inject the event.
+ final long downTime = pointerTracker.getLastInjectedDownEventTime();
+ final int action = computeInjectionAction(prototype.getActionMasked(), actionIndex);
+ final int newPointerCount = newPointerIndex;
+ MotionEvent prunedEvent = MotionEvent.obtain(downTime, prototype.getEventTime(), action,
+ newPointerCount, pointerIds, pointerCoords, prototype.getMetaState(),
+ prototype.getXPrecision(), prototype.getYPrecision(), prototype.getDeviceId(),
+ prototype.getEdgeFlags(), prototype.getSource(),prototype.getFlags());
+
+ // Add the filtered history.
+ final int historySize = prototype.getHistorySize();
+ for (int historyIndex = 0; historyIndex < historySize; historyIndex++) {
+ for (int pointerIndex = 0; pointerIndex < newPointerCount; pointerIndex++) {
+ final int oldPointerIndex = newToOldPointerIndexMap[pointerIndex];
+ prototype.getPointerCoords(oldPointerIndex, pointerCoords[pointerIndex]);
+ }
+ final long historicalTime = prototype.getHistoricalEventTime(historyIndex);
+ prunedEvent.addBatch(historicalTime, pointerCoords, 0);
+ }
+
+ sendMotionEvent(prunedEvent, policyFlags);
+ prunedEvent.recycle();
+ }
+
+ /**
+ * Sends a dragging event from a two pointer event. The two pointers are
+ * merged into one and delivered to the view hierarchy. Through the entire
+ * drag gesture the pointer id delivered to the view hierarchy is the same.
+ *
+ * @param prototype The prototype from which to create the injected event.
+ * @param action The dragging action that is to be injected.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendDragEvent(MotionEvent prototype, int action, int policyFlags) {
+ PointerCoords[] pointerCoords = mTempPointerCoords;
+ int[] pointerIds = mTempPointerIds;
+ final int pointerId = mDraggingPointerId;
+ final int pointerIndex = prototype.findPointerIndex(pointerId);
+
+ // Populate the event with the date of the dragging pointer and inject it.
+ pointerIds[0] = pointerId;
+ prototype.getPointerCoords(pointerIndex, pointerCoords[0]);
+
+ MotionEvent event = MotionEvent.obtain(prototype.getDownTime(),
+ prototype.getEventTime(), action, 1, pointerIds, pointerCoords,
+ prototype.getMetaState(), prototype.getXPrecision(), prototype.getYPrecision(),
+ prototype.getDeviceId(), prototype.getEdgeFlags(), prototype.getSource(),
+ prototype.getFlags());
+
+ sendMotionEvent(event, policyFlags);
+ event.recycle();
+ }
+
+ /**
+ * Sends an up and down events.
+ *
+ * @param prototype The prototype from which to create the injected events.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendActionDownAndUp(MotionEvent prototype, int policyFlags) {
+ PointerCoords[] pointerCoords = mTempPointerCoords;
+ int[] pointerIds = mTempPointerIds;
+ final int pointerId = mPointerTracker.getLastReceivedUpPointerId();
+ final int pointerIndex = prototype.findPointerIndex(pointerId);
+
+ // Send down.
+ pointerIds[0] = pointerId;
+ prototype.getPointerCoords(pointerIndex, pointerCoords[0]);
+
+ final long downTime = SystemClock.uptimeMillis();
+
+ MotionEvent downEvent = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
+ 1, mTempPointerIds, mTempPointerCoords, prototype.getMetaState(),
+ prototype.getXPrecision(), prototype.getYPrecision(), prototype.getDeviceId(),
+ prototype.getEdgeFlags(), prototype.getSource(), prototype.getFlags());
+
+ // Clone the down event before recycling it.
+ MotionEvent upEvent = MotionEvent.obtain(downEvent);
+
+ sendMotionEvent(downEvent, policyFlags);
+ downEvent.recycle();
+
+ // Send up.
+ upEvent.setAction(MotionEvent.ACTION_UP);
+ sendMotionEvent(upEvent, policyFlags);
+ upEvent.recycle();
+ }
+
+ /**
+ * Sends a hover event.
+ *
+ * @param prototype The prototype from which to create the injected event.
+ * @param action The hover action.
+ * @param pointerIndex The action pointer index.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendHoverEvent(MotionEvent prototype, int action, int pointerIndex, int
+ policyFlags) {
+ PointerCoords[] pointerCoords = mTempPointerCoords;
+ int[] pointerIds = mTempPointerIds;
+
+ // Keep only data relevant to a hover event.
+ pointerIds[0] = prototype.getPointerId(pointerIndex);
+ pointerCoords[0].clear();
+ pointerCoords[0].x = prototype.getX(pointerIndex);
+ pointerCoords[0].y = prototype.getY(pointerIndex);
+
+ final long downTime = mPointerTracker.getLastInjectedDownEventTime();
+
+ // Populate and inject a hover event.
+ MotionEvent hoverEvent = MotionEvent.obtain(downTime, prototype.getEventTime(), action,
+ 1, pointerIds, pointerCoords, 0, 0, 0, prototype.getDeviceId(), 0,
+ prototype.getSource(), 0);
+
+ sendMotionEvent(hoverEvent, policyFlags);
+ hoverEvent.recycle();
+ }
+
+ /**
+ * Computes the action for an injected event based on a masked action
+ * and a pointer index.
+ *
+ * @param actionMasked The masked action.
+ * @param pointerIndex The index of the pointer which has changed.
+ * @return The action to be used for injection.
+ */
+ private int computeInjectionAction(int actionMasked, int pointerIndex) {
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ PointerTracker pointerTracker = mPointerTracker;
+ // Compute the action based on how many down pointers are injected.
+ if (pointerTracker.getInjectedPointerDownCount() == 0) {
+ return MotionEvent.ACTION_DOWN;
+ } else {
+ return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT)
+ | MotionEvent.ACTION_POINTER_DOWN;
+ }
+ }
+ case MotionEvent.ACTION_POINTER_UP: {
+ PointerTracker pointerTracker = mPointerTracker;
+ // Compute the action based on how many down pointers are injected.
+ if (pointerTracker.getInjectedPointerDownCount() == 1) {
+ return MotionEvent.ACTION_UP;
+ } else {
+ return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT)
+ | MotionEvent.ACTION_POINTER_UP;
+ }
+ }
+ default:
+ return actionMasked;
+ }
+ }
+
+ /**
+ * Determines whether a two pointer gesture is a dragging one.
+ *
+ * @param event The event with the pointer data.
+ * @return True if the gesture is a dragging one.
+ */
+ private boolean isDraggingGesture(MotionEvent event) {
+ PointerTracker pointerTracker = mPointerTracker;
+ int[] pointerIds = mTempPointerIds;
+ pointerTracker.populateActivePointerIds(pointerIds);
+
+ final int firstPtrIndex = event.findPointerIndex(pointerIds[0]);
+ final int secondPtrIndex = event.findPointerIndex(pointerIds[1]);
+
+ final float firstPtrX = event.getX(firstPtrIndex);
+ final float firstPtrY = event.getY(firstPtrIndex);
+ final float secondPtrX = event.getX(secondPtrIndex);
+ final float secondPtrY = event.getY(secondPtrIndex);
+
+ // Check if the pointers are close enough.
+ final float deltaX = firstPtrX - secondPtrX;
+ final float deltaY = firstPtrY - secondPtrY;
+ final float deltaMove = (float) Math.hypot(deltaX, deltaY);
+ if (deltaMove > mDraggingDistance) {
+ return false;
+ }
+
+ // Check if the pointers are moving in the same direction.
+ final float firstDeltaX =
+ firstPtrX - pointerTracker.getReceivedPointerDownX(firstPtrIndex);
+ final float firstDeltaY =
+ firstPtrY - pointerTracker.getReceivedPointerDownY(firstPtrIndex);
+ final float firstMagnitude =
+ (float) Math.sqrt(firstDeltaX * firstDeltaX + firstDeltaY * firstDeltaY);
+ final float firstXNormalized =
+ (firstMagnitude > 0) ? firstDeltaX / firstMagnitude : firstDeltaX;
+ final float firstYNormalized =
+ (firstMagnitude > 0) ? firstDeltaY / firstMagnitude : firstDeltaY;
+
+ final float secondDeltaX =
+ secondPtrX - pointerTracker.getReceivedPointerDownX(secondPtrIndex);
+ final float secondDeltaY =
+ secondPtrY - pointerTracker.getReceivedPointerDownY(secondPtrIndex);
+ final float secondMagnitude =
+ (float) Math.sqrt(secondDeltaX * secondDeltaX + secondDeltaY * secondDeltaY);
+ final float secondXNormalized =
+ (secondMagnitude > 0) ? secondDeltaX / secondMagnitude : secondDeltaX;
+ final float secondYNormalized =
+ (secondMagnitude > 0) ? secondDeltaY / secondMagnitude : secondDeltaY;
+
+ final float angleCos =
+ firstXNormalized * secondXNormalized + firstYNormalized * secondYNormalized;
+
+ if (angleCos < MIN_ANGLE_COS) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Sends an event announcing the start/end of a touch exploration gesture.
+ *
+ * @param eventType The type of the event to send.
+ */
+ private void sendAccessibilityEvent(int eventType) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ event.setPackageName(mContext.getPackageName());
+ event.setClassName(getClass().getName());
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+
+ /**
+ * Sends a motion event to the input filter for injection.
+ *
+ * @param event The event to send.
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendMotionEvent(MotionEvent event, int policyFlags) {
+ if (DEBUG) {
+ Slog.d(LOG_TAG_INJECTED, "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);
+ }
+
+ /**
+ * Clears the internal state of this explorer.
+ */
+ private void clear() {
+ mSendHoverDelayed.remove();
+ mPointerTracker.clear();
+ mLastTouchExploreEvent = null;
+ mCurrentState = STATE_TOUCH_EXPLORING;
+ mTouchExploreGestureInProgress = false;
+ mDraggingPointerId = INVALID_POINTER_ID;
+ }
+
+ /**
+ * 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.
+ */
+ class PointerTracker {
+ private static final String LOG_TAG = "PointerTracker";
+
+ // The coefficient by which to multiply
+ // ViewConfiguration.#getScaledTouchSlop()
+ // to compute #mThresholdActivePointer.
+ private static final int COEFFICIENT_ACTIVE_POINTER = 2;
+
+ // Pointers that moved less than mThresholdActivePointer
+ // are considered active i.e. are ignored.
+ private final double mThresholdActivePointer;
+
+ // Keep track of where and when a pointer went down.
+ private final float[] mReceivedPointerDownX = new float[MAX_POINTER_COUNT];
+ private final float[] mReceivedPointerDownY = new float[MAX_POINTER_COUNT];
+ private final long[] mReceivedPointerDownTime = new long[MAX_POINTER_COUNT];
+
+ // Which pointers are down.
+ private int mReceivedPointersDown;
+
+ // Which down pointers are active.
+ private int mActivePointers;
+
+ // Primary active pointer which is either the first that went down
+ // or if it goes up the next active that most recently went down.
+ private int mPrimaryActivePointerId;
+
+ // 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 float mLastReceivedUpPointerDownX;
+ private float mLastReveivedUpPointerDownY;
+ 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;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param context Context for looking up resources.
+ */
+ public PointerTracker(Context context) {
+ mThresholdActivePointer =
+ ViewConfiguration.get(context).getScaledTouchSlop() * COEFFICIENT_ACTIVE_POINTER;
+ }
+
+ /**
+ * Clears the internals state.
+ */
+ public void clear() {
+ Arrays.fill(mReceivedPointerDownX, 0);
+ Arrays.fill(mReceivedPointerDownY, 0);
+ Arrays.fill(mReceivedPointerDownTime, 0);
+ mReceivedPointersDown = 0;
+ mActivePointers = 0;
+ mPrimaryActivePointerId = 0;
+ mHasMovingActivePointer = false;
+ mInjectedPointersDown = 0;
+ mLastReceivedUpPointerDownX = 0;
+ mLastReveivedUpPointerDownY = 0;
+ mLastReceivedUpPointerDownTime = 0;
+ mLastReceivedUpPointerId = 0;
+ mLastReceivedUpPointerActive = false;
+ }
+
+ /**
+ * Processes a received {@link MotionEvent} event.
+ *
+ * @param event The event to process.
+ */
+ public void onReceivedMotionEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ // New gesture so restart tracking injected down pointers.
+ mInjectedPointersDown = 0;
+ handleReceivedPointerDown(0, event);
+ } break;
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ handleReceivedPointerDown(event.getActionIndex(), event);
+ } break;
+ case MotionEvent.ACTION_MOVE: {
+ handleReceivedPointerMove(event);
+ } break;
+ case MotionEvent.ACTION_UP: {
+ handleReceivedPointerUp(0, event);
+ } break;
+ case MotionEvent.ACTION_POINTER_UP: {
+ handleReceivedPointerUp(event.getActionIndex(), event);
+ } break;
+ }
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Received pointer: " + toString());
+ }
+ }
+
+ /**
+ * Processes an injected {@link MotionEvent} event.
+ *
+ * @param event The event to process.
+ */
+ public void onInjectedMotionEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ handleInjectedPointerDown(0, event);
+ } break;
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ handleInjectedPointerDown(event.getActionIndex(), event);
+ } break;
+ case MotionEvent.ACTION_UP: {
+ handleInjectedPointerUp(0, 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());
+ }
+ }
+
+ /**
+ * @return The number of received pointers that are down.
+ */
+ public int getReceivedPointerDownCount() {
+ return Integer.bitCount(mReceivedPointersDown);
+ }
+
+ /**
+ * @return The number of down input pointers that are active.
+ */
+ public int getActivePointerCount() {
+ return Integer.bitCount(mActivePointers);
+ }
+
+ /**
+ * Whether an received pointer is down.
+ *
+ * @param pointerId The unique pointer id.
+ * @return True if the pointer is down.
+ */
+ public boolean isReceivedPointerDown(int pointerId) {
+ final int pointerFlag = (1 << pointerId);
+ return (mReceivedPointersDown & pointerFlag) != 0;
+ }
+
+ /**
+ * 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.
+ * @return True if the pointer is active.
+ */
+ public boolean isActivePointer(int pointerId) {
+ final int pointerFlag = (1 << pointerId);
+ return (mActivePointers & pointerFlag) != 0;
+ }
+
+ /**
+ * @param pointerId The unique pointer id.
+ * @return The X coordinate where the pointer went down.
+ */
+ public float getReceivedPointerDownX(int pointerId) {
+ return mReceivedPointerDownX[pointerId];
+ }
+
+ /**
+ * @param pointerId The unique pointer id.
+ * @return The Y coordinate where the pointer went down.
+ */
+ public float getReceivedPointerDownY(int pointerId) {
+ return mReceivedPointerDownY[pointerId];
+ }
+
+ /**
+ * @param pointerId The unique pointer id.
+ * @return The time when the pointer went down.
+ */
+ public long getReceivedPointerDownTime(int pointerId) {
+ return mReceivedPointerDownTime[pointerId];
+ }
+
+ /**
+ * @return The id of the primary pointer.
+ */
+ public int getPrimaryActivePointerId() {
+ if (mPrimaryActivePointerId == INVALID_POINTER_ID) {
+ mPrimaryActivePointerId = findPrimaryActivePointer();
+ }
+ return mPrimaryActivePointerId;
+ }
+
+ /**
+ * @return The X coordinate where the last up received pointer went down.
+ */
+ public float getLastReceivedUpPointerDownX() {
+ return mLastReceivedUpPointerDownX;
+ }
+
+ /**
+ * @return The Y coordinate where the last up received pointer went down.
+ */
+ public float getLastReceivedUpPointerDownY() {
+ return mLastReveivedUpPointerDownY;
+ }
+
+ /**
+ * @return The time when the last up received pointer went down.
+ */
+ public long getLastReceivedUpPointerDownTime() {
+ return mLastReceivedUpPointerDownTime;
+ }
+
+ /**
+ * @return The id of the last received pointer that went up.
+ */
+ public int getLastReceivedUpPointerId() {
+ return mLastReceivedUpPointerId;
+ }
+
+ /**
+ * @return Whether the last received pointer that went up was active.
+ */
+ public boolean wasLastReceivedUpPointerActive() {
+ return mLastReceivedUpPointerActive;
+ }
+
+ /**
+ * @return The time of the last injected down event.
+ */
+ public long getLastInjectedDownEventTime() {
+ return mLastInjectedDownEventTime;
+ }
+
+ /**
+ * @return The action of the last injected hover event.
+ */
+ public int getLastInjectedHoverAction() {
+ return mLastInjectedHoverEventAction;
+ }
+
+ /**
+ * Populates the active pointer IDs to the given array.
+ * <p>
+ * Note: The client is responsible for providing large enough array.
+ *
+ * @param outPointerIds The array to which to write the active pointers.
+ */
+ public void populateActivePointerIds(int[] outPointerIds) {
+ int index = 0;
+ for (int idBits = mActivePointers; idBits != 0; ) {
+ final int id = Integer.numberOfTrailingZeros(idBits);
+ idBits &= ~(1 << id);
+ outPointerIds[index] = id;
+ index++;
+ }
+ }
+
+ /**
+ * @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) {
+ return (isActivePointer(pointerId)
+ || (mLastReceivedUpPointerId == pointerId
+ && mLastReceivedUpPointerActive));
+ }
+
+ /**
+ * Handles a received pointer down event.
+ *
+ * @param pointerIndex The index of the pointer that has changed.
+ * @param event The event to be handled.
+ */
+ private void handleReceivedPointerDown(int pointerIndex, MotionEvent event) {
+ final int pointerId = event.getPointerId(pointerIndex);
+ final int pointerFlag = (1 << pointerId);
+
+ mLastReceivedUpPointerId = 0;
+ mLastReceivedUpPointerDownX = 0;
+ mLastReveivedUpPointerDownY = 0;
+ mLastReceivedUpPointerDownTime = 0;
+ mLastReceivedUpPointerActive = false;
+
+ mReceivedPointersDown |= pointerFlag;
+ mReceivedPointerDownX[pointerId] = event.getX(pointerIndex);
+ mReceivedPointerDownY[pointerId] = event.getY(pointerIndex);
+ mReceivedPointerDownTime[pointerId] = event.getEventTime();
+
+ if (!mHasMovingActivePointer) {
+ // If still no moving active pointers every
+ // down pointer is the only active one.
+ mActivePointers = pointerFlag;
+ mPrimaryActivePointerId = pointerId;
+ } else {
+ // If at least one moving active pointer every
+ // subsequent down pointer is active.
+ mActivePointers |= pointerFlag;
+ }
+ }
+
+ /**
+ * Handles a received pointer move event.
+ *
+ * @param event The event to be handled.
+ */
+ private void handleReceivedPointerMove(MotionEvent event) {
+ detectActivePointers(event);
+ }
+
+ /**
+ * Handles a received pointer up event.
+ *
+ * @param pointerIndex The index of the pointer that has changed.
+ * @param event The event to be handled.
+ */
+ private void handleReceivedPointerUp(int pointerIndex, MotionEvent event) {
+ final int pointerId = event.getPointerId(pointerIndex);
+ final int pointerFlag = (1 << pointerId);
+
+ mLastReceivedUpPointerId = pointerId;
+ mLastReceivedUpPointerDownX = getReceivedPointerDownX(pointerId);
+ mLastReveivedUpPointerDownY = getReceivedPointerDownY(pointerId);
+ mLastReceivedUpPointerDownTime = getReceivedPointerDownTime(pointerId);
+ mLastReceivedUpPointerActive = isActivePointer(pointerId);
+
+ mReceivedPointersDown &= ~pointerFlag;
+ mActivePointers &= ~pointerFlag;
+ mReceivedPointerDownX[pointerId] = 0;
+ mReceivedPointerDownY[pointerId] = 0;
+ mReceivedPointerDownTime[pointerId] = 0;
+
+ if (mActivePointers == 0) {
+ mHasMovingActivePointer = false;
+ }
+ if (mPrimaryActivePointerId == pointerId) {
+ mPrimaryActivePointerId = INVALID_POINTER_ID;
+ }
+ }
+
+ /**
+ * 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;
+ mLastInjectedDownEventTime = event.getEventTime();
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Detects the active pointers in an event.
+ *
+ * @param event The event to examine.
+ */
+ private void detectActivePointers(MotionEvent event) {
+ for (int i = 0, count = event.getPointerCount(); i < count; i++) {
+ final int pointerId = event.getPointerId(i);
+ if (mHasMovingActivePointer) {
+ // If already active => nothing to do.
+ if (isActivePointer(pointerId)) {
+ continue;
+ }
+ }
+ // Active pointers are ones that moved more than a given threshold.
+ final float pointerDeltaMove = computePointerDeltaMove(i, event);
+ if (pointerDeltaMove > mThresholdActivePointer) {
+ final int pointerFlag = (1 << pointerId);
+ mActivePointers |= pointerFlag;
+ mHasMovingActivePointer = true;
+ }
+ }
+ }
+
+ /**
+ * @return The primary active pointer.
+ */
+ private int findPrimaryActivePointer() {
+ int primaryActivePointerId = INVALID_POINTER_ID;
+ long minDownTime = Long.MAX_VALUE;
+ // Find the active pointer that went down first.
+ for (int i = 0, count = mReceivedPointerDownTime.length; i < count; i++) {
+ if (isActivePointer(i)) {
+ final long downPointerTime = mReceivedPointerDownTime[i];
+ if (downPointerTime < minDownTime) {
+ minDownTime = downPointerTime;
+ primaryActivePointerId = i;
+ }
+ }
+ }
+ return primaryActivePointerId;
+ }
+
+ /**
+ * Computes the move for a given action pointer index since the
+ * corresponding pointer went down.
+ *
+ * @param pointerIndex The action pointer index.
+ * @param event The event to examine.
+ * @return The distance the pointer has moved.
+ */
+ private float computePointerDeltaMove(int pointerIndex, MotionEvent event) {
+ final int pointerId = event.getPointerId(pointerIndex);
+ final float deltaX = event.getX(pointerIndex) - mReceivedPointerDownX[pointerId];
+ final float deltaY = event.getY(pointerIndex) - mReceivedPointerDownY[pointerId];
+ return (float) Math.hypot(deltaX, deltaY);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("=========================");
+ builder.append("\nDown pointers #");
+ builder.append(getReceivedPointerDownCount());
+ builder.append(" [ ");
+ for (int i = 0; i < MAX_POINTER_COUNT; i++) {
+ if (isReceivedPointerDown(i)) {
+ builder.append(i);
+ builder.append(" ");
+ }
+ }
+ builder.append("]");
+ builder.append("\nActive pointers #");
+ builder.append(getActivePointerCount());
+ builder.append(" [ ");
+ for (int i = 0; i < MAX_POINTER_COUNT; i++) {
+ if (isActivePointer(i)) {
+ builder.append(i);
+ builder.append(" ");
+ }
+ }
+ builder.append("]");
+ builder.append("\nPrimary active pointer id [ ");
+ builder.append(getPrimaryActivePointerId());
+ builder.append(" ]");
+ builder.append("\n=========================");
+ return builder.toString();
+ }
+ }
+
+ /**
+ * 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 mPointerIndex;
+ private int mPolicyFlags;
+
+ public void post(MotionEvent prototype, int action, int pointerIndex, int policyFlags,
+ long delay) {
+ remove();
+ mEvent = MotionEvent.obtain(prototype);
+ mAction = action;
+ mPointerIndex = pointerIndex;
+ 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;
+ mPointerIndex = -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");
+ }
+ }
+
+ sendHoverEvent(mEvent, mAction, mPointerIndex, mPolicyFlags);
+ clear();
+ }
+ }
+}
diff --git a/services/java/com/android/server/wm/InputFilter.java b/services/java/com/android/server/wm/InputFilter.java
index 7e1ab07..8f0001a 100644
--- a/services/java/com/android/server/wm/InputFilter.java
+++ b/services/java/com/android/server/wm/InputFilter.java
@@ -105,11 +105,13 @@ public abstract class InputFilter {
private final InputEventConsistencyVerifier mInboundInputEventConsistencyVerifier =
InputEventConsistencyVerifier.isInstrumentationEnabled() ?
new InputEventConsistencyVerifier(this,
- InputEventConsistencyVerifier.FLAG_RAW_DEVICE_INPUT) : null;
+ InputEventConsistencyVerifier.FLAG_RAW_DEVICE_INPUT,
+ "InputFilter#InboundInputEventConsistencyVerifier") : null;
private final InputEventConsistencyVerifier mOutboundInputEventConsistencyVerifier =
InputEventConsistencyVerifier.isInstrumentationEnabled() ?
new InputEventConsistencyVerifier(this,
- InputEventConsistencyVerifier.FLAG_RAW_DEVICE_INPUT) : null;
+ InputEventConsistencyVerifier.FLAG_RAW_DEVICE_INPUT,
+ "InputFilter#OutboundInputEventConsistencyVerifier") : null;
/**
* Creates the input filter.