Add MotionEvent Matrix transformations.

Fixed issued in ViewGroup's transformation of MotionEvents to ensure
that the entire historical trace is transformed, not just the current
pointer.

Simplified the code in ViewGroup for splitting events across Views.
The new code also handles the case where some pointers are dispatched
to the ViewGroup in addition to its children whereas the previous
code would drop some pointers on the floor.

Change-Id: I56ac31903e1de8a9c376d9c935b7217b0c42d93e
diff --git a/api/current.xml b/api/current.xml
index e3b6a01..1b8381e 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -195957,6 +195957,19 @@
 <parameter name="y" type="float">
 </parameter>
 </method>
+<method name="transform"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="matrix" type="android.graphics.Matrix">
+</parameter>
+</method>
 <method name="writeToParcel"
  return="void"
  abstract="false"
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index 6705596..dfbe65c 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import android.graphics.Matrix;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.SystemClock;
@@ -347,6 +348,8 @@
     private RuntimeException mRecycledLocation;
     private boolean mRecycled;
 
+    private native void nativeTransform(Matrix matrix);
+
     private MotionEvent(int pointerCount, int sampleCount) {
         mPointerIdentifiers = new int[pointerCount];
         mDataSamples = new float[pointerCount * sampleCount * NUM_SAMPLE_DATA];
@@ -1413,6 +1416,19 @@
         mYOffset = y - dataSamples[lastDataSampleIndex + SAMPLE_Y];
     }
     
+    /**
+     * Applies a transformation matrix to all of the points in the event.
+     *
+     * @param matrix The transformation matrix to apply.
+     */
+    public final void transform(Matrix matrix) {
+        if (matrix == null) {
+            throw new IllegalArgumentException("matrix must not be null");
+        }
+
+        nativeTransform(matrix);
+    }
+
     private final void getPointerCoordsAtSampleIndex(int sampleIndex,
             PointerCoords outPointerCoords) {
         final float[] dataSamples = mDataSamples;
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 3b10437..f2d134b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -5761,13 +5761,21 @@
     }
 
     /**
+     * Determines whether the given point, in local coordinates is inside the view.
+     */
+    /*package*/ final boolean pointInView(float localX, float localY) {
+        return localX >= 0 && localX < (mRight - mLeft)
+                && localY >= 0 && localY < (mBottom - mTop);
+    }
+
+    /**
      * Utility method to determine whether the given point, in local coordinates,
      * is inside the view, where the area of the view is expanded by the slop factor.
      * This method is called while processing touch-move events to determine if the event
      * is still within the view.
      */
     private boolean pointInView(float localX, float localY, float slop) {
-        return localX > -slop && localY > -slop && localX < ((mRight - mLeft) + slop) &&
+        return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
                 localY < ((mBottom - mTop) + slop);
     }
 
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 570e288..1e86f74 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -105,16 +105,18 @@
      */
     private Transformation mInvalidationTransformation;
 
-    // Target of Motion events
-    private View mMotionTarget;
-
-    // Targets of MotionEvents in split mode
-    private SplitMotionTargets mSplitMotionTargets;
-
     // Layout animation
     private LayoutAnimationController mLayoutAnimationController;
     private Animation.AnimationListener mAnimationListener;
 
