Merge "DO NOT MERGE Remove dead code" into jb-dev
diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java
index 0114a41..23337f0 100644
--- a/core/java/android/view/GestureDetector.java
+++ b/core/java/android/view/GestureDetector.java
@@ -226,17 +226,12 @@
      */
     private boolean mIsDoubleTapping;
 
-    private float mLastMotionY;
-    private float mLastMotionX;
+    private float mLastFocusX;
+    private float mLastFocusY;
+    private float mDownFocusX;
+    private float mDownFocusY;
 
     private boolean mIsLongpressEnabled;
-    
-    /**
-     * True if we are at a target API level of >= Froyo or the developer can
-     * explicitly set it. If true, input events with > 1 pointer will be ignored
-     * so we can work side by side with multitouch gesture detectors.
-     */
-    private boolean mIgnoreMultitouch;
 
     /**
      * Determines speed during touch scrolling
@@ -349,8 +344,16 @@
      * @throws NullPointerException if {@code listener} is null.
      */
     public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
-        this(context, listener, handler, context != null &&
-                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.FROYO);
+        if (handler != null) {
+            mHandler = new GestureHandler(handler);
+        } else {
+            mHandler = new GestureHandler();
+        }
+        mListener = listener;
+        if (listener instanceof OnDoubleTapListener) {
+            setOnDoubleTapListener((OnDoubleTapListener) listener);
+        }
+        init(context);
     }
     
     /**
@@ -362,31 +365,19 @@
      * @param listener the listener invoked for all the callbacks, this must
      * not be null.
      * @param handler the handler to use
-     * @param ignoreMultitouch whether events involving more than one pointer should
-     * be ignored.
      *
      * @throws NullPointerException if {@code listener} is null.
      */
     public GestureDetector(Context context, OnGestureListener listener, Handler handler,
-            boolean ignoreMultitouch) {
-        if (handler != null) {
-            mHandler = new GestureHandler(handler);
-        } else {
-            mHandler = new GestureHandler();
-        }
-        mListener = listener;
-        if (listener instanceof OnDoubleTapListener) {
-            setOnDoubleTapListener((OnDoubleTapListener) listener);
-        }
-        init(context, ignoreMultitouch);
+            boolean unused) {
+        this(context, listener, handler);
     }
 
-    private void init(Context context, boolean ignoreMultitouch) {
+    private void init(Context context) {
         if (mListener == null) {
             throw new NullPointerException("OnGestureListener must not be null");
         }
         mIsLongpressEnabled = true;
-        mIgnoreMultitouch = ignoreMultitouch;
 
         // Fallback to support pre-donuts releases
         int touchSlop, doubleTapSlop, doubleTapTouchSlop;
@@ -456,34 +447,40 @@
         }
 
         final int action = ev.getAction();
-        final float y = ev.getY();
-        final float x = ev.getX();
 
         if (mVelocityTracker == null) {
             mVelocityTracker = VelocityTracker.obtain();
         }
         mVelocityTracker.addMovement(ev);
 
+        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
+        final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
+
+        // Determine focal point
+        float sumX = 0, sumY = 0;
+        final int count = ev.getPointerCount();
+        for (int i = 0; i < count; i++) {
+            if (skipIndex == i) continue;
+            sumX += ev.getX(i);
+            sumY += ev.getY(i);
+        }
+        final int div = pointerUp ? count - 1 : count;
+        final float focusX = sumX / div;
+        final float focusY = sumY / div;
+
         boolean handled = false;
 
         switch (action & MotionEvent.ACTION_MASK) {
         case MotionEvent.ACTION_POINTER_DOWN:
-            if (mIgnoreMultitouch) {
-                // Multitouch event - abort.
-                cancel();
-            }
+            mDownFocusX = mLastFocusX = focusX;
+            mDownFocusY = mLastFocusY = focusY;
+            // Cancel long press and taps
+            cancelTaps();
             break;
 
         case MotionEvent.ACTION_POINTER_UP:
-            // Ending a multitouch gesture and going back to 1 finger
-            if (mIgnoreMultitouch && ev.getPointerCount() == 2) {
-                int index = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
-                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1 : 0;
-                mLastMotionX = ev.getX(index);
-                mLastMotionY = ev.getY(index);
-                mVelocityTracker.recycle();
-                mVelocityTracker = VelocityTracker.obtain();
-            }
+            mDownFocusX = mLastFocusX = focusX;
+            mDownFocusY = mLastFocusY = focusY;
             break;
 
         case MotionEvent.ACTION_DOWN:
@@ -504,8 +501,8 @@
                 }
             }
 
