Merge "Add support for mouse hover and scroll wheel."
diff --git a/api/current.xml b/api/current.xml
index a1a98d4..90b46ff 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -217976,6 +217976,17 @@
  visibility="public"
 >
 </field>
+<field name="ACTION_SCROLL"
+ type="int"
+ transient="false"
+ volatile="false"
+ value="8"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
 <field name="ACTION_UP"
  type="int"
  transient="false"
@@ -224299,6 +224310,19 @@
 <parameter name="l" type="android.view.View.OnFocusChangeListener">
 </parameter>
 </method>
+<method name="setOnGenericMotionListener"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="l" type="android.view.View.OnGenericMotionListener">
+</parameter>
+</method>
 <method name="setOnKeyListener"
  return="void"
  abstract="false"
@@ -226002,6 +226026,29 @@
 </parameter>
 </method>
 </interface>
+<interface name="View.OnGenericMotionListener"
+ abstract="true"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<method name="onGenericMotion"
+ return="boolean"
+ abstract="true"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="v" type="android.view.View">
+</parameter>
+<parameter name="event" type="android.view.MotionEvent">
+</parameter>
+</method>
+</interface>
 <interface name="View.OnKeyListener"
  abstract="true"
  static="true"
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 67e4806..9e72c1b 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -2122,24 +2122,21 @@
      * Called when a generic motion event was not handled by any of the
      * views inside of the activity.
      * <p>