+    // First touch target in the linked list of touch targets.
+    private TouchTarget mFirstTouchTarget;
+
+    // Temporary arrays for splitting pointers.
+    private int[] mTmpPointerIndexMap;
+    private int[] mTmpPointerIds;
+    private MotionEvent.PointerCoords[] mTmpPointerCoords;
+
     /**
      * Internal flags.
      *
@@ -872,150 +874,254 @@
             return false;
         }
 
-        if ((mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) == FLAG_SPLIT_MOTION_EVENTS) {
-            if (mSplitMotionTargets == null) {
-                mSplitMotionTargets = new SplitMotionTargets();
-            }
-            return dispatchSplitTouchEvent(ev);
+        final int action = ev.getAction();
+        final int actionMasked = action & MotionEvent.ACTION_MASK;
+
+        // Handle an initial down.
+        if (actionMasked == MotionEvent.ACTION_DOWN) {
+            // Throw away all previous state when starting a new touch gesture.
+            // The framework may have dropped the up or cancel event for the previous gesture
+            // due to an app switch, ANR, or some other state change.
+            cancelAndClearTouchTargets(ev);
+            resetTouchState();
         }
 
-        final int action = ev.getAction();
-        final float xf = ev.getX();
-        final float yf = ev.getY();
-        final float scrolledXFloat = xf + mScrollX;
-        final float scrolledYFloat = yf + mScrollY;
-
-        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
-
-        if (action == MotionEvent.ACTION_DOWN) {
-            if (mMotionTarget != null) {
-                // this is weird, we got a pen down, but we thought it was
-                // already down!
-                // XXX: We should probably send an ACTION_UP to the current
-                // target.
-                mMotionTarget = null;
+        // Check for interception.
+        final boolean intercepted;
+        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
+            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
+            if (!disallowIntercept) {
+                intercepted = onInterceptTouchEvent(ev);
+                ev.setAction(action); // restore action in case onInterceptTouchEvent() changed it
+            } else {
+                intercepted = false;
             }
-            // If we're disallowing intercept or if we're allowing and we didn't
-            // intercept
-            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
-                // reset this event's action (just to protect ourselves)
-                ev.setAction(MotionEvent.ACTION_DOWN);
-                // We know we want to dispatch the event down, find a child
-                // who can handle it, start with the front-most child.
-                final View[] children = mChildren;
-                final int count = mChildrenCount;
+        } else {
+            intercepted = true;
+        }
 
-                for (int i = count - 1; i >= 0; i--) {
-                    final View child = children[i];
-                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
-                            || child.getAnimation() != null) {
-                        // Single dispatch always picks its target based on the initial down
-                        // event's position - index 0
-                        if (dispatchTouchEventIfInView(child, ev, 0)) {
-                            mMotionTarget = child;
-                            return true;
+        // Check for cancelation.
+        final boolean canceled = resetCancelNextUpFlag(this)
+                || actionMasked == MotionEvent.ACTION_CANCEL;
+
+        // Update list of touch targets for pointer down, if needed.
+        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
+        TouchTarget newTouchTarget = null;
+        boolean alreadyDispatchedToNewTouchTarget = false;
+        if (!canceled && !intercepted) {
+            if (actionMasked == MotionEvent.ACTION_DOWN
+                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)) {
+                final int actionIndex = ev.getActionIndex(); // always 0 for down
+                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
+                        : TouchTarget.ALL_POINTER_IDS;
+
+                // Clean up earlier touch targets for this pointer id in case they
+                // have become out of sync.
+                removePointersFromTouchTargets(idBitsToAssign);
+
+                final int childrenCount = mChildrenCount;
+                if (childrenCount != 0) {
+                    // Find a child that can receive the event.  Scan children from front to back.
+                    final View[] children = mChildren;
+                    final float x = ev.getX(actionIndex);
+                    final float y = ev.getY(actionIndex);
+
+                    for (int i = childrenCount - 1; i >= 0; i--) {
+                        final View child = children[i];
+                        if ((child.mViewFlags & VISIBILITY_MASK) != VISIBLE
+                                && child.getAnimation() == null) {
+                            // Skip invisible child unless it is animating.
+                            continue;
+                        }
+
+                        if (!isTransformedTouchPointInView(x, y, child)) {
+                            // New pointer is out of child's bounds.
+                            continue;
+                        }
+
+                        newTouchTarget = getTouchTarget(child);
+                        if (newTouchTarget != null) {
+                            // Child is already receiving touch within its bounds.
+                            // Give it the new pointer in addition to the ones it is handling.
+                            newTouchTarget.pointerIdBits |= idBitsToAssign;
+                            break;
+                        }
+
+                        resetCancelNextUpFlag(child);
+                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
+                            // Child wants to receive touch within its bounds.
+                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
+                            alreadyDispatchedToNewTouchTarget = true;
+                            break;
                         }
                     }
                 }
+
+                if (newTouchTarget == null && mFirstTouchTarget != null) {
+                    // Did not find a child to receive the event.
+                    // Assign the pointer to the least recently added target.
+                    newTouchTarget = mFirstTouchTarget;
+                    while (newTouchTarget.next != null) {
+                        newTouchTarget = newTouchTarget.next;
+                    }
+                    newTouchTarget.pointerIdBits |= idBitsToAssign;
+                }
             }
         }
 
-        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
-                (action == MotionEvent.ACTION_CANCEL);
-
-        if (isUpOrCancel) {
-            // Note, we've already copied the previous state to our local
-            // variable, so this takes effect on the next event
-            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
-        }
-
-        // The event wasn't an ACTION_DOWN, dispatch it to our target if
-        // we have one.
-        final View target = mMotionTarget;
-        if (target == null) {
-            // We don't have a target, this means we're handling the
-            // event as a regular view.
-            ev.setLocation(xf, yf);
-            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
-                ev.setAction(MotionEvent.ACTION_CANCEL);
-                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
-            }
-            return super.dispatchTouchEvent(ev);
-        }
-
-        // Calculate the offset point into the target's local coordinates
-        float xc = scrolledXFloat - (float) target.mLeft;
-        float yc = scrolledYFloat - (float) target.mTop;
-        if (!target.hasIdentityMatrix() && mAttachInfo != null) {
-            // non-identity matrix: transform the point into the view's coordinates
-            final float[] localXY = mAttachInfo.mTmpTransformLocation;
-            localXY[0] = xc;
-            localXY[1] = yc;
-            target.getInverseMatrix().mapPoints(localXY);
-            xc = localXY[0];
-            yc = localXY[1];
-        }
-
-        // if have a target, see if we're allowed to and want to intercept its
-        // events
-        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
-            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
-            ev.setAction(MotionEvent.ACTION_CANCEL);
-            ev.setLocation(xc, yc);
-            if (!target.dispatchTouchEvent(ev)) {
-                // target didn't handle ACTION_CANCEL. not much we can do
-                // but they should have.
-            }
-            // clear the target
-            mMotionTarget = null;
-            // Don't dispatch this event to our own view, because we already
-            // saw it when intercepting; we just want to give the following
-            // event to the normal onTouchEvent().
-            return true;
-        }
-
-        if (isUpOrCancel) {
-            mMotionTarget = null;
-        }
-
-        // finally offset the event to the target's coordinate system and
-        // dispatch the event.
-        ev.setLocation(xc, yc);
-
-        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
-            ev.setAction(MotionEvent.ACTION_CANCEL);
-            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
-            mMotionTarget = null;
-        }
-
-        if (target.dispatchTouchEvent(ev)) {
-            return true;
+        // Dispatch to touch targets.
+        boolean handled = false;
+        if (mFirstTouchTarget == null) {
+            // No touch targets so treat this as an ordinary view.
+            handled = dispatchTransformedTouchEvent(ev, canceled, null,
+                    TouchTarget.ALL_POINTER_IDS);
         } else {
-            ev.setLocation(xf, yf);
+            // Dispatch to touch targets, excluding the new touch target if we already
+            // dispatched to it.  Cancel touch targets if necessary.
+            TouchTarget predecessor = null;
+            TouchTarget target = mFirstTouchTarget;
+            while (target != null) {
+                final TouchTarget next = target.next;
+                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
+                    handled = true;
+                } else {
+                    final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
+                    if (dispatchTransformedTouchEvent(ev, cancelChild,
+                            target.child, target.pointerIdBits)) {
+                        handled = true;
+                    }
+                    if (cancelChild) {
+                        if (predecessor == null) {
+                            mFirstTouchTarget = next;
+                        } else {
+                            predecessor.next = next;
+                        }
+                        target.recycle();
+                        target = next;
+                        continue;
+                    }
+                }
+                predecessor = target;
+                target = next;
+            }
+        }
+
+        // Update list of touch targets for pointer up or cancel, if needed.
+        if (canceled || actionMasked == MotionEvent.ACTION_UP) {
+            resetTouchState();
+        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
+            final int actionIndex = ev.getActionIndex();
+            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
+            removePointersFromTouchTargets(idBitsToRemove);
+        }
+
+        return handled;
+    }
+
+    /* Resets all touch state in preparation for a new cycle. */
+    private final void resetTouchState() {
+        clearTouchTargets();
+        resetCancelNextUpFlag(this);
+        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
+    }
+
+    /* Resets the cancel next up flag.
+     * Returns true if the flag was previously set. */
+    private final boolean resetCancelNextUpFlag(View view) {
+        if ((view.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
+            view.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
+            return true;
         }
         return false;
     }
 
-    /**
-     * This method detects whether the pointer location at <code>pointerIndex</code> within
-     * <code>ev</code> is inside the specified view. If so, the transformed event is dispatched to
-     * <code>child</code>.
-     *
-     * @param child View to hit test against
-     * @param ev MotionEvent to test
-     * @param pointerIndex Index of the pointer within <code>ev</code> to test
-     * @return <code>false</code> if the hit test failed, or the result of
-     *         <code>child.dispatchTouchEvent</code>
-     */
-    private boolean dispatchTouchEventIfInView(View child, MotionEvent ev, int pointerIndex) {
-        final float x = ev.getX(pointerIndex);
-        final float y = ev.getY(pointerIndex);
-        final float scrolledX = x + mScrollX;
-        final float scrolledY = y + mScrollY;
-        float localX = scrolledX - child.mLeft;
-        float localY = scrolledY - child.mTop;
-        if (!child.hasIdentityMatrix() && mAttachInfo != null) {
-            // non-identity matrix: transform the point into the view's coordinates
+    /* Clears all touch targets. */
+    private final void clearTouchTargets() {
+        TouchTarget target = mFirstTouchTarget;
+        if (target != null) {
+            do {
+                TouchTarget next = target.next;
+                target.recycle();
+                target = next;
+            } while (target != null);
+            mFirstTouchTarget = null;
+        }
+    }
+
+    /* Cancels and clears all touch targets. */
+    private final void cancelAndClearTouchTargets(MotionEvent event) {
+        if (mFirstTouchTarget != null) {
+            boolean syntheticEvent = false;
+            if (event == null) {
+                final long now = SystemClock.uptimeMillis();
+                event = MotionEvent.obtain(now, now,
+                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+                syntheticEvent = true;
+            }
+
+            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
+                resetCancelNextUpFlag(target.child);
+                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
+            }
+            clearTouchTargets();
+
+            if (syntheticEvent) {
+                event.recycle();
+            }
+        }
+    }
+
+    /* Gets the touch target for specified child view.
+     * Returns null if not found. */
+    private final TouchTarget getTouchTarget(View child) {
+        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
+            if (target.child == child) {
+                return target;
+            }
+        }
+        return null;
+    }
+
+    /* Adds a touch target for specified child to the beginning of the list.
+     * Assumes the target child is not already present. */
+    private final TouchTarget addTouchTarget(View child, int pointerIdBits) {
+        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
+        target.next = mFirstTouchTarget;
+        mFirstTouchTarget = target;
+        return target;
+    }
+
+    /* Removes the pointer ids from consideration. */
+    private final void removePointersFromTouchTargets(int pointerIdBits) {
+        TouchTarget predecessor = null;
+        TouchTarget target = mFirstTouchTarget;
+        while (target != null) {
+            final TouchTarget next = target.next;
+            if ((target.pointerIdBits & pointerIdBits) != 0) {
+                target.pointerIdBits &= ~pointerIdBits;
+                if (target.pointerIdBits == 0) {
+                    if (predecessor == null) {
+                        mFirstTouchTarget = next;
+                    } else {
+                        predecessor.next = next;
+                    }
+                    target.recycle();
+                    target = next;
+                    continue;
+                }
+            }
+            predecessor = target;
+            target = next;
+        }
+    }
+
+    /* Returns true if a child view contains the specified point when transformed
+     * into its coordinate space.
+     * Child must not be null. */
+    private final boolean isTransformedTouchPointInView(float x, float y, View child) {
+        float localX = x + mScrollX - child.mLeft;
+        float localY = y + mScrollY - child.mTop;
+        if (! child.hasIdentityMatrix() && mAttachInfo != null) {
             final float[] localXY = mAttachInfo.mTmpTransformLocation;
             localXY[0] = localX;
             localXY[1] = localY;
@@ -1023,224 +1129,215 @@
             localX = localXY[0];
             localY = localXY[1];
         }
-        if (localX >= 0 && localY >= 0 && localX < (child.mRight - child.mLeft) &&
-                localY < (child.mBottom - child.mTop)) {
-            // It would be safer to clone the event here but we don't for performance.
-            // There are many subtle interactions in touch event dispatch; change at your own risk.
-            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
-            ev.offsetLocation(localX - x, localY - y);
-            if (child.dispatchTouchEvent(ev)) {
-                return true;
-            } else {
-                ev.offsetLocation(x - localX, y - localY);
-                return false;
-            }
-        }
-        return false;
+        return child.pointInView(localX, localY);
     }
 
-    private boolean dispatchSplitTouchEvent(MotionEvent ev) {
-        final SplitMotionTargets targets = mSplitMotionTargets;
-        final int action = ev.getAction();
-        final int maskedAction = ev.getActionMasked();
-        float xf = ev.getX();
-        float yf = ev.getY();
-        float scrolledXFloat = xf + mScrollX;
-        float scrolledYFloat = yf + mScrollY;
+    /* Transforms a motion event into the coordinate space of a particular child view,
+     * filters out irrelevant pointer ids, and overrides its action if necessary.
+     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead. */
+    private final boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
+            View child, int desiredPointerIdBits) {
+        final boolean handled;
 
-        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
+        // Canceling motions is a special case.  We don't need to perform any transformations
+        // or filtering.  The important part is the action, not the contents.
+        final int oldAction = event.getAction();
+        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
+            event.setAction(MotionEvent.ACTION_CANCEL);
+            if (child == null) {
+                handled = super.dispatchTouchEvent(event);
+            } else {
+                handled = child.dispatchTouchEvent(event);
+            }
+            event.setAction(oldAction);
+            return handled;
+        }
 
-        if (maskedAction == MotionEvent.ACTION_DOWN ||
-                maskedAction == MotionEvent.ACTION_POINTER_DOWN) {
-            final int actionIndex = ev.getActionIndex();
-            final int actionId = ev.getPointerId(actionIndex);
+        // Calculate the number of pointers to deliver.
+        final int oldPointerCount = event.getPointerCount();
+        int newPointerCount = 0;
+        if (desiredPointerIdBits == TouchTarget.ALL_POINTER_IDS) {
+            newPointerCount = oldPointerCount;
+        } else {
+            for (int i = 0; i < oldPointerCount; i++) {
+                final int pointerId = event.getPointerId(i);
+                final int pointerIdBit = 1 << pointerId;
+                if ((pointerIdBit & desiredPointerIdBits) != 0) {
+                    newPointerCount += 1;
+                }
+            }
+        }
 
-            // Clear out any current target for this ID.
-            // XXX: We should probably send an ACTION_UP to the current
-            // target if present.
-            targets.removeById(actionId);
+        // If for some reason we ended up in an inconsistent state where it looks like we
+        // might produce a motion event with no pointers in it, then drop the event.
+        if (newPointerCount == 0) {
+            return false;
+        }
 
-            // If we're disallowing intercept or if we're allowing and we didn't
-            // intercept
-            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
-                // reset this event's action (just to protect ourselves)
-                ev.setAction(action);
-                // We know we want to dispatch the event down, try to find a child
-                // who can handle it, start with the front-most child.
-                final long downTime = ev.getEventTime();
-                final View[] children = mChildren;
-                final int count = mChildrenCount;
-                for (int i = count - 1; i >= 0; i--) {
-                    final View child = children[i];
-                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
-                            || child.getAnimation() != null) {
-                        final MotionEvent childEvent =
-                                targets.filterMotionEventForChild(ev, child, downTime);
-                        if (childEvent != null) {
-                            try {
-                                final int childActionIndex = childEvent.findPointerIndex(actionId);
-                                if (dispatchTouchEventIfInView(child, childEvent,
-                                        childActionIndex)) {
-                                    targets.add(actionId, child, downTime);
+        // If the number of pointers is the same and we don't need to perform any fancy
+        // irreversible transformations, then we can reuse the motion event for this
+        // dispatch as long as we are careful to revert any changes we make.
+        final boolean reuse = newPointerCount == oldPointerCount
+                && (child == null || child.hasIdentityMatrix());
+        if (reuse) {
+            if (child == null) {
+                handled = super.dispatchTouchEvent(event);
+            } else {
+                final float offsetX = mScrollX - child.mLeft;
+                final float offsetY = mScrollY - child.mTop;
+                event.offsetLocation(offsetX, offsetY);
 
-                                    return true;
-                                }
-                            } finally {
-                                childEvent.recycle();
+                handled = child.dispatchTouchEvent(event);
+
+                event.offsetLocation(-offsetX, -offsetY);
+            }
+            return handled;
+        }
+
+        // Make a copy of the event.
+        // If the number of pointers is different, then we need to filter out irrelevant pointers
+        // as we make a copy of the motion event.
+        MotionEvent transformedEvent;
+        if (newPointerCount == oldPointerCount) {
+            transformedEvent = MotionEvent.obtain(event);
+        } else {
+            growTmpPointerArrays(newPointerCount);
+            final int[] newPointerIndexMap = mTmpPointerIndexMap;
+            final int[] newPointerIds = mTmpPointerIds;
+            final MotionEvent.PointerCoords[] newPointerCoords = mTmpPointerCoords;
+
+            int newPointerIndex = 0;
+            int oldPointerIndex = 0;
+            while (newPointerIndex < newPointerCount) {
+                final int pointerId = event.getPointerId(oldPointerIndex);
+                final int pointerIdBits = 1 << pointerId;
+                if ((pointerIdBits & desiredPointerIdBits) != 0) {
+                    newPointerIndexMap[newPointerIndex] = oldPointerIndex;
+                    newPointerIds[newPointerIndex] = pointerId;
+                    if (newPointerCoords[newPointerIndex] == null) {
+                        newPointerCoords[newPointerIndex] = new MotionEvent.PointerCoords();
+                    }
+
+                    newPointerIndex += 1;
+                }
+                oldPointerIndex += 1;
+            }
+
+            final int newAction;
+            if (cancel) {
+                newAction = MotionEvent.ACTION_CANCEL;
+            } else {
+                final int oldMaskedAction = oldAction & MotionEvent.ACTION_MASK;
+                if (oldMaskedAction == MotionEvent.ACTION_POINTER_DOWN
+                        || oldMaskedAction == MotionEvent.ACTION_POINTER_UP) {
+                    final int changedPointerId = event.getPointerId(
+                            (oldAction & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                                    >> MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+                    final int changedPointerIdBits = 1 << changedPointerId;
+                    if ((changedPointerIdBits & desiredPointerIdBits) != 0) {
+                        if (newPointerCount == 1) {
+                            // The first/last pointer went down/up.
+                            newAction = oldMaskedAction == MotionEvent.ACTION_POINTER_DOWN
+                                    ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP;
+                        } else {
+                            // A secondary pointer went down/up.
+                            int newChangedPointerIndex = 0;
+                            while (newPointerIds[newChangedPointerIndex] != changedPointerId) {
+                                newChangedPointerIndex += 1;
                             }
+                            newAction = oldMaskedAction | (newChangedPointerIndex
+                                    << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
                         }
+                    } else {
+                        // An unrelated pointer changed.
+                        newAction = MotionEvent.ACTION_MOVE;
+                    }
+                } else {
+                    // Simple up/down/cancel/move motion action.
+                    newAction = oldMaskedAction;
+                }
+            }
+
+            transformedEvent = null;
+            final int historySize = event.getHistorySize();
+            for (int historyIndex = 0; historyIndex <= historySize; historyIndex++) {
+                for (newPointerIndex = 0; newPointerIndex < newPointerCount; newPointerIndex++) {
+                    final MotionEvent.PointerCoords c = newPointerCoords[newPointerIndex];
+                    oldPointerIndex = newPointerIndexMap[newPointerIndex];
+                    if (historyIndex != historySize) {
+                        event.getHistoricalPointerCoords(oldPointerIndex, historyIndex, c);
+                    } else {
+                        event.getPointerCoords(oldPointerIndex, c);
                     }
                 }
 
-                // Didn't find a new target. Do we have a "primary" target to send to?
-                final SplitMotionTargets.TargetInfo primaryTargetInfo = targets.getPrimaryTarget();
-                if (primaryTargetInfo != null) {
-                    final View primaryTarget = primaryTargetInfo.view;
-                    final MotionEvent childEvent = targets.filterMotionEventForChild(ev,
-                            primaryTarget, primaryTargetInfo.downTime);
-                    if (childEvent != null) {
-                        try {
-                            // Calculate the offset point into the target's local coordinates
-                            float xc = scrolledXFloat - (float) primaryTarget.mLeft;
-                            float yc = scrolledYFloat - (float) primaryTarget.mTop;
-                            if (!primaryTarget.hasIdentityMatrix() && mAttachInfo != null) {
-                                // non-identity matrix: transform the point into the view's
-                                // coordinates
-                                final float[] localXY = mAttachInfo.mTmpTransformLocation;
-                                localXY[0] = xc;
-                                localXY[1] = yc;
-                                primaryTarget.getInverseMatrix().mapPoints(localXY);
-                                xc = localXY[0];
-                                yc = localXY[1];
-                            }
-                            childEvent.setLocation(xc, yc);
-                            if (primaryTarget.dispatchTouchEvent(childEvent)) {
-                                targets.add(actionId, primaryTarget, primaryTargetInfo.downTime);
-                                return true;
-                            }
-                        } finally {
-                            childEvent.recycle();
-                        }
-                    }
+                final long eventTime;
+                if (historyIndex != historySize) {
+                    eventTime = event.getHistoricalEventTime(historyIndex);
+                } else {
+                    eventTime = event.getEventTime();
+                }
+
+                if (transformedEvent == null) {
+                    transformedEvent = MotionEvent.obtain(
+                            event.getDownTime(), eventTime, newAction,
+                            newPointerCount, newPointerIds, newPointerCoords,
+                            event.getMetaState(), event.getXPrecision(), event.getYPrecision(),
+                            event.getDeviceId(), event.getEdgeFlags(), event.getSource(),
+                            event.getFlags());
+                } else {
+                    transformedEvent.addBatch(eventTime, newPointerCoords, 0);
                 }
             }
         }
 
-        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
-                (action == MotionEvent.ACTION_CANCEL);
-
-        if (isUpOrCancel) {
-            // Note, we've already copied the previous state to our local
-            // variable, so this takes effect on the next event
-            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
-        }
-
-        if (targets.isEmpty()) {
-            // We don't have any targets, this means we're handling the
-            // event as a regular view.
-            ev.setLocation(xf, yf);
-            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
-                ev.setAction(MotionEvent.ACTION_CANCEL);
-                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
-            }
-            return super.dispatchTouchEvent(ev);
-        }
-
-        // if we have targets, see if we're allowed to and want to intercept their
-        // events
-        int uniqueTargetCount = targets.getUniqueTargetCount();
-        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
-            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
-
-            for (int uniqueIndex = 0; uniqueIndex < uniqueTargetCount; uniqueIndex++) {
-                final View target = targets.getUniqueTargetAt(uniqueIndex).view;
-
-                // Calculate the offset point into the target's local coordinates
-                float xc = scrolledXFloat - (float) target.mLeft;
-                float yc = scrolledYFloat - (float) target.mTop;
-                if (!target.hasIdentityMatrix() && mAttachInfo != null) {
-                    // non-identity matrix: transform the point into the view's coordinates
-                    final float[] localXY = mAttachInfo.mTmpTransformLocation;
-                    localXY[0] = xc;
-                    localXY[1] = yc;
-                    target.getInverseMatrix().mapPoints(localXY);
-                    xc = localXY[0];
-                    yc = localXY[1];
-                }
-
-                ev.setAction(MotionEvent.ACTION_CANCEL);
-                ev.setLocation(xc, yc);
-                if (!target.dispatchTouchEvent(ev)) {
-                    // target didn't handle ACTION_CANCEL. not much we can do
-                    // but they should have.
-                }
-            }
-            targets.clear();
-            // Don't dispatch this event to our own view, because we already
-            // saw it when intercepting; we just want to give the following
-            // event to the normal onTouchEvent().
-            return true;
-        }
-
-        boolean handled = false;
-        for (int uniqueIndex = 0; uniqueIndex < uniqueTargetCount; uniqueIndex++) {
-            final SplitMotionTargets.TargetInfo targetInfo = targets.getUniqueTargetAt(uniqueIndex);
-            final View target = targetInfo.view;
-
-            final MotionEvent targetEvent =
-                    targets.filterMotionEventForChild(ev, target, targetInfo.downTime);
-            if (targetEvent == null) {
-                continue;
+        // Perform any necessary transformations and dispatch.
+        if (child == null) {
+            handled = super.dispatchTouchEvent(transformedEvent);
+        } else {
+            final float offsetX = mScrollX - child.mLeft;
+            final float offsetY = mScrollY - child.mTop;
+            transformedEvent.offsetLocation(offsetX, offsetY);
+            if (! child.hasIdentityMatrix()) {
+                transformedEvent.transform(child.getInverseMatrix());
             }
 
-            try {
-                // Calculate the offset point into the target's local coordinates
-                xf = targetEvent.getX();
-                yf = targetEvent.getY();
-                scrolledXFloat = xf + mScrollX;
-                scrolledYFloat = yf + mScrollY;
-                float xc = scrolledXFloat - (float) target.mLeft;
-                float yc = scrolledYFloat - (float) target.mTop;
-                if (!target.hasIdentityMatrix() && mAttachInfo != null) {
-                    // non-identity matrix: transform the point into the view's coordinates
-                    final float[] localXY = mAttachInfo.mTmpTransformLocation;
-                    localXY[0] = xc;
-                    localXY[1] = yc;
-                    target.getInverseMatrix().mapPoints(localXY);
-                    xc = localXY[0];
-                    yc = localXY[1];
-                }
-
-                // finally offset the event to the target's coordinate system and
-                // dispatch the event.
-                targetEvent.setLocation(xc, yc);
-
-                if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
-                    targetEvent.setAction(MotionEvent.ACTION_CANCEL);
-                    target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
-                    targets.removeView(target);
-                    uniqueIndex--;
-                    uniqueTargetCount--;
-                }
-
-                handled |= target.dispatchTouchEvent(targetEvent);
-            } finally {
-                targetEvent.recycle();
-            }
+            handled = child.dispatchTouchEvent(transformedEvent);
         }
 
-        if (maskedAction == MotionEvent.ACTION_POINTER_UP) {
-            final int removeId = ev.getPointerId(ev.getActionIndex());
-            targets.removeById(removeId);
-        }
-
-        if (isUpOrCancel) {
-            targets.clear();
-        }
-
+        // Done.
+        transformedEvent.recycle();
         return handled;
     }
 
+    /* Enlarge the temporary pointer arrays for splitting pointers.
+     * May discard contents (but keeps PointerCoords objects to avoid reallocating them). */
+    private final void growTmpPointerArrays(int desiredCapacity) {
+        final MotionEvent.PointerCoords[] oldTmpPointerCoords = mTmpPointerCoords;
+        int capacity;
+        if (oldTmpPointerCoords != null) {
+            capacity = oldTmpPointerCoords.length;
+            if (desiredCapacity <= capacity) {
+                return;
+            }
+        } else {
+            capacity = 4;
+        }
+
+        while (capacity < desiredCapacity) {
+            capacity *= 2;
+        }
+
+        mTmpPointerIndexMap = new int[capacity];
+        mTmpPointerIds = new int[capacity];
+        mTmpPointerCoords = new MotionEvent.PointerCoords[capacity];
+
+        if (oldTmpPointerCoords != null) {
+            System.arraycopy(oldTmpPointerCoords, 0, mTmpPointerCoords, 0,
+                    oldTmpPointerCoords.length);
+        }
+    }
+
     /**
      * Enable or disable the splitting of MotionEvents to multiple children during touch event
      * dispatch. This behavior is disabled by default.
@@ -1262,7 +1359,6 @@
             mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;
         } else {
             mGroupFlags &= ~FLAG_SPLIT_MOTION_EVENTS;
-            mSplitMotionTargets = null;
         }
     }
 
@@ -1473,19 +1569,12 @@
      */
     @Override
     void dispatchDetachedFromWindow() {
-        // If we still have a motion target, we are still in the process of
+        // If we still have a touch target, we are still in the process of
         // dispatching motion events to a child; we need to get rid of that
         // child to avoid dispatching events to it after the window is torn
         // down. To make sure we keep the child in a consistent state, we
         // first send it an ACTION_CANCEL motion event.
-        if (mMotionTarget != null) {
-            final long now = SystemClock.uptimeMillis();
-            final MotionEvent event = MotionEvent.obtain(now, now,
-                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
-            mMotionTarget.dispatchTouchEvent(event);
-            event.recycle();
-            mMotionTarget = null;
-        }
+        cancelAndClearTouchTargets(null);
 
         final int count = mChildrenCount;
         final View[] children = mChildren;
@@ -4287,290 +4376,57 @@
         }
     }
 
-    private static class SplitMotionTargets {
-        private SparseArray<View> mTargets;
-        private TargetInfo[] mUniqueTargets;
-        private int mUniqueTargetCount;
-        private MotionEvent.PointerCoords[] mPointerCoords;
-        private int[] mPointerIds;
+    /* Describes a touched view and the ids of the pointers that it has captured.
+     *
+     * This code assumes that pointer ids are always in the range 0..31 such that
+     * it can use a bitfield to track which pointer ids are present.
+     * As it happens, the lower layers of the input dispatch pipeline also use the
+     * same trick so the assumption should be safe here...
+     */
+    private static final class TouchTarget {
+        private static final int MAX_RECYCLED = 32;
+        private static final Object sRecycleLock = new Object();
+        private static TouchTarget sRecycleBin;
+        private static int sRecycledCount;
 
-        private static final int INITIAL_UNIQUE_MOTION_TARGETS_SIZE = 5;
-        private static final int INITIAL_BUCKET_SIZE = 5;
+        public static final int ALL_POINTER_IDS = -1; // all ones
 
-        public SplitMotionTargets() {
-            mTargets = new SparseArray<View>();
-            mUniqueTargets = new TargetInfo[INITIAL_UNIQUE_MOTION_TARGETS_SIZE];
-            mPointerIds = new int[INITIAL_BUCKET_SIZE];
-            mPointerCoords = new MotionEvent.PointerCoords[INITIAL_BUCKET_SIZE];
-            for (int i = 0; i < INITIAL_BUCKET_SIZE; i++) {
-                mPointerCoords[i] = new MotionEvent.PointerCoords();
-            }
+        // The touched child view.
+        public View child;
+
+        // The combined bit mask of pointer ids for all pointers captured by the target.
+        public int pointerIdBits;
+
+        // The next target in the target list.
+        public TouchTarget next;
+
+        private TouchTarget() {
         }
 
-        public void clear() {
-            mTargets.clear();
-            final int count = mUniqueTargetCount;
-            for (int i = 0; i < count; i++) {
-                mUniqueTargets[i].recycle();
-                mUniqueTargets[i] = null;
-            }
-            mUniqueTargetCount = 0;
-        }
-
-        public void add(int pointerId, View target, long downTime) {
-            mTargets.put(pointerId, target);
-
-            final int uniqueCount = mUniqueTargetCount;
-            boolean addUnique = true;
-            for (int i = 0; i < uniqueCount; i++) {
-                if (mUniqueTargets[i].view == target) {
-                    addUnique = false;
-                }
-            }
-            if (addUnique) {
-                if (mUniqueTargets.length == uniqueCount) {
-                    TargetInfo[] newTargets =
-                        new TargetInfo[uniqueCount + INITIAL_UNIQUE_MOTION_TARGETS_SIZE];
-                    System.arraycopy(mUniqueTargets, 0, newTargets, 0, uniqueCount);
-                    mUniqueTargets = newTargets;
-                }
-                mUniqueTargets[uniqueCount] = TargetInfo.obtain(target, downTime);
-                mUniqueTargetCount++;
-            }
-        }
-
-        public int getIdCount() {
-            return mTargets.size();
-        }
-
-        public int getUniqueTargetCount() {
-            return mUniqueTargetCount;
-        }
-
-        public TargetInfo getUniqueTargetAt(int index) {
-            return mUniqueTargets[index];
-        }
-
-        public View get(int id) {
-            return mTargets.get(id);
-        }
-
-        public int indexOfTarget(View target) {
-            return mTargets.indexOfValue(target);
-        }
-
-        public View targetAt(int index) {
-            return mTargets.valueAt(index);
-        }
-
-        public TargetInfo getPrimaryTarget() {
-            if (!isEmpty()) {
-                // Find the longest-lived target
-                long firstTime = Long.MAX_VALUE;
-                int firstIndex = 0;
-                final int uniqueCount = mUniqueTargetCount;
-                for (int i = 0; i < uniqueCount; i++) {
-                    TargetInfo info = mUniqueTargets[i];
-                    if (info.downTime < firstTime) {
-                        firstTime = info.downTime;
-                        firstIndex = i;
-                    }
-                }
-                return mUniqueTargets[firstIndex];
-            }
-            return null;
-        }
-
-        public boolean isEmpty() {
-            return mUniqueTargetCount == 0;
-        }
-
-        public void removeById(int id) {
-            final int index = mTargets.indexOfKey(id);
-            removeAt(index);
-        }
-        
-        public void removeView(View view) {
-            int i = 0;
-            while (i < mTargets.size()) {
-                if (mTargets.valueAt(i) == view) {
-                    mTargets.removeAt(i);
-                } else {
-                    i++;
-                }
-            }
-            removeUnique(view);
-        }
-
-        public void removeAt(int index) {
-            if (index < 0 || index >= mTargets.size()) {
-                return;
-            }
-
-            final View removeView = mTargets.valueAt(index);
-            mTargets.removeAt(index);
-            if (mTargets.indexOfValue(removeView) < 0) {
-                removeUnique(removeView);
-            }
-        }
-
-        private void removeUnique(View removeView) {
-            TargetInfo[] unique = mUniqueTargets;
-            int uniqueCount = mUniqueTargetCount;
-            for (int i = 0; i < uniqueCount; i++) {
-                if (unique[i].view == removeView) {
-                    unique[i].recycle();
-                    unique[i] = unique[--uniqueCount];
-                    unique[uniqueCount] = null;
-                    break;
-                }
-            }
-
-            mUniqueTargetCount = uniqueCount;
-        }
-
-        /**
-         * Return a new (obtain()ed) MotionEvent containing only data for pointers that should
-         * be dispatched to child. Don't forget to recycle it!
-         */
-        public MotionEvent filterMotionEventForChild(MotionEvent ev, View child, long downTime) {
-            int action = ev.getAction();
-            final int maskedAction = action & MotionEvent.ACTION_MASK;
-
-            // Only send pointer up events if this child was the target. Drop it otherwise.
-            if (maskedAction == MotionEvent.ACTION_POINTER_UP &&
-                    get(ev.getPointerId(ev.getActionIndex())) != child) {
-                return null;
-            }
-
-            int pointerCount = 0;
-            final int idCount = getIdCount();
-            for (int i = 0; i < idCount; i++) {
-                if (targetAt(i) == child) {
-                    pointerCount++;
-                }
-            }
-
-            int actionId = -1;
-            boolean needsNewIndex = false; // True if we should fill in the action's masked index
-
-            // If we have a down event, it wasn't counted above.
-            if (maskedAction == MotionEvent.ACTION_DOWN) {
-                pointerCount++;
-                actionId = ev.getPointerId(0);
-            } else if (maskedAction == MotionEvent.ACTION_POINTER_DOWN) {
-                pointerCount++;
-
-                actionId = ev.getPointerId(ev.getActionIndex());
-
-                if (indexOfTarget(child) < 0) {
-                    // The new action should be ACTION_DOWN if this child isn't currently getting
-                    // any events.
-                    action = MotionEvent.ACTION_DOWN;
-                } else {
-                    // Fill in the index portion of the action later.
-                    needsNewIndex = true;
-                }
-            } else if (maskedAction == MotionEvent.ACTION_POINTER_UP) {
-                actionId = ev.getPointerId(ev.getActionIndex());
-                if (pointerCount == 1) {
-                    // The new action should be ACTION_UP if there's only one pointer left for
-                    // this target.
-                    action = MotionEvent.ACTION_UP;
-                } else {
-                    // Fill in the index portion of the action later.
-                    needsNewIndex = true;
-                }
-            }
-
-            if (pointerCount == 0) {
-                return null;
-            }
-
-            // Fill the buckets with pointer data!
-            final int eventPointerCount = ev.getPointerCount();
-            int bucketIndex = 0;
-            int newActionIndex = -1;
-            for (int evp = 0; evp < eventPointerCount; evp++) {
-                final int id = ev.getPointerId(evp);
-
-                // Add this pointer to the bucket if it is new or targeted at child
-                if (id == actionId || get(id) == child) {
-                    // Expand scratch arrays if needed
-                    if (mPointerCoords.length <= bucketIndex) {
-                        int[] pointerIds = new int[pointerCount];
-                        MotionEvent.PointerCoords[] pointerCoords =
-                                new MotionEvent.PointerCoords[pointerCount];
-                        for (int i = mPointerCoords.length; i < pointerCoords.length; i++) {
-                            pointerCoords[i] = new MotionEvent.PointerCoords();
-                        }
-
-                        System.arraycopy(mPointerCoords, 0,
-                                pointerCoords, 0, mPointerCoords.length);
-                        System.arraycopy(mPointerIds, 0, pointerIds, 0, mPointerIds.length);
-
-                        mPointerCoords = pointerCoords;
-                        mPointerIds = pointerIds;
-                    }
-
-                    mPointerIds[bucketIndex] = id;
-                    ev.getPointerCoords(evp, mPointerCoords[bucketIndex]);
-
-                    if (needsNewIndex && id == actionId) {
-                        newActionIndex = bucketIndex;
-                    }
-
-                    bucketIndex++;
-                }
-            }
-
-            // Encode the new action index if we have one
-            if (newActionIndex >= 0) {
-                action = (action & MotionEvent.ACTION_MASK) |
-                        (newActionIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
-            }
-
-            return MotionEvent.obtain(downTime, ev.getEventTime(),
-                    action, pointerCount, mPointerIds, mPointerCoords, ev.getMetaState(),
-                    ev.getXPrecision(), ev.getYPrecision(), ev.getDeviceId(), ev.getEdgeFlags(),
-                    ev.getSource(), ev.getFlags());
-        }
-
-        static class TargetInfo {
-            public View view;
-            public long downTime;
-
-            private TargetInfo mNextRecycled;
-
-            private static TargetInfo sRecycleBin;
-            private static int sRecycledCount;
-
-            private static int MAX_RECYCLED = 15;
-
-            private TargetInfo() {
-            }
-
-            public static TargetInfo obtain(View v, long time) {
-                TargetInfo info;
+        public static TouchTarget obtain(View child, int pointerIdBits) {
+            final TouchTarget target;
+            synchronized (sRecycleLock) {
                 if (sRecycleBin == null) {
-                    info = new TargetInfo();
+                    target = new TouchTarget();
                 } else {
-                    info = sRecycleBin;
-                    sRecycleBin = info.mNextRecycled;
-                    sRecycledCount--;
+                    target = sRecycleBin;
+                    sRecycleBin = target.next;
+                     sRecycledCount--;
+                    target.next = null;
                 }
-                info.view = v;
-                info.downTime = time;
-                return info;
             }
+            target.child = child;
+            target.pointerIdBits = pointerIdBits;
+            return target;
+        }
 
-            public void recycle() {
-                if (sRecycledCount >= MAX_RECYCLED) {
-                    return;
+        public void recycle() {
+            synchronized (sRecycleLock) {
+                if (sRecycledCount < MAX_RECYCLED) {
+                    next = sRecycleBin;
+                    sRecycleBin = this;
+                    sRecycledCount += 1;
                 }
-                mNextRecycled = sRecycleBin;
-                sRecycleBin = this;
-                sRecycledCount++;
             }
         }
     }
diff --git a/core/jni/android/graphics/Matrix.cpp b/core/jni/android/graphics/Matrix.cpp
index b782766..cafceab 100644
--- a/core/jni/android/graphics/Matrix.cpp
+++ b/core/jni/android/graphics/Matrix.cpp
@@ -27,6 +27,8 @@
 #include "SkMatrix.h"
 #include "SkTemplates.h"
 
+#include "Matrix.h"
+
 namespace android {
 
 class SkMatrixGlue {
@@ -403,10 +405,20 @@
     {"native_equals", "(II)Z", (void*) SkMatrixGlue::equals}
 };
 
+static jfieldID sNativeInstanceField;
+
 int register_android_graphics_Matrix(JNIEnv* env) {
     int result = AndroidRuntime::registerNativeMethods(env, "android/graphics/Matrix", methods,
         sizeof(methods) / sizeof(methods[0]));
+
+    jclass clazz = env->FindClass("android/graphics/Matrix");
+    sNativeInstanceField = env->GetFieldID(clazz, "native_instance", "I");
+
     return result;
 }
 
+SkMatrix* android_graphics_Matrix_getSkMatrix(JNIEnv* env, jobject matrixObj) {
+    return reinterpret_cast<SkMatrix*>(env->GetIntField(matrixObj, sNativeInstanceField));
+}
+
 }
diff --git a/core/jni/android/graphics/Matrix.h b/core/jni/android/graphics/Matrix.h
new file mode 100644
index 0000000..31edf88
--- /dev/null
+++ b/core/jni/android/graphics/Matrix.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef _ANDROID_GRAPHICS_MATRIX_H
+#define _ANDROID_GRAPHICS_MATRIX_H
+
+#include "jni.h"
+#include "SkMatrix.h"
+
+namespace android {
+
+/* Gets the underlying SkMatrix from a Matrix object. */
+extern SkMatrix* android_graphics_Matrix_getSkMatrix(JNIEnv* env, jobject matrixObj);
+
+} // namespace android
+
+#endif // _ANDROID_GRAPHICS_MATRIX_H
diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp
index 93fd54f..537ac72 100644
--- a/core/jni/android_view_MotionEvent.cpp
+++ b/core/jni/android_view_MotionEvent.cpp
@@ -22,10 +22,26 @@
 #include <utils/Log.h>
 #include <ui/Input.h>
 #include "android_view_MotionEvent.h"
+#include "android/graphics/Matrix.h"
+
+#include <math.h>
+#include "SkMatrix.h"
+#include "SkScalar.h"
 
 // Number of float items per entry in a DVM sample data array
 #define NUM_SAMPLE_DATA 9
 
+#define SAMPLE_X 0
+#define SAMPLE_Y 1
+#define SAMPLE_PRESSURE 2
+#define SAMPLE_SIZE 3
+#define SAMPLE_TOUCH_MAJOR 4
+#define SAMPLE_TOUCH_MINOR 5
+#define SAMPLE_TOOL_MAJOR 6
+#define SAMPLE_TOOL_MINOR 7
+#define SAMPLE_ORIENTATION 8
+
+
 namespace android {
 
 // ----------------------------------------------------------------------------
@@ -238,8 +254,87 @@
     }
 }
 
+static inline float transformAngle(const SkMatrix* matrix, float angleRadians) {
+    // Construct and transform a vector oriented at the specified clockwise angle from vertical.
+    // Coordinate system: down is increasing Y, right is increasing X.
+    SkPoint vector;
+    vector.fX = SkFloatToScalar(sinf(angleRadians));
+    vector.fY = SkFloatToScalar(- cosf(angleRadians));
+    matrix->mapVectors(& vector, 1);
+
+    // Derive the transformed vector's clockwise angle from vertical.
+    float result = atan2f(SkScalarToFloat(vector.fX), SkScalarToFloat(- vector.fY));
+    if (result < - M_PI_2) {
+        result += M_PI;
+    } else if (result > M_PI_2) {
+        result -= M_PI;
+    }
+    return result;
+}
+
+static void android_view_MotionEvent_nativeTransform(JNIEnv* env,
+        jobject eventObj, jobject matrixObj) {
+    SkMatrix* matrix = android_graphics_Matrix_getSkMatrix(env, matrixObj);
+
+    jfloat oldXOffset = env->GetFloatField(eventObj, gMotionEventClassInfo.mXOffset);
+    jfloat oldYOffset = env->GetFloatField(eventObj, gMotionEventClassInfo.mYOffset);
+    jint numPointers = env->GetIntField(eventObj, gMotionEventClassInfo.mNumPointers);
+    jint numSamples = env->GetIntField(eventObj, gMotionEventClassInfo.mNumSamples);
+    jfloatArray dataSampleArray = jfloatArray(env->GetObjectField(eventObj,
+            gMotionEventClassInfo.mDataSamples));
+    jfloat* dataSamples = (jfloat*)env->GetPrimitiveArrayCritical(dataSampleArray, NULL);
+
+    // The tricky part of this implementation is to preserve the value of
+    // rawX and rawY.  So we apply the transformation to the first point
+    // then derive an appropriate new X/Y offset that will preserve rawX and rawY.
+    SkPoint point;
+    jfloat rawX = dataSamples[SAMPLE_X];
+    jfloat rawY = dataSamples[SAMPLE_Y];
+    matrix->mapXY(SkFloatToScalar(rawX + oldXOffset), SkFloatToScalar(rawY + oldYOffset),
+            & point);
+    jfloat newX = SkScalarToFloat(point.fX);
+    jfloat newY = SkScalarToFloat(point.fY);
+    jfloat newXOffset = newX - rawX;
+    jfloat newYOffset = newY - rawY;
+
+    dataSamples[SAMPLE_ORIENTATION] = transformAngle(matrix, dataSamples[SAMPLE_ORIENTATION]);
+
+    // Apply the transformation to all samples.
+    jfloat* currentDataSample = dataSamples;
+    jfloat* endDataSample = dataSamples + numPointers * numSamples * NUM_SAMPLE_DATA;
+    for (;;) {
+        currentDataSample += NUM_SAMPLE_DATA;
+        if (currentDataSample == endDataSample) {
+            break;
+        }
+
+        jfloat x = currentDataSample[SAMPLE_X] + oldXOffset;
+        jfloat y = currentDataSample[SAMPLE_Y] + oldYOffset;
+        matrix->mapXY(SkFloatToScalar(x), SkFloatToScalar(y), & point);
+        currentDataSample[SAMPLE_X] = SkScalarToFloat(point.fX) - newXOffset;
+        currentDataSample[SAMPLE_Y] = SkScalarToFloat(point.fY) - newYOffset;
+
+        currentDataSample[SAMPLE_ORIENTATION] = transformAngle(matrix,
+                currentDataSample[SAMPLE_ORIENTATION]);
+    }
+
+    env->ReleasePrimitiveArrayCritical(dataSampleArray, dataSamples, 0);
+
+    env->SetFloatField(eventObj, gMotionEventClassInfo.mXOffset, newXOffset);
+    env->SetFloatField(eventObj, gMotionEventClassInfo.mYOffset, newYOffset);
+
+    env->DeleteLocalRef(dataSampleArray);
+}
+
 // ----------------------------------------------------------------------------
 
+static JNINativeMethod gMotionEventMethods[] = {
+    /* name, signature, funcPtr */
+    { "nativeTransform",
+            "(Landroid/graphics/Matrix;)V",
+            (void*)android_view_MotionEvent_nativeTransform },
+};
+
 #define FIND_CLASS(var, className) \
         var = env->FindClass(className); \
         LOG_FATAL_IF(! var, "Unable to find class " className); \
@@ -258,6 +353,10 @@
         LOG_FATAL_IF(! var, "Unable to find field " fieldName);
 
 int register_android_view_MotionEvent(JNIEnv* env) {
+    int res = jniRegisterNativeMethods(env, "android/view/MotionEvent",
+            gMotionEventMethods, NELEM(gMotionEventMethods));
+    LOG_FATAL_IF(res < 0, "Unable to register native methods.");
+
     FIND_CLASS(gMotionEventClassInfo.clazz, "android/view/MotionEvent");
 
     GET_STATIC_METHOD_ID(gMotionEventClassInfo.obtain, gMotionEventClassInfo.clazz,