-            mLastMotionX = x;
-            mLastMotionY = y;
+            mDownFocusX = mLastFocusX = focusX;
+            mDownFocusY = mLastFocusY = focusY;
             if (mCurrentDownEvent != null) {
                 mCurrentDownEvent.recycle();
             }
@@ -525,22 +522,22 @@
             break;
 
         case MotionEvent.ACTION_MOVE:
-            if (mInLongPress || (mIgnoreMultitouch && ev.getPointerCount() > 1)) {
+            if (mInLongPress) {
                 break;
             }
-            final float scrollX = mLastMotionX - x;
-            final float scrollY = mLastMotionY - y;
+            final float scrollX = mLastFocusX - focusX;
+            final float scrollY = mLastFocusY - focusY;
             if (mIsDoubleTapping) {
                 // Give the move events of the double-tap
                 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
             } else if (mAlwaysInTapRegion) {
-                final int deltaX = (int) (x - mCurrentDownEvent.getX());
-                final int deltaY = (int) (y - mCurrentDownEvent.getY());
+                final int deltaX = (int) (focusX - mDownFocusX);
+                final int deltaY = (int) (focusY - mDownFocusY);
                 int distance = (deltaX * deltaX) + (deltaY * deltaY);
                 if (distance > mTouchSlopSquare) {
                     handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
-                    mLastMotionX = x;
-                    mLastMotionY = y;
+                    mLastFocusX = focusX;
+                    mLastFocusY = focusY;
                     mAlwaysInTapRegion = false;
                     mHandler.removeMessages(TAP);
                     mHandler.removeMessages(SHOW_PRESS);
@@ -551,8 +548,8 @@
                 }
             } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
                 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
-                mLastMotionX = x;
-                mLastMotionY = y;
+                mLastFocusX = focusX;
+                mLastFocusY = focusY;
             }
             break;
 
@@ -571,9 +568,10 @@
 
                 // A fling must travel the minimum tap distance
                 final VelocityTracker velocityTracker = mVelocityTracker;
+                final int pointerId = ev.getPointerId(0);
                 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
-                final float velocityY = velocityTracker.getYVelocity();
-                final float velocityX = velocityTracker.getXVelocity();
+                final float velocityY = velocityTracker.getYVelocity(pointerId);
+                final float velocityX = velocityTracker.getXVelocity(pointerId);
 
                 if ((Math.abs(velocityY) > mMinimumFlingVelocity)
                         || (Math.abs(velocityX) > mMinimumFlingVelocity)){
@@ -622,6 +620,18 @@
         }
     }
 
+    private void cancelTaps() {
+        mHandler.removeMessages(SHOW_PRESS);
+        mHandler.removeMessages(LONG_PRESS);
+        mHandler.removeMessages(TAP);
+        mIsDoubleTapping = false;
+        mAlwaysInTapRegion = false;
+        mAlwaysInBiggerTapRegion = false;
+        if (mInLongPress) {
+            mInLongPress = false;
+        }
+    }
+
     private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
             MotionEvent secondDown) {
         if (!mAlwaysInBiggerTapRegion) {
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;
     }
 }
diff --git a/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java b/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java
index 5cacd23..3d88f50 100755
--- a/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java
+++ b/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java
@@ -595,7 +595,6 @@
         byte[] payload = encodeUtf16(uData.payloadStr);
         int udhBytes = udhData.length + 1;  // Add length octet.
         int udhCodeUnits = (udhBytes + 1) / 2;
-        int udhPadding = udhBytes % 2;
         int payloadCodeUnits = payload.length / 2;
         uData.msgEncoding = UserData.ENCODING_UNICODE_16;
         uData.msgEncodingSet = true;
@@ -603,7 +602,7 @@
         uData.payload = new byte[uData.numFields * 2];
         uData.payload[0] = (byte)udhData.length;
         System.arraycopy(udhData, 0, uData.payload, 1, udhData.length);
