Merge "Only handle onHoverEvent in actionable views."
diff --git a/api/current.txt b/api/current.txt
index 1bc0812..721e33e 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -21704,7 +21704,10 @@
     method public void dispatchDisplayHint(int);
     method public boolean dispatchDragEvent(android.view.DragEvent);
     method protected void dispatchDraw(android.graphics.Canvas);
+    method protected boolean dispatchGenericFocusedEvent(android.view.MotionEvent);
     method public boolean dispatchGenericMotionEvent(android.view.MotionEvent);
+    method protected boolean dispatchGenericPointerEvent(android.view.MotionEvent);
+    method protected boolean dispatchHoverEvent(android.view.MotionEvent);
     method public boolean dispatchKeyEvent(android.view.KeyEvent);
     method public boolean dispatchKeyEventPreIme(android.view.KeyEvent);
     method public boolean dispatchKeyShortcutEvent(android.view.KeyEvent);
@@ -21892,6 +21895,7 @@
     method public void onFinishTemporaryDetach();
     method protected void onFocusChanged(boolean, int, android.graphics.Rect);
     method public boolean onGenericMotionEvent(android.view.MotionEvent);
+    method public void onHoverChanged(boolean);
     method public boolean onHoverEvent(android.view.MotionEvent);
     method public void onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
     method public void onInitializeAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo);
@@ -22168,6 +22172,10 @@
     method public abstract boolean onGenericMotion(android.view.View, android.view.MotionEvent);
   }
 
+  public static abstract interface View.OnHoverListener {
+    method public abstract boolean onHover(android.view.View, android.view.MotionEvent);
+  }
+
   public static abstract interface View.OnKeyListener {
     method public abstract boolean onKey(android.view.View, int, android.view.KeyEvent);
   }
@@ -22338,6 +22346,7 @@
     method protected void measureChildren(int, int);
     method public final void offsetDescendantRectToMyCoords(android.view.View, android.graphics.Rect);
     method public final void offsetRectIntoDescendantCoords(android.view.View, android.graphics.Rect);
+    method public boolean onInterceptHoverEvent(android.view.MotionEvent);
     method public boolean onInterceptTouchEvent(android.view.MotionEvent);
     method protected abstract void onLayout(boolean, int, int, int, int);
     method protected boolean onRequestFocusInDescendants(int, android.graphics.Rect);
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index c8f68c7..1148436 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -2262,6 +2262,8 @@
 
     private OnTouchListener mOnTouchListener;
 
+    private OnHoverListener mOnHoverListener;
+
     private OnGenericMotionListener mOnGenericMotionListener;
 
     private OnDragListener mOnDragListener;
@@ -5118,9 +5120,6 @@
             return true;
         }
 
-        if (mInputEventConsistencyVerifier != null) {
-            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
-        }
         return false;
     }
 
@@ -5148,6 +5147,12 @@
                     || action == MotionEvent.ACTION_HOVER_MOVE
                     || action == MotionEvent.ACTION_HOVER_EXIT) {
                 if (dispatchHoverEvent(event)) {
+                    // For compatibility with existing applications that handled HOVER_MOVE
+                    // events in onGenericMotionEvent, dispatch the event there.  The
+                    // onHoverEvent method did not exist at the time.
+                    if (action == MotionEvent.ACTION_HOVER_MOVE) {
+                        dispatchGenericMotionEventInternal(event);
+                    }
                     return true;
                 }
             } else if (dispatchGenericPointerEvent(event)) {
@@ -5157,6 +5162,17 @@
             return true;
         }
 
