diff options
25 files changed, 2530 insertions, 489 deletions
diff --git a/api/current.txt b/api/current.txt index 0f5c98ac596b..f26852d88125 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 b5ca2c2a8c1c..e14b97558e49 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 fe8af1974deb..2f519f443964 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 739758c8c79f..94eb429dad64 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 08daa280b10a..7b404b45f729 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 d7d4c3f02d0d..655df391ed4f 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 1a6bae710f45..06c33f6eb8f9 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 9af19b8796a9..11c93929b23e 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 22cb0d482bf8..dd7719364a31 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 000000000000..e095f435bfd6 --- /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 7633569d5312..aaaae327cb64 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 6cb5c35339d5..d63d421fb3b5 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 f16efbdeb7be..060f1a9b2bc0 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 bf636079987a..bd595a568fc8 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 0df45cca6a65..f050d41e3c55 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 7210e219b300..30fb927cc3cc 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 d76a956b8c67..5618dbe25e06 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 8db34d91d74f..96d41a0ed0b1 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 6f76dd0d2b75..31ec7853c804 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 537709d1324d..51b3bb4585b4 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 029d69020c78..423e735c787b 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 ced8feb450da..8ba0a0b67ef4 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 7a483aad3757..1ad80470f717 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 000000000000..4ba6060ca47f --- /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 7e1ab079cceb..8f0001aec30b 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. |