-        System.arraycopy(payload, 0, uData.payload, udhBytes + udhPadding, payload.length);
+        System.arraycopy(payload, 0, uData.payload, udhBytes, payload.length);
     }
 
     private static void encodeEmsUserDataPayload(UserData uData)
@@ -997,27 +996,37 @@
     private static String decodeUtf8(byte[] data, int offset, int numFields)
         throws CodingException
     {
-        if (numFields < 0 || (numFields + offset) > data.length) {
-            throw new CodingException("UTF-8 decode failed: offset or length out of range");
-        }
-        try {
-            return new String(data, offset, numFields, "UTF-8");
-        } catch (java.io.UnsupportedEncodingException ex) {
-            throw new CodingException("UTF-8 decode failed: " + ex);
-        }
+        return decodeCharset(data, offset, numFields, 1, "UTF-8");
     }
 
     private static String decodeUtf16(byte[] data, int offset, int numFields)
         throws CodingException
     {
-        int byteCount = numFields * 2;
-        if (byteCount < 0 || (byteCount + offset) > data.length) {
-            throw new CodingException("UTF-16 decode failed: offset or length out of range");
+        // Subtract header and possible padding byte (at end) from num fields.
+        int padding = offset % 2;
+        numFields -= (offset + padding) / 2;
+        return decodeCharset(data, offset, numFields, 2, "utf-16be");
+    }
+
+    private static String decodeCharset(byte[] data, int offset, int numFields, int width,
+            String charset) throws CodingException
+    {
+        if (numFields < 0 || (numFields * width + offset) > data.length) {
+            // Try to decode the max number of characters in payload
+            int padding = offset % width;
+            int maxNumFields = (data.length - offset - padding) / width;
+            if (maxNumFields < 0) {
+                throw new CodingException(charset + " decode failed: offset out of range");
+            }
+            Log.e(LOG_TAG, charset + " decode error: offset = " + offset + " numFields = "
+                    + numFields + " data.length = " + data.length + " maxNumFields = "
+                    + maxNumFields);
+            numFields = maxNumFields;
         }
         try {
-            return new String(data, offset, byteCount, "utf-16be");
+            return new String(data, offset, numFields * width, charset);
         } catch (java.io.UnsupportedEncodingException ex) {
-            throw new CodingException("UTF-16 decode failed: " + ex);
+            throw new CodingException(charset + " decode failed: " + ex);
         }
     }
 
@@ -1073,14 +1082,7 @@
     private static String decodeLatin(byte[] data, int offset, int numFields)
         throws CodingException
     {
-        if (numFields < 0 || (numFields + offset) > data.length) {
-            throw new CodingException("ISO-8859-1 decode failed: offset or length out of range");
-        }
-        try {
-            return new String(data, offset, numFields, "ISO-8859-1");
-        } catch (java.io.UnsupportedEncodingException ex) {
-            throw new CodingException("ISO-8859-1 decode failed: " + ex);
-        }
+        return decodeCharset(data, offset, numFields, 1, "ISO-8859-1");
     }
 
     private static void decodeUserDataPayload(UserData userData, boolean hasUserDataHeader)
diff --git a/telephony/tests/telephonytests/src/com/android/internal/telephony/cdma/sms/CdmaSmsTest.java b/telephony/tests/telephonytests/src/com/android/internal/telephony/cdma/sms/CdmaSmsTest.java
index 58e73e0..f1bc268 100644
--- a/telephony/tests/telephonytests/src/com/android/internal/telephony/cdma/sms/CdmaSmsTest.java
+++ b/telephony/tests/telephonytests/src/com/android/internal/telephony/cdma/sms/CdmaSmsTest.java
@@ -35,10 +35,21 @@
 import android.util.Log;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 
 public class CdmaSmsTest extends AndroidTestCase {
     private final static String LOG_TAG = "XXX CdmaSmsTest XXX";
 
+    // CJK ideographs, Hiragana, Katakana, full width letters, Cyrillic, etc.
+    private static final String sUnicodeChars = "\u4e00\u4e01\u4e02\u4e03" +
+            "\u4e04\u4e05\u4e06\u4e07\u4e08\u4e09\u4e0a\u4e0b\u4e0c\u4e0d" +
+            "\u4e0e\u4e0f\u3041\u3042\u3043\u3044\u3045\u3046\u3047\u3048" +
+            "\u30a1\u30a2\u30a3\u30a4\u30a5\u30a6\u30a7\u30a8" +
+            "\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18" +
+            "\uff70\uff71\uff72\uff73\uff74\uff75\uff76\uff77\uff78" +
+            "\u0400\u0401\u0402\u0403\u0404\u0405\u0406\u0407\u0408" +
+            "\u00a2\u00a9\u00ae\u2122";
+
     @SmallTest
     public void testCdmaSmsAddrParsing() throws Exception {
         CdmaSmsAddress addr = CdmaSmsAddress.parse("6502531000");
@@ -811,23 +822,51 @@
 
     @SmallTest
     public void testUserDataHeaderWithEightCharMsg() throws Exception {
+        encodeDecodeAssertEquals("01234567", 2, 2, false);
+    }
+
+    private void encodeDecodeAssertEquals(String payload, int index, int total,
+            boolean oddLengthHeader) throws Exception {
         BearerData bearerData = new BearerData();
         bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER;
         bearerData.messageId = 55;
-        SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef();
-        concatRef.refNumber = 0xEE;
-        concatRef.msgCount = 2;
-        concatRef.seqNumber = 2;
-        concatRef.isEightBits = true;
         SmsHeader smsHeader = new SmsHeader();
-        smsHeader.concatRef = concatRef;
+        if (oddLengthHeader) {
+            // Odd length header to verify correct UTF-16 header padding
+            SmsHeader.MiscElt miscElt = new SmsHeader.MiscElt();
+            miscElt.id = 0x27;  // reserved for future use; ignored on decode
+            miscElt.data = new byte[]{0x12, 0x34};
+            smsHeader.miscEltList.add(miscElt);
+        } else {
+            // Even length header normally generated for concatenated SMS.
+            SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef();
+            concatRef.refNumber = 0xEE;
+            concatRef.msgCount = total;
+            concatRef.seqNumber = index;
+            concatRef.isEightBits = true;
+            smsHeader.concatRef = concatRef;
+        }
+        byte[] encodeHeader = SmsHeader.toByteArray(smsHeader);
+        if (oddLengthHeader) {
+            assertEquals(4, encodeHeader.length);     // 5 bytes with UDH length
+        } else {
+            assertEquals(5, encodeHeader.length);     // 6 bytes with UDH length
+        }
         UserData userData = new UserData();
-        userData.payloadStr = "01234567";
+        userData.payloadStr = payload;
         userData.userDataHeader = smsHeader;
         bearerData.userData = userData;
         byte[] encodedSms = BearerData.encode(bearerData);
         BearerData revBearerData = BearerData.decode(encodedSms);
         assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
+        assertTrue(revBearerData.hasUserDataHeader);
+        byte[] header = SmsHeader.toByteArray(revBearerData.userData.userDataHeader);
+        if (oddLengthHeader) {
+            assertEquals(4, header.length);     // 5 bytes with UDH length
+        } else {
+            assertEquals(5, header.length);     // 6 bytes with UDH length
+        }
+        assertTrue(Arrays.equals(encodeHeader, header));
     }
 
     @SmallTest
@@ -881,7 +920,27 @@
         if (isCdmaPhone) {
             ArrayList<String> fragments = android.telephony.SmsMessage.fragmentText(text2);
             assertEquals(3, fragments.size());
+
+            for (int i = 0; i < 3; i++) {
+                encodeDecodeAssertEquals(fragments.get(i), i + 1, 3, false);
+                encodeDecodeAssertEquals(fragments.get(i), i + 1, 3, true);
+            }
         }
 
+        // Test case for multi-part UTF-16 message.
+        String text3 = sUnicodeChars + sUnicodeChars + sUnicodeChars;
+        ted = SmsMessage.calculateLength(text3, false);
+        assertEquals(3, ted.msgCount);
+        assertEquals(189, ted.codeUnitCount);
+        assertEquals(3, ted.codeUnitSize);
+        if (isCdmaPhone) {
+            ArrayList<String> fragments = android.telephony.SmsMessage.fragmentText(text3);
+            assertEquals(3, fragments.size());
+
+            for (int i = 0; i < 3; i++) {
+                encodeDecodeAssertEquals(fragments.get(i), i + 1, 3, false);
+                encodeDecodeAssertEquals(fragments.get(i), i + 1, 3, true);
+            }
+        }
     }
 }