+        if (dispatchGenericMotionEventInternal(event)) {
+            return true;
+        }
+
+        if (mInputEventConsistencyVerifier != null) {
+            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
+        }
+        return false;
+    }
+
+    private boolean dispatchGenericMotionEventInternal(MotionEvent event) {
         //noinspection SimplifiableIfStatement
         if (mOnGenericMotionListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                 && mOnGenericMotionListener.onGenericMotion(this, event)) {
@@ -5182,9 +5198,13 @@
      *
      * @param event The motion event to be dispatched.
      * @return True if the event was handled by the view, false otherwise.
-     * @hide
      */
     protected boolean dispatchHoverEvent(MotionEvent event) {
+        if (mOnHoverListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
+                && mOnHoverListener.onHover(this, event)) {
+            return true;
+        }
+
         return onHoverEvent(event);
     }
 
@@ -5197,7 +5217,6 @@
      *
      * @param event The motion event to be dispatched.
      * @return True if the event was handled by the view, false otherwise.
-     * @hide
      */
     protected boolean dispatchGenericPointerEvent(MotionEvent event) {
         return false;
@@ -5212,7 +5231,6 @@
      *
      * @param event The motion event to be dispatched.
      * @return True if the event was handled by the view, false otherwise.
-     * @hide
      */
     protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
         return false;
@@ -5789,35 +5807,55 @@
     /**
      * Implement this method to handle hover events.
      * <p>
-     * Hover events are pointer events with action {@link MotionEvent#ACTION_HOVER_ENTER},
-     * {@link MotionEvent#ACTION_HOVER_MOVE}, or {@link MotionEvent#ACTION_HOVER_EXIT}.
+     * This method is called whenever a pointer is hovering into, over, or out of the
+     * bounds of a view and the view is not currently being touched.
+     * Hover events are represented as pointer events with action
+     * {@link MotionEvent#ACTION_HOVER_ENTER}, {@link MotionEvent#ACTION_HOVER_MOVE},
+     * or {@link MotionEvent#ACTION_HOVER_EXIT}.
+     * </p>
+     * <ul>
+     * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_ENTER}
+     * when the pointer enters the bounds of the view.</li>
+     * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_MOVE}
+     * when the pointer has already entered the bounds of the view and has moved.</li>
+     * <li>The view receives a hover event with action {@link MotionEvent#ACTION_HOVER_EXIT}
+     * when the pointer has exited the bounds of the view or when the pointer is
+     * about to go down due to a button click, tap, or similar user action that
+     * causes the view to be touched.</li>
+     * </ul>
+     * <p>
+     * The view should implement this method to return true to indicate that it is
+     * handling the hover event, such as by changing its drawable state.
      * </p><p>
-     * The view receives hover enter as the pointer enters the bounds of the view and hover
-     * exit as the pointer exits the bound of the view or just before the pointer goes down
-     * (which implies that {@link #onTouchEvent(MotionEvent)} will be called soon).
-     * </p><p>
-     * If the view would like to handle the hover event itself and prevent its children
-     * from receiving hover, it should return true from this method.  If this method returns
-     * true and a child has already received a hover enter event, the child will
-     * automatically receive a hover exit event.
-     * </p><p>
-     * The default implementation sets the hovered state of the view if the view is
-     * clickable.
+     * The default implementation calls {@link #setHovered} to update the hovered state
+     * of the view when a hover enter or hover exit event is received, if the view
+     * is enabled and is clickable.
      * </p>
      *
      * @param event The motion event that describes the hover.
-     * @return True if this view handled the hover event and does not want its children
-     * to receive the hover event.
+     * @return True if the view handled the hover event.
+     *
+     * @see #isHovered
+     * @see #setHovered
+     * @see #onHoverChanged
      */
     public boolean onHoverEvent(MotionEvent event) {
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_HOVER_ENTER:
-                setHovered(true);
-                break;
+        final int viewFlags = mViewFlags;
+        if ((viewFlags & ENABLED_MASK) == DISABLED) {
+            return false;
+        }
 
-            case MotionEvent.ACTION_HOVER_EXIT:
-                setHovered(false);
-                break;
+        if ((viewFlags & CLICKABLE) == CLICKABLE
+                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_HOVER_ENTER:
+                    setHovered(true);
+                    break;
+                case MotionEvent.ACTION_HOVER_EXIT:
+                    setHovered(false);
+                    break;
+            }
+            return true;
         }
 
         return false;
@@ -5827,33 +5865,64 @@
      * Returns true if the view is currently hovered.
      *
      * @return True if the view is currently hovered.
+     *
+     * @see #setHovered
+     * @see #onHoverChanged
      */
+    @ViewDebug.ExportedProperty
     public boolean isHovered() {
         return (mPrivateFlags & HOVERED) != 0;
     }
 
     /**
      * Sets whether the view is currently hovered.
+     * <p>
+     * Calling this method also changes the drawable state of the view.  This
+     * enables the view to react to hover by using different drawable resources
+     * to change its appearance.
+     * </p><p>
+     * The {@link #onHoverChanged} method is called when the hovered state changes.
+     * </p>
      *
      * @param hovered True if the view is hovered.
+     *
+     * @see #isHovered
+     * @see #onHoverChanged
      */
     public void setHovered(boolean hovered) {
         if (hovered) {
             if ((mPrivateFlags & HOVERED) == 0) {
                 mPrivateFlags |= HOVERED;
                 refreshDrawableState();
-                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+                onHoverChanged(true);
             }
         } else {
             if ((mPrivateFlags & HOVERED) != 0) {
                 mPrivateFlags &= ~HOVERED;
                 refreshDrawableState();
-                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+                onHoverChanged(false);
             }
         }
     }
 
     /**
+     * Implement this method to handle hover state changes.
+     * <p>
+     * This method is called whenever the hover state changes as a result of a
+     * call to {@link #setHovered}.
+     * </p>
+     *
+     * @param hovered The current hover state, as returned by {@link #isHovered}.
+     *
+     * @see #isHovered
+     * @see #setHovered
+     */
+    public void onHoverChanged(boolean hovered) {
+        sendAccessibilityEvent(hovered ? AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
+                : AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+    }
+
+    /**
      * Implement this method to handle touch screen motion events.
      *
      * @param event The motion event.
@@ -13102,6 +13171,24 @@
     }
 
     /**
+     * Interface definition for a callback to be invoked when a hover event is
+     * dispatched to this view. The callback will be invoked before the hover
+     * event is given to the view.
+     */
+    public interface OnHoverListener {
+        /**
+         * Called when a hover event is dispatched to a view. This allows listeners to
+         * get a chance to respond before the target view.
+         *
+         * @param v The view the hover 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 onHover(View v, MotionEvent event);
+    }
+
+    /**
      * 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.
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index a6bce75..d6a6e2c 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -143,9 +143,16 @@
     @ViewDebug.ExportedProperty(category = "events")
     private float mLastTouchDownY;
 
-    // Child which last received ACTION_HOVER_ENTER and ACTION_HOVER_MOVE.
+    // The child which last received ACTION_HOVER_ENTER and ACTION_HOVER_MOVE.
+    // The child might not have actually handled the hover event, but we will
+    // continue sending hover events to it as long as the pointer remains over
+    // it and the view group does not intercept hover.
     private View mHoveredChild;
 
+    // True if the view group itself received a hover event.
+    // It might not have actually handled the hover event.
+    private boolean mHoveredSelf;
+
     /**
      * Internal flags.
      *
@@ -1222,81 +1229,132 @@
         return false;
     }
 
-    /** @hide */
+    /**
+     * {@inheritDoc}
+     */
     @Override
     protected boolean dispatchHoverEvent(MotionEvent event) {
-        // Send the hover enter or hover move event to the view group first.
-        // If it handles the event then a hovered child should receive hover exit.
-        boolean handled = false;
-        final boolean interceptHover;
         final int action = event.getAction();
-        if (action == MotionEvent.ACTION_HOVER_EXIT) {
-            interceptHover = true;
-        } else {
-            handled = super.dispatchHoverEvent(event);
-            interceptHover = handled;
-        }
 
-        // Send successive hover events to the hovered child as long as the pointer
-        // remains within the child's bounds.
-        MotionEvent eventNoHistory = event;
-        if (mHoveredChild != null) {
+        // First check whether the view group wants to intercept the hover event.
+        final boolean interceptHover = onInterceptHoverEvent(event);
+        event.setAction(action); // restore action in case it was changed
+
+        // Figure out which child should receive the next hover event.
+        View newHoveredChild = null;
+        if (!interceptHover && action != MotionEvent.ACTION_HOVER_EXIT) {
             final float x = event.getX();
             final float y = event.getY();
-
-            if (interceptHover
-                    || !isTransformedTouchPointInView(x, y, mHoveredChild, null)) {
-                // Pointer exited the child.
-                // Send it a hover exit with only the most recent coordinates.  We could
-                // try to find the exact point in history when the pointer left the view
-                // but it is not worth the effort.
-                eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
-                eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
-                handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, mHoveredChild);
-                eventNoHistory.setAction(action);
-                mHoveredChild = null;
-            } else {
-                // Pointer is still within the child.
-                //noinspection ConstantConditions
-                handled |= dispatchTransformedGenericPointerEvent(event, mHoveredChild);
-            }
-        }
-
-        // Find a new hovered child if needed.
-        if (!interceptHover && mHoveredChild == null
-                && (action == MotionEvent.ACTION_HOVER_ENTER
-                        || action == MotionEvent.ACTION_HOVER_MOVE)) {
             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 (!canViewReceivePointerEvents(child)
-                            || !isTransformedTouchPointInView(x, y, child, null)) {
-                        continue;
+                    if (canViewReceivePointerEvents(child)
+                            && isTransformedTouchPointInView(x, y, child, null)) {
+                        newHoveredChild = child;
+                        break;
                     }
+                }
+            }
+        }
 
-                    // Found the hovered child.
-                    mHoveredChild = child;
+        MotionEvent eventNoHistory = event;
+        boolean handled = false;
+
+        // Send events to the hovered child.
+        if (mHoveredChild == newHoveredChild) {
+            if (newHoveredChild != null) {
+                // Send event to the same child as before.
+                handled |= dispatchTransformedGenericPointerEvent(event, newHoveredChild);
+            }
+        } else {
+            if (mHoveredChild != null) {
+                // Exit the old hovered child.
+                if (action == MotionEvent.ACTION_HOVER_EXIT) {
+                    // Send the exit as is.
+                    handled |= dispatchTransformedGenericPointerEvent(
+                            event, mHoveredChild); // exit
+                } else {
+                    // Synthesize an exit from a move or enter.
+                    // Ignore the result because hover focus is moving to a different view.
                     if (action == MotionEvent.ACTION_HOVER_MOVE) {
-                        // Pointer was moving within the view group and entered the child.
-                        // Send it a hover enter and hover move with only the most recent
-                        // coordinates.  We could try to find the exact point in history when
-                        // the pointer entered the view but it is not worth the effort.
-                        eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
-                        eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
-                        handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child);
-                        eventNoHistory.setAction(action);
-
-                        handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child);
-                    } else { /* must be ACTION_HOVER_ENTER */
-                        // Pointer entered the child.
-                        handled |= dispatchTransformedGenericPointerEvent(event, child);
+                        dispatchTransformedGenericPointerEvent(
+                                event, mHoveredChild); // move
                     }
-                    break;
+                    eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
+                    eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
+                    dispatchTransformedGenericPointerEvent(
+                            eventNoHistory, mHoveredChild); // exit
+                    eventNoHistory.setAction(action);
+                }
+                mHoveredChild = null;
+            }
+
+            if (newHoveredChild != null) {
+                // Enter the new hovered child.
+                if (action == MotionEvent.ACTION_HOVER_ENTER) {
+                    // Send the enter as is.
+                    handled |= dispatchTransformedGenericPointerEvent(
+                            event, newHoveredChild); // enter
+                    mHoveredChild = newHoveredChild;
+                } else if (action == MotionEvent.ACTION_HOVER_MOVE) {
+                    // Synthesize an enter from a move.
+                    eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
+                    eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
+                    handled |= dispatchTransformedGenericPointerEvent(
+                            eventNoHistory, newHoveredChild); // enter
+                    eventNoHistory.setAction(action);
+
+                    handled |= dispatchTransformedGenericPointerEvent(
+                            eventNoHistory, newHoveredChild); // move
+                    mHoveredChild = newHoveredChild;
+                }
+            }
+        }
+
+        // Send events to the view group itself if it is hovered.
+        boolean newHoveredSelf = !handled;
+        if (newHoveredSelf == mHoveredSelf) {
+            if (newHoveredSelf) {
+                // Send event to the view group as before.
+                handled |= super.dispatchHoverEvent(event);
+            }
+        } else {
+            if (mHoveredSelf) {
+                // Exit the view group.
+                if (action == MotionEvent.ACTION_HOVER_EXIT) {
+                    // Send the exit as is.
+                    handled |= super.dispatchHoverEvent(event); // exit
+                } else {
+                    // Synthesize an exit from a move or enter.
+                    // Ignore the result because hover focus is moving to a different view.
+                    if (action == MotionEvent.ACTION_HOVER_MOVE) {
+                        super.dispatchHoverEvent(event); // move
+                    }
+                    eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
+                    eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
+                    super.dispatchHoverEvent(eventNoHistory); // exit
+                    eventNoHistory.setAction(action);
+                }
+                mHoveredSelf = false;
+            }
+
+            if (newHoveredSelf) {
+                // Enter the view group.
+                if (action == MotionEvent.ACTION_HOVER_ENTER) {
+                    // Send the enter as is.
+                    handled |= super.dispatchHoverEvent(event); // enter
+                    mHoveredSelf = true;
+                } else if (action == MotionEvent.ACTION_HOVER_MOVE) {
+                    // Synthesize an enter from a move.
+                    eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory);
+                    eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
+                    handled |= super.dispatchHoverEvent(eventNoHistory); // enter
+                    eventNoHistory.setAction(action);
+
+                    handled |= super.dispatchHoverEvent(eventNoHistory); // move
+                    mHoveredSelf = true;
                 }
             }
         }
@@ -1306,25 +1364,49 @@
             eventNoHistory.recycle();
         }
 
-        // Send hover exit to the view group.  If there was a child, we will already have
-        // sent the hover exit to it.
-        if (action == MotionEvent.ACTION_HOVER_EXIT) {
-            handled |= super.dispatchHoverEvent(event);
-        }
-
         // Done.
         return handled;
     }
 
