diff options
author | Svetoslav Ganov <svetoslavganov@google.com> | 2012-05-16 15:48:55 -0700 |
---|---|---|
committer | Svetoslav Ganov <svetoslavganov@google.com> | 2012-05-21 14:08:57 -0700 |
commit | e15ccb93add99ebb9cd7aec03a04faa37f45b39d (patch) | |
tree | aa80b8d4fc4099c26246772992411701739bc655 /services/java/com/android/server/accessibility/TouchExplorer.java | |
parent | d51ac09fd7db5d810452138749f9291617c0dbf7 (diff) | |
download | frameworks_base-e15ccb93add99ebb9cd7aec03a04faa37f45b39d.zip frameworks_base-e15ccb93add99ebb9cd7aec03a04faa37f45b39d.tar.gz frameworks_base-e15ccb93add99ebb9cd7aec03a04faa37f45b39d.tar.bz2 |
Changing the interaction model of the touch explorer.
1. Now the user have to double tap to activate the last
item. If the last touched window is not active because
it does not take input focus the click on the last
touch explored location. Othewise the click is on the
accessibility focus location.
bug:5932640
Change-Id: Ibb7b97262a7c5f2f94abef429e02790fdc91a8dd
Diffstat (limited to 'services/java/com/android/server/accessibility/TouchExplorer.java')
-rw-r--r-- | services/java/com/android/server/accessibility/TouchExplorer.java | 911 |
1 files changed, 594 insertions, 317 deletions
diff --git a/services/java/com/android/server/accessibility/TouchExplorer.java b/services/java/com/android/server/accessibility/TouchExplorer.java index 39012e6..b0b2b8d 100644 --- a/services/java/com/android/server/accessibility/TouchExplorer.java +++ b/services/java/com/android/server/accessibility/TouchExplorer.java @@ -16,9 +16,6 @@ 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 android.content.Context; import android.gesture.Gesture; import android.gesture.GestureLibraries; @@ -26,17 +23,19 @@ import android.gesture.GestureLibrary; import android.gesture.GesturePoint; import android.gesture.GestureStroke; import android.gesture.Prediction; +import android.graphics.Rect; import android.os.Handler; +import android.os.SystemClock; import android.util.Slog; import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.view.WindowManagerPolicy; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import com.android.server.input.InputFilter; import com.android.internal.R; +import com.android.server.input.InputFilter; import java.util.ArrayList; import java.util.Arrays; @@ -47,17 +46,18 @@ import java.util.Arrays; * 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 + * <li>1. One finger moving slow around performs touch exploration.</li> + * <li>2. One finger moving fast around performs gestures.</li> + * <li>3. Two close fingers moving in the same direction perform a drag.</li> + * <li>4. Multi-finger gestures are delivered to view hierarchy.</li> + * <li>5. 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> + * <li>6. Two fingers moving in different directions are considered a multi-finger gesture.</li> + * <li>7. Double tapping clicks on the on the last touch explored location of it was in + * a window that does not take focus, otherwise the click is within the accessibility + * focused rectangle.</li> + * <li>7. Tapping and holding for a while performs a long press in a similar fashion + * as the click above.</li> * <ol> * * @hide @@ -75,85 +75,116 @@ public class TouchExplorer { private static final int STATE_DELEGATING = 0x00000004; private static final int STATE_GESTURE_DETECTING = 0x00000005; - // The time slop in milliseconds for activating an item after it has - // been touch explored. Tapping on an item within this slop will perform - // a click and tapping and holding down a long press. - private static final long ACTIVATION_TIME_SLOP = 2000; - // The minimum of the cosine between the vectors of two moving // pointers so they can be considered moving in the same direction. private static final float MAX_DRAGGING_ANGLE_COS = 0.525321989f; // cos(pi/4) - // The delay for sending a hover enter event. - private static final long DELAY_SEND_HOVER_ENTER = 200; - // Constant referring to the ids bits of all pointers. private static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF; // This constant captures the current implementation detail that // pointer IDs are between 0 and 31 inclusive (subject to change). // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) - public static final int MAX_POINTER_COUNT = 32; + private static final int MAX_POINTER_COUNT = 32; // Invalid pointer ID. - public static final int INVALID_POINTER_ID = -1; + private static final int INVALID_POINTER_ID = -1; + + // The velocity above which we detect gestures. + private static final int GESTURE_DETECTION_VELOCITY_DIP = 1000; + + // The minimal distance before we take the middle of the distance between + // the two dragging pointers as opposed to use the location of the primary one. + private static final int MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP = 200; // Temporary array for storing pointer IDs. private final int[] mTempPointerIds = new int[MAX_POINTER_COUNT]; - // 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; + // Timeout within which we try to detect a tap. + private final int mTapTimeout; + + // Timeout within which we try to detect a double tap. + private final int mDoubleTapTimeout; + + // Slop between the down and up tap to be a tap. + private final int mTouchSlop; + + // Slop between the first and second tap to be a double tap. + private final int mDoubleTapSlop; // The InputFilter this tracker is associated with i.e. the filter // which delegates event processing to this touch explorer. private final InputFilter mInputFilter; - // 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; + // Command for delayed sending of a hover enter event. + private final SendHoverDelayed mSendHoverEnterDelayed; + + // Command for delayed sending of a hover exit event. + private final SendHoverDelayed mSendHoverExitDelayed; // Command for delayed sending of a long press. private final PerformLongPressDelayed mPerformLongPressDelayed; + // Helper to detect and react to double tap in touch explore mode. + private final DoubleTapDetector mDoubleTapDetector; + + // The scaled minimal distance before we take the middle of the distance between + // the two dragging pointers as opposed to use the location of the primary one. + private final int mScaledMinPointerDistanceToUseMiddleLocation; + + // The scaled velocity above which we detect gestures. + private final int mScaledGestureDetectionVelocity; + + // Helper to track gesture velocity. private VelocityTracker mVelocityTracker; + // Helper class to track received pointers. private final ReceivedPointerTracker mReceivedPointerTracker; + // Helper class to track injected pointers. private final InjectedPointerTracker mInjectedPointerTracker; - private final GestureListener mGestureListener; + // Handle to the accessibility manager service. + private final AccessibilityManagerService mAms; - /** - * Callback for gesture detection. - */ - public interface GestureListener { + // Temporary rectangle to avoid instantiation. + private final Rect mTempRect = new Rect(); - /** - * Called when a given gesture was performed. - * - * @param gestureId The gesture id. - */ - public boolean onGesture(int gestureId); - } + // The X of the previous event. + private float mPreviousX; + + // The Y of the previous event. + private float mPreviousY; + + // Buffer for storing points for gesture detection. + private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); + + // The minimal delta between moves to add a gesture point. + private static final int TOUCH_TOLERANCE = 3; + + // The minimal score for accepting a predicted gesture. + private static final float MIN_PREDICTION_SCORE = 2.0f; + + // The library for gesture detection. + private GestureLibrary mGestureLibrary; + + // The long pressing pointer id if coordinate remapping is needed. + private int mLongPressingPointerId; + + // The long pressing pointer X if coordinate remapping is needed. + private int mLongPressingPointerDeltaX; + + // The long pressing pointer Y if coordinate remapping is needed. + private int mLongPressingPointerDeltaY; /** * Creates a new instance. @@ -162,25 +193,73 @@ public class TouchExplorer { * @param context A context handle for accessing resources. */ public TouchExplorer(InputFilter inputFilter, Context context, - GestureListener gestureListener) { - mGestureListener = gestureListener; + AccessibilityManagerService service) { + mAms = service; mReceivedPointerTracker = new ReceivedPointerTracker(context); mInjectedPointerTracker = new InjectedPointerTracker(); mInputFilter = inputFilter; - mTouchExplorationTapSlop = - ViewConfiguration.get(context).getScaledTouchExploreTapSlop(); + mTapTimeout = ViewConfiguration.getTapTimeout(); + mDoubleTapTimeout = ViewConfiguration.getDoubleTapTimeout(); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); mHandler = new Handler(context.getMainLooper()); - mSendHoverDelayed = new SendHoverDelayed(); mPerformLongPressDelayed = new PerformLongPressDelayed(); - mAccessibilityManager = AccessibilityManager.getInstance(context); mGestureLibrary = GestureLibraries.fromRawResource(context, R.raw.accessibility_gestures); mGestureLibrary.setOrientationStyle(4); mGestureLibrary.load(); + mSendHoverEnterDelayed = new SendHoverDelayed(MotionEvent.ACTION_HOVER_ENTER, true); + mSendHoverExitDelayed = new SendHoverDelayed(MotionEvent.ACTION_HOVER_EXIT, false); + mDoubleTapDetector = new DoubleTapDetector(); + final float density = context.getResources().getDisplayMetrics().density; + mScaledMinPointerDistanceToUseMiddleLocation = + (int) (MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP * density); + mScaledGestureDetectionVelocity = (int) (GESTURE_DETECTION_VELOCITY_DIP * density); + } + + public void clear() { + // If we have not received an event then we are in initial + // state. Therefore, there is not need to clean anything. + MotionEvent event = mReceivedPointerTracker.getLastReceivedEvent(); + if (event != null) { + clear(mReceivedPointerTracker.getLastReceivedEvent(), WindowManagerPolicy.FLAG_TRUSTED); + } } public void clear(MotionEvent event, int policyFlags) { - sendUpForInjectedDownPointers(event, policyFlags); - clear(); + switch (mCurrentState) { + case STATE_TOUCH_EXPLORING: { + // If a touch exploration gesture is in progress send events for its end. + sendExitEventsIfNeeded(policyFlags); + } break; + case STATE_DRAGGING: { + mDraggingPointerId = INVALID_POINTER_ID; + // Send exit to all pointers that we have delivered. + sendUpForInjectedDownPointers(event, policyFlags); + } break; + case STATE_DELEGATING: { + // Send exit to all pointers that we have delivered. + sendUpForInjectedDownPointers(event, policyFlags); + } break; + case STATE_GESTURE_DETECTING: { + // Clear the current stroke. + mStrokeBuffer.clear(); + } break; + } + // Remove all pending callbacks. + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); + // Reset the pointer trackers. + mReceivedPointerTracker.clear(); + mInjectedPointerTracker.clear(); + // Clear the double tap detector + mDoubleTapDetector.clear(); + // Go to initial state. + // Clear the long pressing pointer remap data. + mLongPressingPointerId = -1; + mLongPressingPointerDeltaX = 0; + mLongPressingPointerDeltaY = 0; + mCurrentState = STATE_TOUCH_EXPLORING; } public void onMotionEvent(MotionEvent event, int policyFlags) { @@ -218,7 +297,6 @@ public class TouchExplorer { */ private void handleMotionEventStateTouchExploring(MotionEvent event, int policyFlags) { ReceivedPointerTracker receivedTracker = mReceivedPointerTracker; - InjectedPointerTracker injectedTracker = mInjectedPointerTracker; final int activePointerCount = receivedTracker.getActivePointerCount(); if (mVelocityTracker == null) { @@ -226,8 +304,16 @@ public class TouchExplorer { } mVelocityTracker.addMovement(event); + mDoubleTapDetector.onMotionEvent(event, policyFlags); + switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: + // Pre-feed the motion events to the gesture detector since we + // have a distance slop before getting into gesture detection + // mode and not using the points within this slop significantly + // decreases the quality of gesture recognition. + handleMotionEventGestureDetecting(event, policyFlags); + //$FALL-THROUGH$ case MotionEvent.ACTION_POINTER_DOWN: { switch (activePointerCount) { case 0: { @@ -235,44 +321,31 @@ public class TouchExplorer { + "touch exploring state!"); } case 1: { - mSendHoverDelayed.remove(); - mPerformLongPressDelayed.remove(); - // Send a hover for every finger down so the user gets feedback. - final int pointerId = receivedTracker.getPrimaryActivePointerId(); - final int pointerIdBits = (1 << pointerId); - final int lastAction = injectedTracker.getLastInjectedHoverAction(); - - // Deliver hover enter with a delay to have a change to detect - // whether the user actually starts a scrolling gesture. - if (lastAction == MotionEvent.ACTION_HOVER_EXIT) { - mSendHoverDelayed.post(event, MotionEvent.ACTION_HOVER_ENTER, - pointerIdBits, policyFlags, DELAY_SEND_HOVER_ENTER); - } else { - sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, - policyFlags); - } - - if (mLastTouchExploreEvent == null) { - break; + // If we still have not notified the user for the last + // touch, we figure out what to do. If were waiting + // we resent the delayed callback and wait again. + if (mSendHoverEnterDelayed.isPending()) { + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); } - // 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; - } - - // If the down is in the time slop => schedule a long press. - final long pointerDownTime = - receivedTracker.getReceivedPointerDownTime(pointerId); - final long lastExploreTime = mLastTouchExploreEvent.getEventTime(); - final long deltaTimeExplore = pointerDownTime - lastExploreTime; - if (deltaTimeExplore <= ACTIVATION_TIME_SLOP) { - mPerformLongPressDelayed.post(event, policyFlags, - ViewConfiguration.getLongPressTimeout()); + // If we have the first tap schedule a long press and break + // since we do not want to schedule hover enter because + // the delayed callback will kick in before the long click. + // This would lead to a state transition resulting in long + // pressing the item below the double taped area which is + // not necessary where accessibility focus is. + if (mDoubleTapDetector.firstTapDetected()) { + // We got a tap now post a long press action. + mPerformLongPressDelayed.post(event, policyFlags); break; } + // Deliver hover enter with a delay to have a chance + // to detect what the user is trying to do. + final int pointerId = receivedTracker.getPrimaryActivePointerId(); + final int pointerIdBits = (1 << pointerId); + mSendHoverEnterDelayed.post(event, pointerIdBits, policyFlags); } break; default: { /* do nothing - let the code for ACTION_MOVE decide what to do */ @@ -288,119 +361,130 @@ public class TouchExplorer { /* do nothing - no active pointers so we swallow the event */ } break; case 1: { - // Detect touch exploration gesture start by having one active pointer - // that moved more than a given distance. - if (!mTouchExploreGestureInProgress) { + // We have not started sending events since we try to + // figure out what the user is doing. + if (mSendHoverEnterDelayed.isPending()) { + // Pre-feed the motion events to the gesture detector since we + // have a distance slop before getting into gesture detection + // mode and not using the points within this slop significantly + // decreases the quality of gesture recognition. + handleMotionEventGestureDetecting(event, policyFlags); + final float deltaX = receivedTracker.getReceivedPointerDownX(pointerId) - event.getX(pointerIndex); final float deltaY = receivedTracker.getReceivedPointerDownY(pointerId) - event.getY(pointerIndex); final double moveDelta = Math.hypot(deltaX, deltaY); - - if (moveDelta > mTouchExplorationTapSlop) { - + // The user has moved enough for us to decide. + if (moveDelta > mDoubleTapSlop) { + // Check whether the user is performing a gesture. We + // detect gestures if the pointer is moving above a + // given velocity. mVelocityTracker.computeCurrentVelocity(1000); final float maxAbsVelocity = Math.max( Math.abs(mVelocityTracker.getXVelocity(pointerId)), Math.abs(mVelocityTracker.getYVelocity(pointerId))); - // TODO: Tune the velocity cut off and add a constant. - if (maxAbsVelocity > 1000) { - clear(event, policyFlags); + if (maxAbsVelocity > mScaledGestureDetectionVelocity) { + // We have to perform gesture detection, so + // clear the current state and try to detect. mCurrentState = STATE_GESTURE_DETECTING; - event.setAction(MotionEvent.ACTION_DOWN); - handleMotionEventGestureDetecting(event, policyFlags); - return; - } - - mTouchExploreGestureInProgress = true; - sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_START); - // Make sure the scheduled down/move event is sent. - mSendHoverDelayed.forceSendAndRemove(); - mPerformLongPressDelayed.remove(); - // If we have transitioned to exploring state from another one - // we need to send a hover enter event here. - final int lastAction = injectedTracker.getLastInjectedHoverAction(); - if (lastAction == MotionEvent.ACTION_HOVER_EXIT) { - sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER, + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); + } else { + // We have just decided that the user is touch, + // exploring so start sending events. + mSendHoverEnterDelayed.forceSendAndRemove(); + mSendHoverExitDelayed.remove(); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, policyFlags); } - sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, - policyFlags); + break; } } else { - // Touch exploration gesture in progress so send a hover event. + // The user is wither double tapping or performing long + // press so do not send move events yet. + if (mDoubleTapDetector.firstTapDetected()) { + break; + } + sendEnterEventsIfNeeded(policyFlags); sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, policyFlags); } - - // If the exploring pointer moved enough => cancel the long press. - if (!mTouchExploreGestureInProgress && mLastTouchExploreEvent != null - && mPerformLongPressDelayed.isPenidng()) { - - // If the pointer moved more than the tap slop => cancel long press. - final float deltaX = mLastTouchExploreEvent.getX(pointerIndex) + } break; + case 2: { + // More than one pointer so the user is not touch exploring + // and now we have to decide whether to delegate or drag. + if (mSendHoverEnterDelayed.isPending()) { + // We have not started sending events so cancel + // scheduled sending events. + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); + } else { + // If the user is touch exploring the second pointer may be + // performing a double tap to activate an item without need + // for the user to lift his exploring finger. + final float deltaX = receivedTracker.getReceivedPointerDownX(pointerId) - event.getX(pointerIndex); - final float deltaY = mLastTouchExploreEvent.getY(pointerIndex) + final float deltaY = receivedTracker.getReceivedPointerDownY(pointerId) - event.getY(pointerIndex); - final float moveDelta = (float) Math.hypot(deltaX, deltaY); - if (moveDelta > mTouchExplorationTapSlop) { - mLastTouchExploreEvent = null; - mPerformLongPressDelayed.remove(); + final double moveDelta = Math.hypot(deltaX, deltaY); + if (moveDelta < mDoubleTapSlop) { break; } + // We are sending events so send exit and gesture + // end since we transition to another state. + sendExitEventsIfNeeded(policyFlags); } - } break; - case 2: { - mSendHoverDelayed.remove(); - mPerformLongPressDelayed.remove(); - // We want to no longer hover over the location so subsequent - // touch at the same spot will generate a hover enter. - ensureHoverExitSent(event, pointerIdBits, policyFlags); + + // We know that a new state transition is to happen and the + // new state will not be gesture recognition, so clear the + // stashed gesture strokes. + mStrokeBuffer.clear(); if (isDraggingGesture(event)) { // Two pointers moving in the same direction within // a given distance perform a drag. + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); mCurrentState = STATE_DRAGGING; - if (mTouchExploreGestureInProgress) { - sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END); - mTouchExploreGestureInProgress = false; - } - mLastTouchExploreEvent = null; mDraggingPointerId = pointerId; sendMotionEvent(event, MotionEvent.ACTION_DOWN, pointerIdBits, policyFlags); } else { // Two pointers moving arbitrary are delegated to the view hierarchy. mCurrentState = STATE_DELEGATING; - mSendHoverDelayed.remove(); - if (mTouchExploreGestureInProgress) { - sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END); - mTouchExploreGestureInProgress = false; - } - mLastTouchExploreEvent = null; sendDownForAllActiveNotInjectedPointers(event, policyFlags); } } break; default: { - mSendHoverDelayed.remove(); - mPerformLongPressDelayed.remove(); - // We want to no longer hover over the location so subsequent - // touch at the same spot will generate a hover enter. - ensureHoverExitSent(event, pointerIdBits, policyFlags); + // More than one pointer so the user is not touch exploring + // and now we have to decide whether to delegate or drag. + if (mSendHoverEnterDelayed.isPending()) { + // We have not started sending events so cancel + // scheduled sending events. + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); + } else { + // We are sending events so send exit and gesture + // end since we transition to another state. + sendExitEventsIfNeeded(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; - } - mLastTouchExploreEvent = null; sendDownForAllActiveNotInjectedPointers(event, policyFlags); } } } break; case MotionEvent.ACTION_UP: + // We know that we do not need the pre-fed gesture points are not + // needed anymore since the last pointer just went up. + mStrokeBuffer.clear(); + //$FALL-THROUGH$ case MotionEvent.ACTION_POINTER_UP: { final int pointerId = receivedTracker.getLastReceivedUpPointerId(); final int pointerIdBits = (1 << pointerId); @@ -413,59 +497,12 @@ public class TouchExplorer { mPerformLongPressDelayed.remove(); - // If touch exploring announce the end of the gesture. - // Also do not click on the last explored location. - if (mTouchExploreGestureInProgress) { - mTouchExploreGestureInProgress = false; - mSendHoverDelayed.forceSendAndRemove(); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - mLastTouchExploreEvent = MotionEvent.obtain(event); - sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END); - break; - } - - // Detect whether to activate i.e. click on the last explored location. - if (mLastTouchExploreEvent != null) { - // If the down was not in the time slop => nothing else to do. - final long eventTime = - receivedTracker.getLastReceivedUpPointerDownTime(); - final long exploreTime = mLastTouchExploreEvent.getEventTime(); - final long deltaTime = eventTime - exploreTime; - if (deltaTime > ACTIVATION_TIME_SLOP) { - mSendHoverDelayed.forceSendAndRemove(); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - mLastTouchExploreEvent = MotionEvent.obtain(event); - break; - } - - // If a tap is farther than the tap slop => nothing to do. - final int pointerIndex = event.findPointerIndex(pointerId); - final float deltaX = mLastTouchExploreEvent.getX(pointerIndex) - - event.getX(pointerIndex); - final float deltaY = mLastTouchExploreEvent.getY(pointerIndex) - - event.getY(pointerIndex); - final float deltaMove = (float) Math.hypot(deltaX, deltaY); - if (deltaMove > mTouchExplorationTapSlop) { - mSendHoverDelayed.forceSendAndRemove(); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - mLastTouchExploreEvent = MotionEvent.obtain(event); - break; - } - - // This is a tap so do not send hover events since - // this events will result in firing the corresponding - // accessibility events confusing the user about what - // is actually clicked. - mSendHoverDelayed.remove(); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - - // All preconditions are met, so click the last explored location. - sendActionDownAndUp(mLastTouchExploreEvent, policyFlags); - mLastTouchExploreEvent = null; + // If we have not delivered the enter schedule exit. + if (mSendHoverEnterDelayed.isPending()) { + mSendHoverExitDelayed.post(event, pointerIdBits, policyFlags); } else { - mSendHoverDelayed.forceSendAndRemove(); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - mLastTouchExploreEvent = MotionEvent.obtain(event); + // The user is touch exploring so we send events for end. + sendExitEventsIfNeeded(policyFlags); } } break; } @@ -475,16 +512,7 @@ public class TouchExplorer { } } break; case MotionEvent.ACTION_CANCEL: { - mSendHoverDelayed.remove(); - mPerformLongPressDelayed.remove(); - final int pointerId = receivedTracker.getPrimaryActivePointerId(); - final int pointerIdBits = (1 << pointerId); - ensureHoverExitSent(event, pointerIdBits, policyFlags); - clear(); - if (mVelocityTracker != null) { - mVelocityTracker.clear(); - mVelocityTracker = null; - } + clear(event, policyFlags); } break; } } @@ -517,6 +545,28 @@ public class TouchExplorer { } break; case 2: { if (isDraggingGesture(event)) { + // If the dragging pointer are closer that a given distance we + // use the location of the primary one. Otherwise, we take the + // middle between the pointers. + int[] pointerIds = mTempPointerIds; + mReceivedPointerTracker.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); + + final float deltaX = firstPtrX - secondPtrX; + final float deltaY = firstPtrY - secondPtrY; + final double distance = Math.hypot(deltaX, deltaY); + + if (distance > mScaledMinPointerDistanceToUseMiddleLocation) { + event.setLocation(deltaX / 2, deltaY / 2); + } + // If still dragging send a drag event. sendMotionEvent(event, MotionEvent.ACTION_MOVE, pointerIdBits, policyFlags); @@ -557,7 +607,7 @@ public class TouchExplorer { mCurrentState = STATE_TOUCH_EXPLORING; } break; case MotionEvent.ACTION_CANCEL: { - clear(); + clear(event, policyFlags); } break; } } @@ -574,9 +624,6 @@ public class TouchExplorer { 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 @@ -587,30 +634,24 @@ public class TouchExplorer { sendDownForAllActiveNotInjectedPointers(prototype, policyFlags); } } break; + case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: { + mLongPressingPointerId = -1; + mLongPressingPointerDeltaX = 0; + mLongPressingPointerDeltaY = 0; // No active pointers => go to initial state. if (mReceivedPointerTracker.getActivePointerCount() == 0) { mCurrentState = STATE_TOUCH_EXPLORING; } } break; case MotionEvent.ACTION_CANCEL: { - clear(); + clear(event, policyFlags); } break; } // Deliver the event striping out inactive pointers. sendMotionEventStripInactivePointers(event, policyFlags); } - private float mPreviousX; - private float mPreviousY; - - private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); - - private static final int TOUCH_TOLERANCE = 3; - private static final float MIN_PREDICTION_SCORE = 2.0f; - - private GestureLibrary mGestureLibrary; - private void handleMotionEventGestureDetecting(MotionEvent event, int policyFlags) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: { @@ -631,8 +672,7 @@ public class TouchExplorer { mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); } } break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: { + case MotionEvent.ACTION_UP: { float x = event.getX(); float y = event.getY(); mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); @@ -650,7 +690,7 @@ public class TouchExplorer { } try { final int gestureId = Integer.parseInt(bestPrediction.name); - mGestureListener.onGesture(gestureId); + mAms.onGesture(gestureId); } catch (NumberFormatException nfe) { Slog.w(LOG_TAG, "Non numeric gesture id:" + bestPrediction.name); } @@ -661,8 +701,7 @@ public class TouchExplorer { mCurrentState = STATE_TOUCH_EXPLORING; } break; case MotionEvent.ACTION_CANCEL: { - mStrokeBuffer.clear(); - mCurrentState = STATE_TOUCH_EXPLORING; + clear(event, policyFlags); } break; } } @@ -706,17 +745,32 @@ public class TouchExplorer { } /** - * Ensures that hover exit has been sent. + * Sends the exit events if needed. Such events are hover exit and touch explore + * gesture end. * - * @param prototype The prototype from which to create the injected events. - * @param pointerIdBits The bits of the pointers to send. * @param policyFlags The policy flags associated with the event. */ - private void ensureHoverExitSent(MotionEvent prototype, int pointerIdBits, int policyFlags) { - final int lastAction = mInjectedPointerTracker.getLastInjectedHoverAction(); - if (lastAction != MotionEvent.ACTION_HOVER_EXIT) { - sendMotionEvent(prototype, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits, - policyFlags); + private void sendExitEventsIfNeeded(int policyFlags) { + MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent(); + if (event != null && event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) { + final int pointerIdBits = event.getPointerIdBits(); + mAms.touchExplorationGestureEnded(); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits, policyFlags); + } + } + + /** + * Sends the enter events if needed. Such events are hover enter and touch explore + * gesture start. + * + * @param policyFlags The policy flags associated with the event. + */ + private void sendEnterEventsIfNeeded(int policyFlags) { + MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent(); + if (event != null && event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) { + final int pointerIdBits = event.getPointerIdBits(); + mAms.touchExplorationGestureStarted(); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER, pointerIdBits, policyFlags); } } @@ -826,6 +880,36 @@ public class TouchExplorer { event.setDownTime(mInjectedPointerTracker.getLastInjectedDownEventTime()); } + // If the user is long pressing but the long pressing pointer + // was not exactly over the accessibility focused item we need + // to remap the location of that pointer so the user does not + // have to explicitly touch explore something to be able to + // long press it, or even worse to avoid the user long pressing + // on the wrong item since click and long press behave differently. + if (mLongPressingPointerId >= 0) { + final int remappedIndex = event.findPointerIndex(mLongPressingPointerId); + final int pointerCount = event.getPointerCount(); + PointerProperties[] props = PointerProperties.createArray(pointerCount); + PointerCoords[] coords = PointerCoords.createArray(pointerCount); + for (int i = 0; i < pointerCount; i++) { + event.getPointerProperties(i, props[i]); + event.getPointerCoords(i, coords[i]); + if (i == remappedIndex) { + coords[i].x -= mLongPressingPointerDeltaX; + coords[i].y -= mLongPressingPointerDeltaY; + } + } + MotionEvent remapped = MotionEvent.obtain(event.getDownTime(), + event.getEventTime(), event.getAction(), event.getPointerCount(), + props, coords, event.getMetaState(), event.getButtonState(), + 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(), + event.getSource(), event.getFlags()); + if (event != prototype) { + event.recycle(); + } + event = remapped; + } + if (DEBUG) { Slog.d(LOG_TAG, "Injecting event: " + event + ", policyFlags=0x" + Integer.toHexString(policyFlags)); @@ -878,6 +962,172 @@ public class TouchExplorer { } } + private class DoubleTapDetector { + private MotionEvent mDownEvent; + private MotionEvent mFirstTapEvent; + + public void onMotionEvent(MotionEvent event, int policyFlags) { + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + if (mFirstTapEvent != null && !isSamePointerContext(mFirstTapEvent, event)) { + clear(); + } + mDownEvent = MotionEvent.obtain(event); + } break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: { + if (mDownEvent == null) { + return; + } + if (!isSamePointerContext(mDownEvent, event)) { + clear(); + return; + } + if (isTap(mDownEvent, event)) { + if (mFirstTapEvent == null || isTimedOut(mFirstTapEvent, event, + mDoubleTapTimeout)) { + mFirstTapEvent = MotionEvent.obtain(event); + mDownEvent.recycle(); + mDownEvent = null; + return; + } + if (isDoubleTap(mFirstTapEvent, event)) { + onDoubleTap(event, policyFlags); + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + mDownEvent.recycle(); + mDownEvent = null; + return; + } + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } else { + if (mFirstTapEvent != null) { + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } + } + mDownEvent.recycle(); + mDownEvent = null; + } break; + } + } + + public void onDoubleTap(MotionEvent secondTapUp, int policyFlags) { + // This should never be called when more than two pointers are down. + if (secondTapUp.getPointerCount() > 2) { + return; + } + + // Remove pending event deliveries. + mSendHoverEnterDelayed.remove(); + mSendHoverExitDelayed.remove(); + mPerformLongPressDelayed.remove(); + + // This is a tap so do not send hover events since + // this events will result in firing the corresponding + // accessibility events confusing the user about what + // is actually clicked. + sendExitEventsIfNeeded(policyFlags); + + // If the last touched explored location is not within the focused + // window we will click at that exact spot, otherwise we find the + // accessibility focus and if the tap is within its bounds we click + // there, otherwise we pick the middle of the focus rectangle. + MotionEvent lastEvent = mInjectedPointerTracker.getLastInjectedHoverEvent(); + if (lastEvent == null) { + return; + } + + final int exploreLocationX = (int) lastEvent.getX(lastEvent.getActionIndex()); + final int exploreLocationY = (int) lastEvent.getY(lastEvent.getActionIndex()); + + Rect bounds = mTempRect; + boolean useLastHoverLocation = false; + + final int pointerId = secondTapUp.getPointerId(secondTapUp.getActionIndex()); + final int pointerIndex = secondTapUp.findPointerIndex(pointerId); + if (mAms.getAccessibilityFocusBounds(exploreLocationX, exploreLocationY, bounds)) { + // If the user's last touch explored location is not + // within the accessibility focus bounds we use the center + // of the accessibility focused rectangle. + if (!bounds.contains((int) secondTapUp.getX(pointerIndex), + (int) secondTapUp.getY(pointerIndex))) { + useLastHoverLocation = true; + } + } + + // Do the click. + PointerProperties[] properties = new PointerProperties[1]; + properties[0] = new PointerProperties(); + secondTapUp.getPointerProperties(pointerIndex, properties[0]); + PointerCoords[] coords = new PointerCoords[1]; + coords[0] = new PointerCoords(); + coords[0].x = (useLastHoverLocation) ? bounds.centerX() : exploreLocationX; + coords[0].y = (useLastHoverLocation) ? bounds.centerY() : exploreLocationY; + MotionEvent event = MotionEvent.obtain(secondTapUp.getDownTime(), + secondTapUp.getEventTime(), MotionEvent.ACTION_DOWN, 1, properties, + coords, 0, 0, 1.0f, 1.0f, secondTapUp.getDeviceId(), 0, + secondTapUp.getSource(), secondTapUp.getFlags()); + sendActionDownAndUp(event, policyFlags); + event.recycle(); + } + + public void clear() { + if (mDownEvent != null) { + mDownEvent.recycle(); + mDownEvent = null; + } + if (mFirstTapEvent != null) { + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } + } + + public boolean isTap(MotionEvent down, MotionEvent up) { + return eventsWithinTimeoutAndDistance(down, up, mTapTimeout, mTouchSlop); + } + + private boolean isDoubleTap(MotionEvent firstUp, MotionEvent secondUp) { + return eventsWithinTimeoutAndDistance(firstUp, secondUp, mDoubleTapTimeout, + mDoubleTapSlop); + } + + private boolean eventsWithinTimeoutAndDistance(MotionEvent first, MotionEvent second, + int timeout, int distance) { + if (isTimedOut(first, second, timeout)) { + return false; + } + final int downPtrIndex = first.getActionIndex(); + final int upPtrIndex = second.getActionIndex(); + final float deltaX = second.getX(upPtrIndex) - first.getX(downPtrIndex); + final float deltaY = second.getY(upPtrIndex) - first.getY(downPtrIndex); + final double deltaMove = Math.hypot(deltaX, deltaY); + if (deltaMove >= distance) { + return false; + } + return true; + } + + private boolean isTimedOut(MotionEvent firstUp, MotionEvent secondUp, int timeout) { + final long deltaTime = secondUp.getEventTime() - firstUp.getEventTime(); + return (deltaTime >= timeout); + } + + private boolean isSamePointerContext(MotionEvent first, MotionEvent second) { + return (first.getPointerIdBits() == second.getPointerIdBits() + && first.getPointerId(first.getActionIndex()) + == second.getPointerId(second.getActionIndex())); + } + + public boolean firstTapDetected() { + return mFirstTapEvent != null + && SystemClock.uptimeMillis() - mFirstTapEvent.getEventTime() < mDoubleTapTimeout; + } + } + /** * Determines whether a two pointer gesture is a dragging one. * @@ -940,30 +1190,6 @@ public class TouchExplorer { 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); - mAccessibilityManager.sendAccessibilityEvent(event); - } - - /** - * Clears the internal state of this explorer. - */ - public void clear() { - mSendHoverDelayed.remove(); - mPerformLongPressDelayed.remove(); - mReceivedPointerTracker.clear(); - mInjectedPointerTracker.clear(); - mLastTouchExploreEvent = null; - mCurrentState = STATE_TOUCH_EXPLORING; - mTouchExploreGestureInProgress = false; - mDraggingPointerId = INVALID_POINTER_ID; - } - /** * Gets the symbolic name of a state. * @@ -1002,10 +1228,10 @@ public class TouchExplorer { private MotionEvent mEvent; private int mPolicyFlags; - public void post(MotionEvent prototype, int policyFlags, long delay) { + public void post(MotionEvent prototype, int policyFlags) { mEvent = MotionEvent.obtain(prototype); mPolicyFlags = policyFlags; - mHandler.postDelayed(this, delay); + mHandler.postDelayed(this, ViewConfiguration.getLongPressTimeout()); } public void remove() { @@ -1021,16 +1247,29 @@ public class TouchExplorer { @Override public void run() { - mCurrentState = STATE_DELEGATING; - // Make sure the scheduled hover exit is delivered. - mSendHoverDelayed.remove(); + final int pointerIndex = mEvent.getActionIndex(); + final int eventX = (int) mEvent.getX(pointerIndex); + final int eventY = (int) mEvent.getY(pointerIndex); + Rect bounds = mTempRect; + if (mAms.getAccessibilityFocusBounds(eventX, eventY, bounds) + && !bounds.contains(eventX, eventY)) { + mLongPressingPointerId = mEvent.getPointerId(pointerIndex); + mLongPressingPointerDeltaX = eventX - bounds.centerX(); + mLongPressingPointerDeltaY = eventY - bounds.centerY(); + } else { + mLongPressingPointerId = -1; + mLongPressingPointerDeltaX = 0; + mLongPressingPointerDeltaY = 0; + } + // We are sending events so send exit and gesture + // end since we transition to another state. final int pointerId = mReceivedPointerTracker.getPrimaryActivePointerId(); final int pointerIdBits = (1 << pointerId); - ensureHoverExitSent(mEvent, pointerIdBits, mPolicyFlags); + mAms.touchExplorationGestureEnded(); + sendExitEventsIfNeeded(mPolicyFlags); + mCurrentState = STATE_DELEGATING; sendDownForAllActiveNotInjectedPointers(mEvent, mPolicyFlags); - mTouchExploreGestureInProgress = false; - mLastTouchExploreEvent = null; clear(); } @@ -1047,20 +1286,41 @@ public class TouchExplorer { /** * Class for delayed sending of hover events. */ - private final class SendHoverDelayed implements Runnable { - private MotionEvent mEvent; - private int mAction; + class SendHoverDelayed implements Runnable { + private final String LOG_TAG_SEND_HOVER_DELAYED = SendHoverDelayed.class.getName(); + + private final int mHoverAction; + private final boolean mGestureStarted; + + private MotionEvent mPrototype; private int mPointerIdBits; private int mPolicyFlags; - public void post(MotionEvent prototype, int action, int pointerIdBits, int policyFlags, - long delay) { + public SendHoverDelayed(int hoverAction, boolean gestureStarted) { + mHoverAction = hoverAction; + mGestureStarted = gestureStarted; + } + + public void post(MotionEvent prototype, int pointerIdBits, int policyFlags) { remove(); - mEvent = MotionEvent.obtain(prototype); - mAction = action; + mPrototype = MotionEvent.obtain(prototype); mPointerIdBits = pointerIdBits; mPolicyFlags = policyFlags; - mHandler.postDelayed(this, delay); + mHandler.postDelayed(this, mTapTimeout); + } + + public float getX() { + if (isPending()) { + return mPrototype.getX(); + } + return 0; + } + + public float getY() { + if (isPending()) { + return mPrototype.getY(); + } + return 0; } public void remove() { @@ -1068,23 +1328,22 @@ public class TouchExplorer { clear(); } - private boolean isPenidng() { - return (mEvent != null); + private boolean isPending() { + return (mPrototype != null); } private void clear() { - if (!isPenidng()) { + if (!isPending()) { return; } - mEvent.recycle(); - mEvent = null; - mAction = 0; + mPrototype.recycle(); + mPrototype = null; mPointerIdBits = -1; mPolicyFlags = 0; } public void forceSendAndRemove() { - if (isPenidng()) { + if (isPending()) { run(); remove(); } @@ -1092,16 +1351,17 @@ public class TouchExplorer { 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"); - } + Slog.d(LOG_TAG_SEND_HOVER_DELAYED, "Injecting motion event: " + + MotionEvent.actionToString(mHoverAction)); + Slog.d(LOG_TAG_SEND_HOVER_DELAYED, mGestureStarted ? + "touchExplorationGestureStarted" : "touchExplorationGestureEnded"); } - - sendMotionEvent(mEvent, mAction, mPointerIdBits, mPolicyFlags); + if (mGestureStarted) { + mAms.touchExplorationGestureStarted(); + } else { + mAms.touchExplorationGestureEnded(); + } + sendMotionEvent(mPrototype, mHoverAction, mPointerIdBits, mPolicyFlags); clear(); } } @@ -1120,8 +1380,8 @@ public class TouchExplorer { // 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; + // The last injected hover event. + private MotionEvent mLastInjectedHoverEvent; /** * Processes an injected {@link MotionEvent} event. @@ -1150,11 +1410,14 @@ public class TouchExplorer { case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_HOVER_EXIT: { - mLastInjectedHoverEventAction = event.getActionMasked(); + if (mLastInjectedHoverEvent != null) { + mLastInjectedHoverEvent.recycle(); + } + mLastInjectedHoverEvent = MotionEvent.obtain(event); } break; } if (DEBUG) { - Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer: " + toString()); + Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer:\n" + toString()); } } @@ -1198,10 +1461,10 @@ public class TouchExplorer { } /** - * @return The action of the last injected hover event. + * @return The the last injected hover event. */ - public int getLastInjectedHoverAction() { - return mLastInjectedHoverEventAction; + public MotionEvent getLastInjectedHoverEvent() { + return mLastInjectedHoverEvent; } @Override @@ -1260,6 +1523,8 @@ public class TouchExplorer { private float mLastReceivedUpPointerDownX; private float mLastReceivedUpPointerDownY; + private MotionEvent mLastReceivedEvent; + /** * Creates a new instance. * @@ -1294,6 +1559,11 @@ public class TouchExplorer { * @param event The event to process. */ public void onMotionEvent(MotionEvent event) { + if (mLastReceivedEvent != null) { + mLastReceivedEvent.recycle(); + } + mLastReceivedEvent = MotionEvent.obtain(event); + final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: { @@ -1318,6 +1588,13 @@ public class TouchExplorer { } /** + * @return The last received event. + */ + public MotionEvent getLastReceivedEvent() { + return mLastReceivedEvent; + } + + /** * @return The number of received pointers that are down. */ public int getReceivedPointerDownCount() { |