DO NOT MERGE - New implementation for ScaleGestureDetector

This solves the problems around active pointer tracking when the
caller may skip events in the MotionEvent stream and replaces the
old implementation with a much simpler algorithm.

Change-Id: I97c0bfad03a6190e403e843d382e05ff2257b66f
diff --git a/core/java/android/view/ScaleGestureDetector.java b/core/java/android/view/ScaleGestureDetector.java
index 73f94bc..dc36088 100644
--- a/core/java/android/view/ScaleGestureDetector.java
+++ b/core/java/android/view/ScaleGestureDetector.java
@@ -17,14 +17,13 @@
 package android.view;
 
 import android.content.Context;
-import android.util.DisplayMetrics;
 import android.util.FloatMath;
-import android.util.Log;
 
 /**
- * Detects transformation gestures involving more than one pointer ("multitouch")
- * using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener}
- * callback will notify users when a particular gesture event has occurred.
+ * Detects scaling transformation gestures using the supplied {@link MotionEvent}s.
+ * The {@link OnScaleGestureListener} callback will notify users when a particular
+ * gesture event has occurred.
+ *
  * This class should only be used with {@link MotionEvent}s reported via touch.
  *
  * To use this class:
@@ -121,43 +120,21 @@
         }
     }
 
-    /**
-     * This value is the threshold ratio between our previous combined pressure
-     * and the current combined pressure. We will only fire an onScale event if
-     * the computed ratio between the current and previous event pressures is
-     * greater than this value. When pressure decreases rapidly between events
-     * the position values can often be imprecise, as it usually indicates
-     * that the user is in the process of lifting a pointer off of the device.
-     * Its value was tuned experimentally.
-     */
-    private static final float PRESSURE_THRESHOLD = 0.67f;
-
     private final Context mContext;
     private final OnScaleGestureListener mListener;
-    private boolean mGestureInProgress;
-
-    private MotionEvent mPrevEvent;
-    private MotionEvent mCurrEvent;
 
     private float mFocusX;
     private float mFocusY;
-    private float mPrevFingerDiffX;
-    private float mPrevFingerDiffY;
-    private float mCurrFingerDiffX;
-    private float mCurrFingerDiffY;
-    private float mCurrLen;
-    private float mPrevLen;
-    private float mScaleFactor;
-    private float mCurrPressure;
-    private float mPrevPressure;
-    private long mTimeDelta;
 