-    @Override
-    public boolean onHoverEvent(MotionEvent event) {
-        // Handle the event only if leaf. This guarantees that
-        // the leafs (or any custom class that returns true from
-        // this method) will get a change to process the hover.
-        //noinspection SimplifiableIfStatement
-        if (getChildCount() == 0) {
-            return super.onHoverEvent(event);
-        }
+    /**
+     * Implement this method to intercept hover events before they are handled
+     * by child views.
+     * <p>
+     * This method is called before dispatching a hover event to a child of
+     * the view group or to the view group's own {@link #onHoverEvent} to allow
+     * the view group a chance to intercept the hover event.
+     * This method can also be used to watch all pointer motions that occur within
+     * the bounds of the view group even when the pointer is hovering over
+     * a child of the view group rather than over the view group itself.
+     * </p><p>
+     * The view group can prevent its children from receiving hover events by
+     * implementing this method and returning <code>true</code> to indicate
+     * that it would like to intercept hover events.  The view group must
+     * continuously return <code>true</code> from {@link #onInterceptHoverEvent}
+     * for as long as it wishes to continue intercepting hover events from
+     * its children.
+     * </p><p>
+     * Interception preserves the invariant that at most one view can be
+     * hovered at a time by transferring hover focus from the currently hovered
+     * child to the view group or vice-versa as needed.
+     * </p><p>
+     * If this method returns <code>true</code> and a child is already hovered, then the
+     * child view will first receive a hover exit event and then the view group
+     * itself will receive a hover enter event in {@link #onHoverEvent}.
+     * Likewise, if this method had previously returned <code>true</code> to intercept hover
+     * events and instead returns <code>false</code> while the pointer is hovering
+     * within the bounds of one of a child, then the view group will first receive a
+     * hover exit event in {@link #onHoverEvent} and then the hovered child will
+     * receive a hover enter event.
+     * </p><p>
+     * The default implementation always returns false.
+     * </p>
+     *
+     * @param event The motion event that describes the hover.
+     * @return True if the view group would like to intercept the hover event
+     * and prevent its children from receiving it.
+     */
+    public boolean onInterceptHoverEvent(MotionEvent event) {
         return false;
     }
 
@@ -1335,7 +1417,9 @@
         return MotionEvent.obtainNoHistory(event);
     }
 
-    /** @hide */
+    /**
+     * {@inheritDoc}
+     */
     @Override
     protected boolean dispatchGenericPointerEvent(MotionEvent event) {
         // Send the event to the child under the pointer.
@@ -1362,7 +1446,9 @@
         return super.dispatchGenericPointerEvent(event);
     }
 
-    /** @hide */
+    /**
+     * {@inheritDoc}
+     */
     @Override
     protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
         // Send the event to the focused child or to this view group if it has focus.