-     * Generic motion events are dispatched to the focused view to describe
-     * the motions of input devices such as joysticks.  The
+     * Generic motion events describe joystick movements, mouse hovers, track pad
+     * touches, scroll wheel movements and other input events.  The
      * {@link MotionEvent#getSource() source} of the motion event specifies
      * the class of input that was received.  Implementations of this method
      * must examine the bits in the source before processing the event.
      * The following code example shows how this is done.
+     * </p><p>
+     * Generic motion events with source class
+     * {@link android.view.InputDevice#SOURCE_CLASS_POINTER}
+     * are delivered to the view under the pointer.  All other generic motion events are
+     * delivered to the focused view.
+     * </p><p>
+     * See {@link View#onGenericMotionEvent(MotionEvent)} for an example of how to
+     * handle this event.
      * </p>
-     * <code>
-     * public boolean onGenericMotionEvent(MotionEvent event) {
-     *     if ((event.getSource() &amp; InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
-     *         float x = event.getX();
-     *         float y = event.getY();
-     *         // process the joystick motion
-     *         return true;
-     *     }
-     *     return super.onGenericMotionEvent(event);
-     * }
-     * </code>
      *
      * @param event The generic motion event being processed.
      *
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java
index cc4fefc3..087753b 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -627,24 +627,21 @@
      * Called when a generic motion event was not handled by any of the
      * views inside of the dialog.
      * <p>
-     * Generic motion events are dispatched to the focused view to describe
-     * the motions of input devices such as joysticks.  The
+     * Generic motion events describe joystick movements, mouse hovers, track pad
+     * touches, scroll wheel movements and other input events.  The
      * {@link MotionEvent#getSource() source} of the motion event specifies
      * the class of input that was received.  Implementations of this method
      * must examine the bits in the source before processing the event.
      * The following code example shows how this is done.
+     * </p><p>
+     * Generic motion events with source class
+     * {@link android.view.InputDevice#SOURCE_CLASS_POINTER}
+     * are delivered to the view under the pointer.  All other generic motion events are
+     * delivered to the focused view.
+     * </p><p>
+     * See {@link View#onGenericMotionEvent(MotionEvent)} for an example of how to
+     * handle this event.
      * </p>
-     * <code>
-     * public boolean onGenericMotionEvent(MotionEvent event) {
-     *     if ((event.getSource() &amp; InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
-     *         float x = event.getX();
-     *         float y = event.getY();
-     *         // process the joystick motion
-     *         return true;
-     *     }
-     *     return super.onGenericMotionEvent(event);
-     * }
-     * </code>
      *
      * @param event The generic motion event being processed.
      *
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index dd39714..20661d7 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -460,16 +460,17 @@
         }
         
         private void dispatchPointer(MotionEvent event) {
-            synchronized (mLock) {
-                if (event.getAction() == MotionEvent.ACTION_MOVE) {
-                    mPendingMove = event;
-                } else {
-                    mPendingMove = null;
+            if (event.isTouchEvent()) {
+                synchronized (mLock) {
+                    if (event.getAction() == MotionEvent.ACTION_MOVE) {
+                        mPendingMove = event;
+                    } else {
+                        mPendingMove = null;
+                    }
                 }
+                Message msg = mCaller.obtainMessageO(MSG_TOUCH_EVENT, event);
+                mCaller.sendMessage(msg);
             }
-
-            Message msg = mCaller.obtainMessageO(MSG_TOUCH_EVENT, event);
-            mCaller.sendMessage(msg);
         }
 
         void updateSurface(boolean forceRelayout, boolean forceReport, boolean redrawNeeded) {
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index cc37a28..a26dd04 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -40,6 +40,12 @@
  * by a motion event with {@link #ACTION_UP} or when gesture is canceled
  * with {@link #ACTION_CANCEL}.
  * </p><p>
+ * Some pointing devices such as mice may support vertical and/or horizontal scrolling.
+ * A scroll event is reported as a generic motion event with {@link #ACTION_SCROLL} that
+ * includes the relative scroll offset in the {@link #AXIS_VSCROLL} and
+ * {@link #AXIS_HSCROLL} axes.  See {@link #getAxisValue(int)} for information
+ * about retrieving these additional axes.
+ * </p><p>
  * On trackball devices with source class {@link InputDevice#SOURCE_CLASS_TRACKBALL},
  * the pointer coordinates specify relative movements as X/Y deltas.
  * A trackball gesture consists of a sequence of movements described by motion
@@ -51,6 +57,8 @@
  * The joystick axis values are normalized to a range of -1.0 to 1.0 where 0.0 corresponds
  * to the center position.  More information about the set of available axes and the
  * range of motion can be obtained using {@link InputDevice#getMotionRange}.
+ * Some common joystick axes are {@link #AXIS_X}, {@link #AXIS_Y},
+ * {@link #AXIS_HAT_X}, {@link #AXIS_HAT_Y}, {@link #AXIS_Z} and {@link #AXIS_RZ}.
  * </p><p>
  * Motion events always report movements for all pointers at once.  The number
  * of pointers only ever changes by one as individual pointers go up and down,
@@ -163,10 +171,30 @@
      * is not down (unlike {@link #ACTION_MOVE}).  The motion contains the most
      * recent point, as well as any intermediate points since the last
      * hover move event.
+     * <p>
+     * This action is not a touch event so it is delivered to
+     * {@link View#onGenericMotionEvent(MotionEvent)} rather than
+     * {@link View#onTouchEvent(MotionEvent)}.
+     * </p>
      */
     public static final int ACTION_HOVER_MOVE       = 7;
 
     /**
+     * Constant for {@link #getAction}: The motion event contains relative
+     * vertical and/or horizontal scroll offsets.  Use {@link #getAxisValue(int)}
+     * to retrieve the information from {@link #AXIS_VSCROLL} and {@link #AXIS_HSCROLL}.
+     * The pointer may or may not be down when this event is dispatched.
+     * This action is always delivered to the winder under the pointer, which
+     * may not be the window currently touched.
+     * <p>
+     * This action is not a touch event so it is delivered to
+     * {@link View#onGenericMotionEvent(MotionEvent)} rather than
+     * {@link View#onTouchEvent(MotionEvent)}.
+     * </p>
+     */
+    public static final int ACTION_SCROLL           = 8;
+
+    /**
      * Bits in the action code that represent a pointer index, used with
      * {@link #ACTION_POINTER_DOWN} and {@link #ACTION_POINTER_UP}.  Shifting
      * down by {@link #ACTION_POINTER_INDEX_SHIFT} provides the actual pointer
@@ -483,7 +511,7 @@
      * <p>
      * <ul>
      * <li>For a mouse, reports the relative movement of the vertical scroll wheel.
-     * The value is normalized to a range from -1.0 (up) to 1.0 (down).
+     * The value is normalized to a range from -1.0 (down) to 1.0 (up).
      * </ul>
      * </p><p>
      * This axis should be used to scroll views vertically.
@@ -1237,6 +1265,32 @@
     }
 
     /**
+     * Returns true if this motion event is a touch event.
+     * <p>
+     * Specifically excludes pointer events with action {@link #ACTION_HOVER_MOVE}
+     * or {@link #ACTION_SCROLL} because they are not actually touch events
+     * (the pointer is not down).
+     * </p>
+     * @return True if this motion event is a touch event.
+     * @hide
+     */
+    public final boolean isTouchEvent() {
+        if ((getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            switch (getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                case MotionEvent.ACTION_MOVE:
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_POINTER_DOWN:
+                case MotionEvent.ACTION_POINTER_UP:
+                case MotionEvent.ACTION_CANCEL:
+                case MotionEvent.ACTION_OUTSIDE:
+                    return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Gets the motion event flags.
      *
      * @see #FLAG_WINDOW_IS_OBSCURED
@@ -2174,10 +2228,14 @@
                 return "ACTION_UP";
             case ACTION_CANCEL:
                 return "ACTION_CANCEL";
+            case ACTION_OUTSIDE:
+                return "ACTION_OUTSIDE";
             case ACTION_MOVE:
                 return "ACTION_MOVE";
             case ACTION_HOVER_MOVE:
                 return "ACTION_HOVER_MOVE";
+            case ACTION_SCROLL:
+                return "ACTION_SCROLL";
         }
         int index = (action & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT;
         switch (action & ACTION_MASK) {
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 78eb2e5..01bc2df 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -57,6 +57,7 @@
 import android.util.PoolableManager;
 import android.util.Pools;
 import android.util.SparseArray;
+import android.util.TypedValue;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.View.MeasureSpec;
 import android.view.accessibility.AccessibilityEvent;
@@ -2128,6 +2129,8 @@
 
     private OnTouchListener mOnTouchListener;
 
+    private OnGenericMotionListener mOnGenericMotionListener;
+
     private OnDragListener mOnDragListener;
 
     private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;
@@ -2257,6 +2260,11 @@
     public static final int DRAG_FLAG_GLOBAL = 1;
 
     /**
+     * Vertical scroll factor cached by {@link #getVerticalScrollFactor}.
+     */
+    private float mVerticalScrollFactor;
+
+    /**
      * Position of the vertical scroll bar.
      */
     private int mVerticalScrollbarPosition;
@@ -3167,6 +3175,14 @@
     }
 
     /**
+     * Register a callback to be invoked when a generic motion event is sent to this view.
+     * @param l the generic motion listener to attach to this view
+     */
+    public void setOnGenericMotionListener(OnGenericMotionListener l) {
+        mOnGenericMotionListener = l;
+    }
+
+    /**
      * Register a drag event listener callback object for this View. The parameter is
      * an implementation of {@link android.view.View.OnDragListener}. To send a drag event to a
      * View, the system calls the
@@ -4624,16 +4640,47 @@
     }
 
     /**
-     * Pass a generic motion event down to the focused view.
+     * Dispatch a generic motion event.
+     * <p>
+     * Generic motion events with source class {@link InputDevice#SOURCE_CLASS_POINTER}
+     * are delivered to the view under the pointer.  All other generic motion events are
+     * delivered to the focused view.
+     * </p>
      *
      * @param event The motion event to be dispatched.
      * @return True if the event was handled by the view, false otherwise.
      */
     public boolean dispatchGenericMotionEvent(MotionEvent event) {
+        if (mOnGenericMotionListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
+                && mOnGenericMotionListener.onGenericMotion(this, event)) {
+            return true;
+        }
+
         return onGenericMotionEvent(event);
     }
 
     /**
+     * Dispatch a pointer event.
+     * <p>
+     * Dispatches touch related pointer events to {@link #onTouchEvent} and all
+     * other events to {@link #onGenericMotionEvent}.  This separation of concerns
+     * reinforces the invariant that {@link #onTouchEvent} is really about touches
+     * and should not be expected to handle other pointing device features.
+     * </p>
+     *
+     * @param event The motion event to be dispatched.
+     * @return True if the event was handled by the view, false otherwise.
+     * @hide
+     */
+    public final boolean dispatchPointerEvent(MotionEvent event) {
+        if (event.isTouchEvent()) {
+            return dispatchTouchEvent(event);
+        } else {
+            return dispatchGenericMotionEvent(event);
+        }
+    }
+
+    /**
      * Called when the window containing this view gains or loses window focus.
      * ViewGroups should override to route to their children.
      *
@@ -5142,20 +5189,34 @@
     /**
      * Implement this method to handle generic motion events.
      * <p>
-     * Generic motion events are dispatched to the focused view to describe
-     * the motions of input devices such as joysticks.  The
+     * Generic motion events describe joystick movements, mouse hovers, track pad
+     * touches, scroll wheel movements and other input events.  The
      * {@link MotionEvent#getSource() source} of the motion event specifies
      * the class of input that was received.  Implementations of this method
      * must examine the bits in the source before processing the event.
      * The following code example shows how this is done.
+     * </p><p>
+     * Generic motion events with source class {@link InputDevice#SOURCE_CLASS_POINTER}
+     * are delivered to the view under the pointer.  All other generic motion events are
+     * delivered to the focused view.
      * </p>
      * <code>
      * public boolean onGenericMotionEvent(MotionEvent event) {
      *     if ((event.getSource() &amp; InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
-     *         float x = event.getX();
-     *         float y = event.getY();
-     *         // process the joystick motion
-     *         return true;
+     *         if (event.getAction() == MotionEvent.ACTION_MOVE) {
+     *             // process the joystick movement...
+     *             return true;
+     *         }
+     *     }
+     *     if ((event.getSource() &amp; InputDevice.SOURCE_CLASS_POINTER) != 0) {
+     *         switch (event.getAction()) {
+     *             case MotionEvent.ACTION_HOVER_MOVE:
+     *                 // process the mouse hover movement...
+     *                 return true;
+     *             case MotionEvent.ACTION_SCROLL:
+     *                 // process the scroll wheel movement...
+     *                 return true;
+     *         }
      *     }
      *     return super.onGenericMotionEvent(event);
      * }
@@ -11653,6 +11714,37 @@
     }
 
     /**
+     * Gets a scale factor that determines the distance the view should scroll
+     * vertically in response to {@link MotionEvent#ACTION_SCROLL}.
+     * @return The vertical scroll scale factor.
+     * @hide
+     */
+    protected float getVerticalScrollFactor() {
+        if (mVerticalScrollFactor == 0) {
+            TypedValue outValue = new TypedValue();
+            if (!mContext.getTheme().resolveAttribute(
+                    com.android.internal.R.attr.listPreferredItemHeight, outValue, true)) {
+                throw new IllegalStateException(
+                        "Expected theme to define listPreferredItemHeight.");
+            }
+            mVerticalScrollFactor = outValue.getDimension(
+                    mContext.getResources().getDisplayMetrics());
+        }
+        return mVerticalScrollFactor;
+    }
+
+    /**
+     * Gets a scale factor that determines the distance the view should scroll
+     * horizontally in response to {@link MotionEvent#ACTION_SCROLL}.
+     * @return The horizontal scroll scale factor.
+     * @hide
+     */
+    protected float getHorizontalScrollFactor() {
+        // TODO: Should use something else.
+        return getVerticalScrollFactor();
+    }
+
+    /**
      * A MeasureSpec encapsulates the layout requirements passed from parent to child.
      * Each MeasureSpec represents a requirement for either the width or the height.
      * A MeasureSpec is comprised of a size and a mode. There are three possible
@@ -11860,6 +11952,24 @@
     }
 
     /**
+     * Interface definition for a callback to be invoked when a generic motion event is
+     * dispatched to this view. The callback will be invoked before the generic motion
+     * event is given to the view.
+     */
+    public interface OnGenericMotionListener {
+        /**
+         * Called when a generic motion event is dispatched to a view. This allows listeners to
+         * get a chance to respond before the target view.
+         *
+         * @param v The view the generic motion event has been dispatched to.
+         * @param event The MotionEvent object containing full information about
+         *        the event.
+         * @return True if the listener has consumed the event, false otherwise.
+         */
+        boolean onGenericMotion(View v, MotionEvent event);
+    }
+
+    /**
      * Interface definition for a callback to be invoked when a view has been clicked and held.
      */
     public interface OnLongClickListener {
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index a0d4263..5c06151 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -1145,6 +1145,53 @@
      */
     @Override
     public boolean dispatchGenericMotionEvent(MotionEvent event) {
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            // Send the event to the child under the pointer.
+            final int childrenCount = mChildrenCount;
+            if (childrenCount != 0) {
+                final View[] children = mChildren;
+                final float x = event.getX();
+                final float y = event.getY();
+
+                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, null)) {
+                        // Scroll point is out of child's bounds.
+                        continue;
+                    }
+
+                    final float offsetX = mScrollX - child.mLeft;
+                    final float offsetY = mScrollY - child.mTop;
+                    final boolean handled;
+                    if (!child.hasIdentityMatrix()) {
+                        MotionEvent transformedEvent = MotionEvent.obtain(event);
+                        transformedEvent.offsetLocation(offsetX, offsetY);
+                        transformedEvent.transform(child.getInverseMatrix());
+                        handled = child.dispatchGenericMotionEvent(transformedEvent);
+                        transformedEvent.recycle();
+                    } else {
+                        event.offsetLocation(offsetX, offsetY);
+                        handled = child.dispatchGenericMotionEvent(event);
+                        event.offsetLocation(-offsetX, -offsetY);
+                    }
+
+                    if (handled) {
+                        return true;
+                    }
+                }
+            }
+
+            // No child handled the event.  Send it to this view group.
+            return super.dispatchGenericMotionEvent(event);
+        }
+
+        // Send the event to the focused child or to this view group if it has focus.
         if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) {
             return super.dispatchGenericMotionEvent(event);
         } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) {
@@ -1178,7 +1225,6 @@
         // Check for interception.
         final boolean intercepted;
         if (actionMasked == MotionEvent.ACTION_DOWN
-                || actionMasked == MotionEvent.ACTION_HOVER_MOVE
                 || mFirstTouchTarget != null) {
             final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
             if (!disallowIntercept) {
@@ -1188,6 +1234,8 @@
                 intercepted = false;
             }
         } else {
+            // There are no touch targets and this action is not an initial down
+            // so this view group continues to intercept touches.
             intercepted = true;
         }
 
@@ -1548,8 +1596,6 @@
             final int newAction;
             if (cancel) {
                 newAction = MotionEvent.ACTION_CANCEL;
-            } else if (oldAction == MotionEvent.ACTION_HOVER_MOVE) {
-                newAction = MotionEvent.ACTION_HOVER_MOVE;
             } else {
                 final int oldMaskedAction = oldAction & MotionEvent.ACTION_MASK;
                 if (oldMaskedAction == MotionEvent.ACTION_POINTER_DOWN
diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java
index 39f99b8..c7b1955 100644
--- a/core/java/android/view/ViewRoot.java
+++ b/core/java/android/view/ViewRoot.java
@@ -2247,7 +2247,7 @@
     private void deliverPointerEvent(MotionEvent event, boolean sendDone) {
         // If there is no view, then the event will not be handled.
         if (mView == null || !mAdded) {
-            finishPointerEvent(event, sendDone, false);
+            finishMotionEvent(event, sendDone, false);
             return;
         }
 
@@ -2270,7 +2270,7 @@
             event.offsetLocation(0, mCurScrollY);
         }
         if (MEASURE_LATENCY) {
-            lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano());
+            lt.sample("A Dispatching PointerEvents", System.nanoTime() - event.getEventTimeNano());
         }
 
         // Remember the touch position for possible drag-initiation.
@@ -2278,12 +2278,12 @@
         mLastTouchPoint.y = event.getRawY();
 
         // Dispatch touch to view hierarchy.
-        boolean handled = mView.dispatchTouchEvent(event);
+        boolean handled = mView.dispatchPointerEvent(event);
         if (MEASURE_LATENCY) {
-            lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano());
+            lt.sample("B Dispatched PointerEvents ", System.nanoTime() - event.getEventTimeNano());
         }
         if (handled) {
-            finishPointerEvent(event, sendDone, true);
+            finishMotionEvent(event, sendDone, true);
             return;
         }
 
@@ -2325,23 +2325,27 @@
             if (nearest != null) {
                 event.offsetLocation(deltas[0], deltas[1]);
                 event.setEdgeFlags(0);
-                if (mView.dispatchTouchEvent(event)) {
-                    finishPointerEvent(event, sendDone, true);
+                if (mView.dispatchPointerEvent(event)) {
+                    finishMotionEvent(event, sendDone, true);
                     return;
                 }
             }
         }
 
         // Pointer event was unhandled.
-        finishPointerEvent(event, sendDone, false);
+        finishMotionEvent(event, sendDone, false);
     }
 
-    private void finishPointerEvent(MotionEvent event, boolean sendDone, boolean handled) {
+    private void finishMotionEvent(MotionEvent event, boolean sendDone, boolean handled) {
         event.recycle();
         if (sendDone) {
             finishInputEvent(handled);
         }
-        if (LOCAL_LOGV || WATCH_POINTER) Log.i(TAG, "Done dispatching!");
+        if (LOCAL_LOGV || WATCH_POINTER) {
+            if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+                Log.i(TAG, "Done dispatching!");
+            }
+        }
     }
 
     private void deliverTrackballEvent(MotionEvent event, boolean sendDone) {
@@ -2349,7 +2353,7 @@
 
         // If there is no view, then the event will not be handled.
         if (mView == null || !mAdded) {
-            finishTrackballEvent(event, sendDone, false);
+            finishMotionEvent(event, sendDone, false);
             return;
         }
 
@@ -2361,7 +2365,7 @@
             // touch mode here.
             ensureTouchMode(false);
 
-            finishTrackballEvent(event, sendDone, true);
+            finishMotionEvent(event, sendDone, true);
             mLastTrackballTime = Integer.MIN_VALUE;
             return;
         }
@@ -2471,14 +2475,7 @@
 
         // Unfortunately we can't tell whether the application consumed the keys, so
         // we always consider the trackball event handled.
-        finishTrackballEvent(event, sendDone, true);
-    }
-
-    private void finishTrackballEvent(MotionEvent event, boolean sendDone, boolean handled) {
-        event.recycle();
-        if (sendDone) {
-            finishInputEvent(handled);
-        }
+        finishMotionEvent(event, sendDone, true);
     }
 
     private void deliverGenericMotionEvent(MotionEvent event, boolean sendDone) {
@@ -2490,7 +2487,7 @@
             if (isJoystick) {
                 updateJoystickDirection(event, false);
             }
-            finishGenericMotionEvent(event, sendDone, false);
+            finishMotionEvent(event, sendDone, false);
             return;
         }
 
@@ -2499,23 +2496,16 @@
             if (isJoystick) {
                 updateJoystickDirection(event, false);
             }
-            finishGenericMotionEvent(event, sendDone, true);
+            finishMotionEvent(event, sendDone, true);
             return;
         }
 
         if (isJoystick) {
             // Translate the joystick event into DPAD keys and try to deliver those.
             updateJoystickDirection(event, true);
-            finishGenericMotionEvent(event, sendDone, true);
+            finishMotionEvent(event, sendDone, true);
         } else {
-            finishGenericMotionEvent(event, sendDone, false);
-        }
-    }
-
-    private void finishGenericMotionEvent(MotionEvent event, boolean sendDone, boolean handled) {
-        event.recycle();
-        if (sendDone) {
-            finishInputEvent(handled);
+            finishMotionEvent(event, sendDone, false);
         }
     }
 
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index 06cb64e8..98fc290 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -62,6 +62,7 @@
 import android.util.Log;
 import android.view.Gravity;
 import android.view.HardwareCanvas;
+import android.view.InputDevice;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -6147,6 +6148,33 @@
         nativeHideCursor();
     }
 
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_SCROLL: {
+                    final float vscroll;
+                    final float hscroll;
+                    if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
+                        vscroll = 0;
+                        hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                    } else {
+                        vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                        hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+                    }
+                    if (hscroll != 0 || vscroll != 0) {
+                        final int vdelta = (int) (vscroll * getVerticalScrollFactor());
+                        final int hdelta = (int) (hscroll * getHorizontalScrollFactor());
+                        if (pinScrollBy(hdelta, vdelta, true, 0)) {
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
     private long mTrackballFirstTime = 0;
     private long mTrackballLastTime = 0;
     private float mTrackballRemainsX = 0.0f;
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index eb53e56..c2c8d16 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -43,6 +43,7 @@
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -3261,6 +3262,26 @@
     }
 
     @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_SCROLL: {
+                    if (mTouchMode == TOUCH_MODE_REST) {
+                        final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                        if (vscroll != 0) {
+                            final int delta = (int) (vscroll * getVerticalScrollFactor());
+                            if (trackMotionScroll(delta, delta)) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    @Override
     public void draw(Canvas canvas) {
         super.draw(canvas);
         if (mEdgeGlowTop != null) {
diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java
index e26e99b..3255f6f 100644
--- a/core/java/android/widget/HorizontalScrollView.java
+++ b/core/java/android/widget/HorizontalScrollView.java
@@ -26,6 +26,7 @@
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.FocusFinder;
+import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
@@ -634,6 +635,40 @@
     }
 
     @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_SCROLL: {
+                    if (!mIsBeingDragged) {
+                        final float hscroll;
+                        if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
+                            hscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                        } else {
+                            hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+                        }
+                        if (hscroll != 0) {
+                            final int delta = (int) (hscroll * getHorizontalScrollFactor());
+                            final int range = getScrollRange();
+                            int oldScrollX = mScrollX;
+                            int newScrollX = oldScrollX + delta;
+                            if (newScrollX < 0) {
+                                newScrollX = 0;
+                            } else if (newScrollX > range) {
+                                newScrollX = range;
+                            }
+                            if (newScrollX != oldScrollX) {
+                                super.scrollTo(newScrollX, mScrollY);
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    @Override
     protected void onOverScrolled(int scrollX, int scrollY,
             boolean clampedX, boolean clampedY) {
         // Treat animating scrolls differently; see #computeScroll() for why.
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index 9932320..7aca0db 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -27,6 +27,7 @@
 import android.os.StrictMode;
 import android.util.AttributeSet;
 import android.view.FocusFinder;
+import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
@@ -631,6 +632,35 @@
     }
 
     @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_SCROLL: {
+                    if (!mIsBeingDragged) {
+                        final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                        if (vscroll != 0) {
+                            final int delta = (int) (vscroll * getVerticalScrollFactor());
+                            final int range = getScrollRange();
+                            int oldScrollY = mScrollY;
+                            int newScrollY = oldScrollY - delta;
+                            if (newScrollY < 0) {
+                                newScrollY = 0;
+                            } else if (newScrollY > range) {
+                                newScrollY = range;
+                            }
+                            if (newScrollY != oldScrollY) {
+                                super.scrollTo(mScrollX, newScrollY);
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    @Override
     protected void onOverScrolled(int scrollX, int scrollY,
             boolean clampedX, boolean clampedY) {
         // Treat animating scrolls differently; see #computeScroll() for why.
diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java
index c72d0e8..5ac903d 100644
--- a/core/java/com/android/internal/widget/PointerLocationView.java
+++ b/core/java/com/android/internal/widget/PointerLocationView.java
@@ -318,10 +318,56 @@
         }
     }
     
-    private void logPointerCoords(MotionEvent.PointerCoords coords, int id) {
+    private void logPointerCoords(int action, int index, MotionEvent.PointerCoords coords, int id) {
+        final String prefix;
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN:
+                prefix = "DOWN";
+                break;
+            case MotionEvent.ACTION_UP:
+                prefix = "UP";
+                break;
+            case MotionEvent.ACTION_MOVE:
+                prefix = "MOVE";
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                prefix = "CANCEL";
+                break;
+            case MotionEvent.ACTION_OUTSIDE:
+                prefix = "OUTSIDE";
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN:
+                if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
+                    prefix = "DOWN";
+                } else {
+                    prefix = "MOVE";
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
+                    prefix = "UP";
+                } else {
+                    prefix = "MOVE";
+                }
+                break;
+            case MotionEvent.ACTION_HOVER_MOVE:
+                prefix = "HOVER MOVE";
+                break;
+            case MotionEvent.ACTION_SCROLL:
+                prefix = "SCROLL";
+                break;
+            default:
+                prefix = Integer.toString(action);
+                break;
+        }
+
         Log.i(TAG, mText.clear()
                 .append("Pointer ").append(id + 1)
-                .append(": (").append(coords.x, 3).append(", ").append(coords.y, 3)
+                .append(": ")
+                .append(prefix)
+                .append(" (").append(coords.x, 3).append(", ").append(coords.y, 3)
                 .append(") Pressure=").append(coords.pressure, 3)
                 .append(" Size=").append(coords.size, 3)
                 .append(" TouchMajor=").append(coords.touchMajor, 3)
@@ -335,7 +381,7 @@
                 .toString());
     }
 
-    public void addTouchEvent(MotionEvent event) {
+    public void addPointerEvent(MotionEvent event) {
         synchronized (mPointers) {
             int action = event.getAction();
             
@@ -363,10 +409,16 @@
                         ps.mCurDown = false;
                     }
                     mCurDown = true;
+                    mCurNumPointers = 0;
                     mMaxNumPointers = 0;
                     mVelocity.clear();
                 }
-                
+
+                mCurNumPointers += 1;
+                if (mMaxNumPointers < mCurNumPointers) {
+                    mMaxNumPointers = mCurNumPointers;
+                }
+
                 final int id = event.getPointerId(index);
                 while (NP <= id) {
                     PointerState ps = new PointerState();
@@ -375,49 +427,41 @@
                 }
                 
                 if (mActivePointerId < 0 ||
-                        ! mPointers.get(mActivePointerId).mCurDown) {
+                        !mPointers.get(mActivePointerId).mCurDown) {
                     mActivePointerId = id;
                 }
                 
                 final PointerState ps = mPointers.get(id);
                 ps.mCurDown = true;
-                if (mPrintCoords) {
-                    Log.i(TAG, mText.clear().append("Pointer ")
-                            .append(id + 1).append(": DOWN").toString());
-                }
             }
-            
-            final int NI = event.getPointerCount();
 
-            final boolean hover = (action == MotionEvent.ACTION_HOVER_MOVE);
-            mCurDown = action != MotionEvent.ACTION_UP
-                    && action != MotionEvent.ACTION_CANCEL
-                    && !hover;
-            mCurNumPointers = mCurDown ? NI : 0;
-            if (mMaxNumPointers < mCurNumPointers) {
-                mMaxNumPointers = mCurNumPointers;
-            }
+            final int NI = event.getPointerCount();
 
             mVelocity.addMovement(event);
             mVelocity.computeCurrentVelocity(1);
-            
-            for (int i=0; i<NI; i++) {
-                final int id = event.getPointerId(i);
-                final PointerState ps = hover ? null : mPointers.get(id);
-                final PointerCoords coords = ps != null ? ps.mCoords : mHoverCoords;
-                final int N = event.getHistorySize();
-                for (int j=0; j<N; j++) {
-                    event.getHistoricalPointerCoords(i, j, coords);
+
+            final int N = event.getHistorySize();
+            for (int historyPos = 0; historyPos < N; historyPos++) {
+                for (int i = 0; i < NI; i++) {
+                    final int id = event.getPointerId(i);
+                    final PointerState ps = mCurDown ? mPointers.get(id) : null;
+                    final PointerCoords coords = ps != null ? ps.mCoords : mHoverCoords;
+                    event.getHistoricalPointerCoords(i, historyPos, coords);
                     if (mPrintCoords) {
-                        logPointerCoords(coords, id);
+                        logPointerCoords(action, i, coords, id);
                     }
                     if (ps != null) {
-                        ps.addTrace(event.getHistoricalX(i, j), event.getHistoricalY(i, j));
+                        ps.addTrace(coords.x, coords.y);
                     }
                 }
+            }
+            for (int i = 0; i < NI; i++) {
+                final int id = event.getPointerId(i);
+                final PointerState ps = mCurDown ? mPointers.get(id) : null;
+                final PointerCoords coords = ps != null ? ps.mCoords : mHoverCoords;
                 event.getPointerCoords(i, coords);
                 if (mPrintCoords) {
-                    logPointerCoords(coords, id);
+                    logPointerCoords(action, i, coords, id);
                 }
                 if (ps != null) {
                     ps.addTrace(coords.x, coords.y);
@@ -425,7 +469,7 @@
                     ps.mYVelocity = mVelocity.getYVelocity(id);
                 }
             }
-            
+
             if (action == MotionEvent.ACTION_UP
                     || action == MotionEvent.ACTION_CANCEL
                     || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) {
@@ -435,15 +479,13 @@
                 final int id = event.getPointerId(index);
                 final PointerState ps = mPointers.get(id);
                 ps.mCurDown = false;
-                if (mPrintCoords) {
-                    Log.i(TAG, mText.clear().append("Pointer ")
-                            .append(id + 1).append(": UP").toString());
-                }
                 
                 if (action == MotionEvent.ACTION_UP
                         || action == MotionEvent.ACTION_CANCEL) {
                     mCurDown = false;
+                    mCurNumPointers = 0;
                 } else {
+                    mCurNumPointers -= 1;
                     if (mActivePointerId == id) {
                         mActivePointerId = event.getPointerId(index == 0 ? 1 : 0);
                     }
@@ -462,11 +504,20 @@
     
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        addTouchEvent(event);
+        addPointerEvent(event);
         return true;
     }
 
     @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            addPointerEvent(event);
+            return true;
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    @Override
     public boolean onTrackballEvent(MotionEvent event) {
         Log.i(TAG, "Trackball: " + event);
         return super.onTrackballEvent(event);
diff --git a/native/include/android/input.h b/native/include/android/input.h
index d516037..f19e8be 100644
--- a/native/include/android/input.h
+++ b/native/include/android/input.h
@@ -288,6 +288,15 @@
      * the last hover move event.
      */
     AMOTION_EVENT_ACTION_HOVER_MOVE = 7,
+
+    /* The motion event contains relative vertical and/or horizontal scroll offsets.
+     * Use getAxisValue to retrieve the information from AMOTION_EVENT_AXIS_VSCROLL
+     * and AMOTION_EVENT_AXIS_HSCROLL.
+     * The pointer may or may not be down when this event is dispatched.
+     * This action is always delivered to the winder under the pointer, which
+     * may not be the window currently touched.
+     */
+    AMOTION_EVENT_ACTION_SCROLL = 8,
 };
 
 /*
diff --git a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
index f5f4c6e..75ef762 100755
--- a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
+++ b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
@@ -284,7 +284,7 @@
                 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                     synchronized (mLock) {
                         if (mPointerLocationView != null) {
-                            mPointerLocationView.addTouchEvent(event);
+                            mPointerLocationView.addPointerEvent(event);
                             handled = true;
                         }
                     }
diff --git a/services/input/EventHub.cpp b/services/input/EventHub.cpp
index b31381a..25db25e 100644
--- a/services/input/EventHub.cpp
+++ b/services/input/EventHub.cpp
@@ -508,6 +508,7 @@
         }
 
         // Grab the next input event.
+        bool deviceWasRemoved = false;
         for (;;) {
             // Consume buffered input events, if any.
             if (mInputBufferIndex < mInputBufferCount) {
@@ -558,6 +559,10 @@
                 int32_t readSize = read(pfd.fd, mInputBufferData,
                         sizeof(struct input_event) * INPUT_BUFFER_SIZE);
                 if (readSize < 0) {
+                    if (errno == ENODEV) {
+                        deviceWasRemoved = true;
+                        break;
+                    }
                     if (errno != EAGAIN && errno != EINTR) {
                         LOGW("could not get event (errno=%d)", errno);
                     }
@@ -570,6 +575,13 @@
             }
         }
 
+        // Handle the case where a device has been removed but INotify has not yet noticed.
+        if (deviceWasRemoved) {
+            AutoMutex _l(mLock);
+            closeDeviceAtIndexLocked(mInputFdIndex);
+            continue; // report added or removed devices immediately
+        }
+
 #if HAVE_INOTIFY
         // readNotify() will modify mFDs and mFDCount, so this must be done after
         // processing all other events.
@@ -580,8 +592,6 @@
         }
 #endif
 
-        mInputFdIndex = 0;
-
         // Poll for events.  Mind the wake lock dance!
         // We hold a wake lock at all times except during poll().  This works due to some
         // subtle choreography.  When a device driver has pending (unread) events, it acquires
@@ -602,6 +612,9 @@
                 usleep(100000);
             }
         }
+
+        // Prepare to process all of the FDs we just polled.
+        mInputFdIndex = 0;
     }
 }
 
@@ -1008,37 +1021,42 @@
     for (size_t i = FIRST_ACTUAL_DEVICE_INDEX; i < mDevices.size(); i++) {
         Device* device = mDevices[i];
         if (device->path == devicePath) {
-            LOGI("Removed device: path=%s name=%s id=%d fd=%d classes=0x%x\n",
-                 device->path.string(), device->identifier.name.string(), device->id,
-                 device->fd, device->classes);
-
-            for (int j=0; j<EV_SW; j++) {
-                if (mSwitches[j] == device->id) {
-                    mSwitches[j] = 0;
-                }
-            }
-
-            if (device->id == mBuiltInKeyboardId) {
-                LOGW("built-in keyboard device %s (id=%d) is closing! the apps will not like this",
-                        device->path.string(), mBuiltInKeyboardId);
-                mBuiltInKeyboardId = -1;
-                clearKeyboardProperties(device, true);
-            }
-            clearKeyboardProperties(device, false);
-
-            mFds.removeAt(i);
-            mDevices.removeAt(i);
-            device->close();
-
-            device->next = mClosingDevices;
-            mClosingDevices = device;
-            return 0;
+            return closeDeviceAtIndexLocked(i);
         }
     }
-    LOGE("remove device: %s not found\n", devicePath);
+    LOGV("Remove device: %s not found, device may already have been removed.", devicePath);
     return -1;
 }
 
+int EventHub::closeDeviceAtIndexLocked(int index) {
+    Device* device = mDevices[index];
+    LOGI("Removed device: path=%s name=%s id=%d fd=%d classes=0x%x\n",
+         device->path.string(), device->identifier.name.string(), device->id,
+         device->fd, device->classes);
+
+    for (int j=0; j<EV_SW; j++) {
+        if (mSwitches[j] == device->id) {
+            mSwitches[j] = 0;
+        }
+    }
+
+    if (device->id == mBuiltInKeyboardId) {
+        LOGW("built-in keyboard device %s (id=%d) is closing! the apps will not like this",
+                device->path.string(), mBuiltInKeyboardId);
+        mBuiltInKeyboardId = -1;
+        clearKeyboardProperties(device, true);
+    }
+    clearKeyboardProperties(device, false);
+
+    mFds.removeAt(index);
+    mDevices.removeAt(index);
+    device->close();
+
+    device->next = mClosingDevices;
+    mClosingDevices = device;
+    return 0;
+}
+
 int EventHub::readNotify(int nfd) {
 #ifdef HAVE_INOTIFY
     int res;
diff --git a/services/input/EventHub.h b/services/input/EventHub.h
index 23bb344..f7936d2 100644
--- a/services/input/EventHub.h
+++ b/services/input/EventHub.h
@@ -261,6 +261,7 @@
 
     int openDevice(const char *devicePath);
     int closeDevice(const char *devicePath);
+    int closeDeviceAtIndexLocked(int index);
     int scanDir(const char *dirname);
     int readNotify(int nfd);
 
diff --git a/services/input/InputDispatcher.cpp b/services/input/InputDispatcher.cpp
index 2e3f0bd..c064a9c 100644
--- a/services/input/InputDispatcher.cpp
+++ b/services/input/InputDispatcher.cpp
@@ -116,6 +116,7 @@
     case AMOTION_EVENT_ACTION_MOVE:
     case AMOTION_EVENT_ACTION_OUTSIDE:
     case AMOTION_EVENT_ACTION_HOVER_MOVE:
+    case AMOTION_EVENT_ACTION_SCROLL:
         return true;
     case AMOTION_EVENT_ACTION_POINTER_DOWN:
     case AMOTION_EVENT_ACTION_POINTER_UP: {
@@ -480,8 +481,7 @@
         // If the application takes too long to catch up then we drop all events preceding
         // the touch into the other window.
         MotionEntry* motionEntry = static_cast<MotionEntry*>(entry);
-        if ((motionEntry->action == AMOTION_EVENT_ACTION_DOWN
-                || motionEntry->action == AMOTION_EVENT_ACTION_HOVER_MOVE)
+        if (motionEntry->action == AMOTION_EVENT_ACTION_DOWN
                 && (motionEntry->source & AINPUT_SOURCE_CLASS_POINTER)
                 && mInputTargetWaitCause == INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY
                 && mInputTargetWaitApplication != NULL) {
@@ -1180,7 +1180,8 @@
             && (mTouchState.deviceId != entry->deviceId
                     || mTouchState.source != entry->source);
     if (maskedAction == AMOTION_EVENT_ACTION_DOWN
-            || maskedAction == AMOTION_EVENT_ACTION_HOVER_MOVE) {
+            || maskedAction == AMOTION_EVENT_ACTION_HOVER_MOVE
+            || maskedAction == AMOTION_EVENT_ACTION_SCROLL) {
         bool down = maskedAction == AMOTION_EVENT_ACTION_DOWN;
         if (wrongDevice && !down) {
             mTempTouchState.copyFrom(mTouchState);
@@ -1205,8 +1206,9 @@
 
     if (maskedAction == AMOTION_EVENT_ACTION_DOWN
             || (isSplit && maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN)
-            || maskedAction == AMOTION_EVENT_ACTION_HOVER_MOVE) {
-        /* Case 1: New splittable pointer going down. */
+            || maskedAction == AMOTION_EVENT_ACTION_HOVER_MOVE
+            || maskedAction == AMOTION_EVENT_ACTION_SCROLL) {
+        /* Case 1: New splittable pointer going down, or need target for hover or scroll. */
 
         int32_t pointerIndex = getMotionEventActionPointerIndex(action);
         int32_t x = int32_t(entry->firstSample.pointerCoords[pointerIndex].
@@ -1380,8 +1382,10 @@
     // If this is the first pointer going down and the touched window has a wallpaper
     // then also add the touched wallpaper windows so they are locked in for the duration
     // of the touch gesture.
-    if (maskedAction == AMOTION_EVENT_ACTION_DOWN
-            || maskedAction == AMOTION_EVENT_ACTION_HOVER_MOVE) {
+    // We do not collect wallpapers during HOVER_MOVE or SCROLL because the wallpaper
+    // engine only supports touch events.  We would need to add a mechanism similar
+    // to View.onGenericMotionEvent to enable wallpapers to handle these events.
+    if (maskedAction == AMOTION_EVENT_ACTION_DOWN) {
         const InputWindow* foregroundWindow = mTempTouchState.getFirstForegroundWindow();
         if (foregroundWindow->hasWallpaper) {
             for (size_t i = 0; i < mWindows.size(); i++) {
@@ -1423,7 +1427,7 @@
                     || maskedAction == AMOTION_EVENT_ACTION_CANCEL
                     || maskedAction == AMOTION_EVENT_ACTION_HOVER_MOVE) {
                 // All pointers up or canceled.
-                mTempTouchState.reset();
+                mTouchState.reset();
             } else if (maskedAction == AMOTION_EVENT_ACTION_DOWN) {
                 // First pointer went down.
                 if (mTouchState.down) {
@@ -1432,6 +1436,7 @@
                     LOGD("Pointer down received while already down.");
 #endif
                 }
+                mTouchState.copyFrom(mTempTouchState);
             } else if (maskedAction == AMOTION_EVENT_ACTION_POINTER_UP) {
                 // One pointer went up.
                 if (isSplit) {
@@ -1450,10 +1455,13 @@
                         i += 1;
                     }
                 }
+                mTouchState.copyFrom(mTempTouchState);
+            } else if (maskedAction == AMOTION_EVENT_ACTION_SCROLL) {
+                // Discard temporary touch state since it was only valid for this action.
+            } else {
+                // Save changes to touch state as-is for all other actions.
+                mTouchState.copyFrom(mTempTouchState);
             }
-
-            // Save changes to touch state.
-            mTouchState.copyFrom(mTempTouchState);
         }
     } else {
 #if DEBUG_FOCUS
diff --git a/services/input/InputReader.cpp b/services/input/InputReader.cpp
index a963c72..a865d9f 100644
--- a/services/input/InputReader.cpp
+++ b/services/input/InputReader.cpp
@@ -1197,7 +1197,14 @@
     switch (rawEvent->type) {
     case EV_KEY:
         switch (rawEvent->scanCode) {
-        case BTN_MOUSE:
+        case BTN_LEFT:
+        case BTN_RIGHT:
+        case BTN_MIDDLE:
+        case BTN_SIDE:
+        case BTN_EXTRA:
+        case BTN_FORWARD:
+        case BTN_BACK:
+        case BTN_TASK:
             mAccumulator.fields |= Accumulator::FIELD_BTN_MOUSE;
             mAccumulator.btnMouse = rawEvent->value != 0;
             // Sync now since BTN_MOUSE is not necessarily followed by SYN_REPORT and
@@ -1247,6 +1254,7 @@
     int motionEventAction;
     PointerCoords pointerCoords;
     nsecs_t downTime;
+    float vscroll, hscroll;
     { // acquire lock
         AutoMutex _l(mLock);
 
@@ -1331,10 +1339,14 @@
         pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, mLocked.down ? 1.0f : 0.0f);
 
         if (mHaveVWheel && (fields & Accumulator::FIELD_REL_WHEEL)) {
-            pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_VSCROLL, mAccumulator.relWheel);
+            vscroll = mAccumulator.relWheel;
+        } else {
+            vscroll = 0;
         }
         if (mHaveHWheel && (fields & Accumulator::FIELD_REL_HWHEEL)) {
-            pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_HSCROLL, mAccumulator.relHWheel);
+            hscroll = mAccumulator.relHWheel;
+        } else {
+            hscroll = 0;
         }
     } // release lock
 
@@ -1345,6 +1357,15 @@
             1, &pointerId, &pointerCoords, mXPrecision, mYPrecision, downTime);
 
     mAccumulator.clear();
+
+    if (vscroll != 0 || hscroll != 0) {
+        pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_VSCROLL, vscroll);
+        pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_HSCROLL, hscroll);
+
+        getDispatcher()->notifyMotion(when, getDeviceId(), mSources, 0,
+                AMOTION_EVENT_ACTION_SCROLL, 0, metaState, AMOTION_EVENT_EDGE_FLAG_NONE,
+                1, &pointerId, &pointerCoords, mXPrecision, mYPrecision, downTime);
+    }
 }
 
 int32_t CursorInputMapper::getScanCodeState(uint32_t sourceMask, int32_t scanCode) {