-    private boolean mInvalidGesture;
-
-    // Pointer IDs currently responsible for the two fingers controlling the gesture
-    private int mActiveId0;
-    private int mActiveId1;
-    private boolean mActive0MostRecent;
+    private float mCurrSpan;
+    private float mPrevSpan;
+    private float mCurrSpanX;
+    private float mCurrSpanY;
+    private float mPrevSpanX;
+    private float mPrevSpanY;
+    private long mCurrTime;
+    private long mPrevTime;
+    private boolean mInProgress;
 
     /**
      * Consistency verifier for debugging purposes.
@@ -171,6 +148,18 @@
         mListener = listener;
     }
 
+    /**
+     * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
+     * when appropriate.
+     *
+     * <p>Applications should pass a complete and consistent event stream to this method.
+     * A complete and consistent event stream involves all MotionEvents from the initial
+     * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
+     *
+     * @param event The event to process
+     * @return true if the event was processed and the detector wants to receive the
+     *         rest of the MotionEvents in this event stream.
+     */
     public boolean onTouchEvent(MotionEvent event) {
         if (mInputEventConsistencyVerifier != null) {
             mInputEventConsistencyVerifier.onTouchEvent(event, 0);
@@ -178,265 +167,110 @@
 
         final int action = event.getActionMasked();
 
-        if (action == MotionEvent.ACTION_DOWN) {
-            reset(); // Start fresh
-        }
-
-        boolean handled = true;
-        if (mInvalidGesture) {
-            handled = false;
-        } else if (!mGestureInProgress) {
-            switch (action) {
-                case MotionEvent.ACTION_DOWN: {
-                    mActiveId0 = event.getPointerId(0);
-                    mActive0MostRecent = true;
-                }
-                break;
-
-                case MotionEvent.ACTION_UP:
-                    reset();
-                    break;
-
-                case MotionEvent.ACTION_POINTER_DOWN: {
-                    // We have a new multi-finger gesture
-                    if (mPrevEvent != null) mPrevEvent.recycle();
-                    mPrevEvent = MotionEvent.obtain(event);
-                    mTimeDelta = 0;
-
-                    int index1 = event.getActionIndex();
-                    int index0 = event.findPointerIndex(mActiveId0);
-                    mActiveId1 = event.getPointerId(index1);
-                    if (index0 < 0 || index0 == index1) {
-                        // Probably someone sending us a broken event stream.
-                        index0 = findNewActiveIndex(event, mActiveId1, -1);
-                        mActiveId0 = event.getPointerId(index0);
-                    }
-                    mActive0MostRecent = false;
-
-                    setContext(event);
-
-                    mGestureInProgress = mListener.onScaleBegin(this);
-                    break;
-                }
-            }
-        } else {
-            // Transform gesture in progress - attempt to handle it
-            switch (action) {
-                case MotionEvent.ACTION_POINTER_DOWN: {
-                    // End the old gesture and begin a new one with the most recent two fingers.
-                    mListener.onScaleEnd(this);
-                    final int oldActive0 = mActiveId0;
-                    final int oldActive1 = mActiveId1;
-                    reset();
-
-                    mPrevEvent = MotionEvent.obtain(event);
-                    mActiveId0 = mActive0MostRecent ? oldActive0 : oldActive1;
-                    mActiveId1 = event.getPointerId(event.getActionIndex());
-                    mActive0MostRecent = false;
-
-                    int index0 = event.findPointerIndex(mActiveId0);
-                    if (index0 < 0 || mActiveId0 == mActiveId1) {
-                        // Probably someone sending us a broken event stream.
-                        Log.e(TAG, "Got " + MotionEvent.actionToString(action) +
-                                " with bad state while a gesture was in progress. " +
-                                "Did you forget to pass an event to " +
-                                "ScaleGestureDetector#onTouchEvent?");
-                        index0 = findNewActiveIndex(event, mActiveId1, -1);
-                        mActiveId0 = event.getPointerId(index0);
-                    }
-
-                    setContext(event);
-
-                    mGestureInProgress = mListener.onScaleBegin(this);
-                }
-                break;
-
-                case MotionEvent.ACTION_POINTER_UP: {
-                    final int pointerCount = event.getPointerCount();
-                    final int actionIndex = event.getActionIndex();
-                    final int actionId = event.getPointerId(actionIndex);
-
-                    boolean gestureEnded = false;
-                    if (pointerCount > 2) {
-                        if (actionId == mActiveId0) {
-                            final int newIndex = findNewActiveIndex(event, mActiveId1, actionIndex);
-                            if (newIndex >= 0) {
-                                mListener.onScaleEnd(this);
-                                mActiveId0 = event.getPointerId(newIndex);
-                                mActive0MostRecent = true;
-                                mPrevEvent = MotionEvent.obtain(event);
-                                setContext(event);
-                                mGestureInProgress = mListener.onScaleBegin(this);
-                            } else {
-                                gestureEnded = true;
-                            }
-                        } else if (actionId == mActiveId1) {
-                            final int newIndex = findNewActiveIndex(event, mActiveId0, actionIndex);
-                            if (newIndex >= 0) {
-                                mListener.onScaleEnd(this);
-                                mActiveId1 = event.getPointerId(newIndex);
-                                mActive0MostRecent = false;
-                                mPrevEvent = MotionEvent.obtain(event);
-                                setContext(event);
-                                mGestureInProgress = mListener.onScaleBegin(this);
-                            } else {
-                                gestureEnded = true;
-                            }
-                        }
-                        mPrevEvent.recycle();
-                        mPrevEvent = MotionEvent.obtain(event);
-                        setContext(event);
-                    } else {
-                        gestureEnded = true;
-                    }
-
-                    if (gestureEnded) {
-                        // Gesture ended
-                        setContext(event);
-
-                        // Set focus point to the remaining finger
-                        final int activeId = actionId == mActiveId0 ? mActiveId1 : mActiveId0;
-                        final int index = event.findPointerIndex(activeId);
-                        mFocusX = event.getX(index);
-                        mFocusY = event.getY(index);
-
-                        mListener.onScaleEnd(this);
-                        reset();
-                        mActiveId0 = activeId;
-                        mActive0MostRecent = true;
-                    }
-                }
-                break;
-
-                case MotionEvent.ACTION_CANCEL:
-                    mListener.onScaleEnd(this);
-                    reset();
-                    break;
-
-                case MotionEvent.ACTION_UP:
-                    reset();
-                    break;
-
-                case MotionEvent.ACTION_MOVE: {
-                    setContext(event);
-
-                    // Only accept the event if our relative pressure is within
-                    // a certain limit - this can help filter shaky data as a
-                    // finger is lifted.
-                    if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
-                        final boolean updatePrevious = mListener.onScale(this);
-
-                        if (updatePrevious) {
-                            mPrevEvent.recycle();
-                            mPrevEvent = MotionEvent.obtain(event);
-                        }
-                    }
-                }
-                break;
-            }
-        }
-
-        if (!handled && mInputEventConsistencyVerifier != null) {
-            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
-        }
-        return handled;
-    }
-
-    private int findNewActiveIndex(MotionEvent ev, int otherActiveId, int removedPointerIndex) {
-        final int pointerCount = ev.getPointerCount();
-
-        // It's ok if this isn't found and returns -1, it simply won't match.
-        final int otherActiveIndex = ev.findPointerIndex(otherActiveId);
-
-        // Pick a new id and update tracking state.
-        for (int i = 0; i < pointerCount; i++) {
-            if (i != removedPointerIndex && i != otherActiveIndex) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
-    private void setContext(MotionEvent curr) {
-        if (mCurrEvent != null) {
-            mCurrEvent.recycle();
-        }
-        mCurrEvent = MotionEvent.obtain(curr);
-
-        mCurrLen = -1;
-        mPrevLen = -1;
-        mScaleFactor = -1;
-
-        final MotionEvent prev = mPrevEvent;
-
-        final int prevIndex0 = prev.findPointerIndex(mActiveId0);
-        final int prevIndex1 = prev.findPointerIndex(mActiveId1);
-        final int currIndex0 = curr.findPointerIndex(mActiveId0);
-        final int currIndex1 = curr.findPointerIndex(mActiveId1);
-
-        if (prevIndex0 < 0 || prevIndex1 < 0 || currIndex0 < 0 || currIndex1 < 0) {
-            mInvalidGesture = true;
-            Log.e(TAG, "Invalid MotionEvent stream detected.", new Throwable());
-            if (mGestureInProgress) {
+        final boolean streamComplete = action == MotionEvent.ACTION_UP ||
+                action == MotionEvent.ACTION_CANCEL;
+        if (action == MotionEvent.ACTION_DOWN || streamComplete) {
+            // Reset any scale in progress with the listener.
+            // If it's an ACTION_DOWN we're beginning a new event stream.
+            // This means the app probably didn't give us all the events. Shame on it.
+            if (mInProgress) {
                 mListener.onScaleEnd(this);
+                mInProgress = false;
             }
-            return;
+
+            if (streamComplete) {
+                return true;
+            }
         }
 
-        final float px0 = prev.getX(prevIndex0);
-        final float py0 = prev.getY(prevIndex0);
-        final float px1 = prev.getX(prevIndex1);
-        final float py1 = prev.getY(prevIndex1);
-        final float cx0 = curr.getX(currIndex0);
-        final float cy0 = curr.getY(currIndex0);
-        final float cx1 = curr.getX(currIndex1);
-        final float cy1 = curr.getY(currIndex1);
+        final boolean configChanged =
+                action == MotionEvent.ACTION_POINTER_UP ||
+                action == MotionEvent.ACTION_POINTER_DOWN;
+        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
+        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
 
-        final float pvx = px1 - px0;
-        final float pvy = py1 - py0;
-        final float cvx = cx1 - cx0;
-        final float cvy = cy1 - cy0;
-        mPrevFingerDiffX = pvx;
-        mPrevFingerDiffY = pvy;
-        mCurrFingerDiffX = cvx;
-        mCurrFingerDiffY = cvy;
-
-        mFocusX = cx0 + cvx * 0.5f;
-        mFocusY = cy0 + cvy * 0.5f;
-        mTimeDelta = curr.getEventTime() - prev.getEventTime();
-        mCurrPressure = curr.getPressure(currIndex0) + curr.getPressure(currIndex1);
-        mPrevPressure = prev.getPressure(prevIndex0) + prev.getPressure(prevIndex1);
-    }
-
-    private void reset() {
-        if (mPrevEvent != null) {
-            mPrevEvent.recycle();
-            mPrevEvent = null;
+        // Determine focal point
+        float sumX = 0, sumY = 0;
+        final int count = event.getPointerCount();
+        for (int i = 0; i < count; i++) {
+            if (skipIndex == i) continue;
+            sumX += event.getX(i);
+            sumY += event.getY(i);
         }
-        if (mCurrEvent != null) {
-            mCurrEvent.recycle();
-            mCurrEvent = null;
+        final int div = pointerUp ? count - 1 : count;
+        final float focusX = sumX / div;
+        final float focusY = sumY / div;
+
+        // Determine average deviation from focal point
+        float devSumX = 0, devSumY = 0;
+        for (int i = 0; i < count; i++) {
+            if (skipIndex == i) continue;
+            devSumX += Math.abs(event.getX(i) - focusX);
+            devSumY += Math.abs(event.getY(i) - focusY);
         }
-        mGestureInProgress = false;
-        mActiveId0 = -1;
-        mActiveId1 = -1;
-        mInvalidGesture = false;
+        final float devX = devSumX / div;
+        final float devY = devSumY / div;
+
+        // Span is the average distance between touch points through the focal point;
+        // i.e. the diameter of the circle with a radius of the average deviation from
+        // the focal point.
+        final float spanX = devX * 2;
+        final float spanY = devY * 2;
+        final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY);
+
+        // Dispatch begin/end events as needed.
+        // If the configuration changes, notify the app to reset its current state by beginning
+        // a fresh scale event stream.
+        if (mInProgress && (span == 0 || configChanged)) {
+            mListener.onScaleEnd(this);
+            mInProgress = false;
+        }
+        if (configChanged) {
+            mPrevSpanX = mCurrSpanX = spanX;
+            mPrevSpanY = mCurrSpanY = spanY;
+            mPrevSpan = mCurrSpan = span;
+        }
+        if (!mInProgress && span != 0) {
+            mFocusX = focusX;
+            mFocusY = focusY;
+            mInProgress = mListener.onScaleBegin(this);
+        }
+
+        // Handle motion; focal point and span/scale factor are changing.
+        if (action == MotionEvent.ACTION_MOVE) {
+            mCurrSpanX = spanX;
+            mCurrSpanY = spanY;
+            mCurrSpan = span;
+            mFocusX = focusX;
+            mFocusY = focusY;
+
+            boolean updatePrev = true;
+            if (mInProgress) {
+                updatePrev = mListener.onScale(this);
+            }
+
+            if (updatePrev) {
+                mPrevSpanX = mCurrSpanX;
+                mPrevSpanY = mCurrSpanY;
+                mPrevSpan = mCurrSpan;
+            }
+        }
+
+        return true;
     }
 
     /**
-     * Returns {@code true} if a two-finger scale gesture is in progress.
-     * @return {@code true} if a scale gesture is in progress, {@code false} otherwise.
+     * Returns {@code true} if a scale gesture is in progress.
      */
     public boolean isInProgress() {
-        return mGestureInProgress;
+        return mInProgress;
     }
 
     /**
      * Get the X coordinate of the current gesture's focal point.
-     * If a gesture is in progress, the focal point is directly between
-     * the two pointers forming the gesture.
-     * If a gesture is ending, the focal point is the location of the
-     * remaining pointer on the screen.
+     * If a gesture is in progress, the focal point is between
+     * each of the pointers forming the gesture.
+     *
      * If {@link #isInProgress()} would return false, the result of this
      * function is undefined.
      *
@@ -448,10 +282,9 @@
 
     /**
      * Get the Y coordinate of the current gesture's focal point.
-     * If a gesture is in progress, the focal point is directly between
-     * the two pointers forming the gesture.
-     * If a gesture is ending, the focal point is the location of the
-     * remaining pointer on the screen.
+     * If a gesture is in progress, the focal point is between
+     * each of the pointers forming the gesture.
+     *
      * If {@link #isInProgress()} would return false, the result of this
      * function is undefined.
      *
@@ -462,73 +295,63 @@
     }
 
     /**
-     * Return the current distance between the two pointers forming the
-     * gesture in progress.
+     * Return the average distance between each of the pointers forming the
+     * gesture in progress through the focal point.
      *
      * @return Distance between pointers in pixels.
      */
     public float getCurrentSpan() {
-        if (mCurrLen == -1) {
-            final float cvx = mCurrFingerDiffX;
-            final float cvy = mCurrFingerDiffY;
-            mCurrLen = FloatMath.sqrt(cvx*cvx + cvy*cvy);
-        }
-        return mCurrLen;
+        return mCurrSpan;
     }
 
     /**
-     * Return the current x distance between the two pointers forming the
-     * gesture in progress.
+     * Return the average X distance between each of the pointers forming the
+     * gesture in progress through the focal point.
      *
      * @return Distance between pointers in pixels.
      */
     public float getCurrentSpanX() {
-        return mCurrFingerDiffX;
+        return mCurrSpanX;
     }
 
     /**
-     * Return the current y distance between the two pointers forming the
-     * gesture in progress.
+     * Return the average Y distance between each of the pointers forming the
+     * gesture in progress through the focal point.
      *
      * @return Distance between pointers in pixels.
      */
     public float getCurrentSpanY() {
-        return mCurrFingerDiffY;
+        return mCurrSpanY;
     }
 
     /**
-     * Return the previous distance between the two pointers forming the
-     * gesture in progress.
+     * Return the previous average distance between each of the pointers forming the
+     * gesture in progress through the focal point.
      *
      * @return Previous distance between pointers in pixels.
      */
     public float getPreviousSpan() {
-        if (mPrevLen == -1) {
-            final float pvx = mPrevFingerDiffX;
-            final float pvy = mPrevFingerDiffY;
-            mPrevLen = FloatMath.sqrt(pvx*pvx + pvy*pvy);
-        }
-        return mPrevLen;
+        return mPrevSpan;
     }
 
     /**
-     * Return the previous x distance between the two pointers forming the
-     * gesture in progress.
+     * Return the previous average X distance between each of the pointers forming the
+     * gesture in progress through the focal point.
      *
      * @return Previous distance between pointers in pixels.
      */
     public float getPreviousSpanX() {
-        return mPrevFingerDiffX;
+        return mPrevSpanX;
     }
 
     /**
-     * Return the previous y distance between the two pointers forming the
-     * gesture in progress.
+     * Return the previous average Y distance between each of the pointers forming the
+     * gesture in progress through the focal point.
      *
      * @return Previous distance between pointers in pixels.
      */
     public float getPreviousSpanY() {
-        return mPrevFingerDiffY;
+        return mPrevSpanY;
     }
 
     /**
@@ -539,10 +362,7 @@
      * @return The current scaling factor.
      */
     public float getScaleFactor() {
-        if (mScaleFactor == -1) {
-            mScaleFactor = getCurrentSpan() / getPreviousSpan();
-        }
-        return mScaleFactor;
+        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
     }
 
     /**
@@ -552,7 +372,7 @@
      * @return Time difference since the last scaling event in milliseconds.
      */
     public long getTimeDelta() {
-        return mTimeDelta;
+        return mCurrTime - mPrevTime;
     }
 
     /**
@@ -561,6 +381,6 @@
      * @return Current event time in milliseconds.
      */
     public long getEventTime() {
-        return mCurrEvent.getEventTime();
+        return mCurrTime;
     }
 }