Merge "Move forwarding code to ListPopupWindow, add drag-to-open in Spinner" into klp-dev
diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java
index 2b4e520..8919248 100644
--- a/core/java/android/widget/ListPopupWindow.java
+++ b/core/java/android/widget/ListPopupWindow.java
@@ -33,6 +33,7 @@
 import android.view.View;
 import android.view.View.MeasureSpec;
 import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.view.ViewParent;
 import android.view.animation.AccelerateDecelerateInterpolator;
@@ -961,33 +962,6 @@
     }
 
     /**
-     * Receives motion events forwarded from a source view. This is used
-     * internally to implement support for drag-to-open.
-     *
-     * @param src view from which the event was forwarded
-     * @param srcEvent forwarded motion event in source-local coordinates
-     * @param activePointerId id of the pointer that activated forwarding
-     * @return whether the event was handled
-     * @hide
-     */
-    public boolean onForwardedEvent(View src, MotionEvent srcEvent, int activePointerId) {
-        final DropDownListView dst = mDropDownList;
-        if (dst == null || !dst.isShown()) {
-            return false;
-        }
-
-        // Convert event to local coordinates.
-        final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
-        src.toGlobalMotionEvent(dstEvent);
-        dst.toLocalMotionEvent(dstEvent);
-
-        // Forward converted event, then recycle it.
-        final boolean handled = dst.onForwardedEvent(dstEvent, activePointerId);
-        dstEvent.recycle();
-        return handled;
-    }
-
-    /**
      * <p>Builds the popup window's content and returns the height the popup
      * should have. Returns -1 when the content already exists.</p>
      *
@@ -1155,6 +1129,147 @@
     }
 
     /**
+     * Abstract class that forwards touch events to a {@link ListPopupWindow}.
+     *
+     * @hide
+     */
+    public static abstract class ForwardingListener implements View.OnTouchListener {
+        /** Scaled touch slop, used for detecting movement outside bounds. */
+        private final float mScaledTouchSlop;
+
+        /** Whether this listener is currently forwarding touch events. */
+        private boolean mForwarding;
+
+        /** The id of the first pointer down in the current event stream. */
+        private int mActivePointerId;
+
+        public ForwardingListener(Context context) {
+            mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        }
+
+        /**
+         * Returns the popup to which this listener is forwarding events.
+         * <p>
+         * Override this to return the correct popup. If the popup is displayed
+         * asynchronously, you may also need to override
+         * {@link #onForwardingStopped} to prevent premature cancelation of
+         * forwarding.
+         *
+         * @return the popup to which this listener is forwarding events
+         */
+        public abstract ListPopupWindow getPopup();
+
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            final boolean wasForwarding = mForwarding;
+            final boolean forwarding;
+            if (wasForwarding) {
+                forwarding = onTouchForwarded(v, event) || !onForwardingStopped();
+            } else {
+                forwarding = onTouchObserved(v, event) && onForwardingStarted();
+            }
+
+            mForwarding = forwarding;
+            return forwarding || wasForwarding;
+        }
+
+        /**
+         * Called when forwarding would like to start.
+         * <p>
+         * By default, this will show the popup returned by {@link #getPopup()}.
+         * It may be overridden to perform another action, like clicking the
+         * source view or preparing the popup before showing it.
+         *
+         * @return true to start forwarding, false otherwise
+         */
+        public boolean onForwardingStarted() {
+            final ListPopupWindow popup = getPopup();
+            if (popup != null && !popup.isShowing()) {
+                popup.show();
+            }
+            return true;
+        }
+
+        /**
+         * Called when forwarding would like to stop.
+         * <p>
+         * By default, this will dismiss the popup returned by
+         * {@link #getPopup()}. It may be overridden to perform some other
+         * action.
+         *
+         * @return true to stop forwarding, false otherwise
+         */
+        public boolean onForwardingStopped() {
+            final ListPopupWindow popup = getPopup();
+            if (popup != null && popup.isShowing()) {
+                popup.dismiss();
+            }
+            return true;
+        }
+
+        /**
+         * Observes motion events and determines when to start forwarding.
+         *
+         * @param src view from which the event originated
+         * @param srcEvent motion event in source view coordinates
+         * @return true to start forwarding motion events, false otherwise
+         */
+        private boolean onTouchObserved(View src, MotionEvent srcEvent) {
+            if (!src.isEnabled()) {
+                return false;
+            }
+
+            // The first pointer down is always the active pointer.
+            final int actionMasked = srcEvent.getActionMasked();
+            if (actionMasked == MotionEvent.ACTION_DOWN) {
+                mActivePointerId = srcEvent.getPointerId(0);
+            }
+
+            final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
+            if (activePointerIndex >= 0) {
+                final float x = srcEvent.getX(activePointerIndex);
+                final float y = srcEvent.getY(activePointerIndex);
+                if (!src.pointInView(x, y, mScaledTouchSlop)) {
+                    // The pointer has moved outside of the view.
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        /**
+         * Handled forwarded motion events and determines when to stop
+         * forwarding.
+         *
+         * @param src view from which the event originated
+         * @param srcEvent motion event in source view coordinates
+         * @return true to continue forwarding motion events, false to cancel
+         */
+        private boolean onTouchForwarded(View src, MotionEvent srcEvent) {
+            final ListPopupWindow popup = getPopup();
+            if (popup == null || !popup.isShowing()) {
+                return false;
+            }
+
+            final DropDownListView dst = popup.mDropDownList;
+            if (dst == null || !dst.isShown()) {
+                return false;
+            }
+
+            // Convert event to destination-local coordinates.
+            final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
+            src.toGlobalMotionEvent(dstEvent);
+            dst.toLocalMotionEvent(dstEvent);
+
+            // Forward converted event to destination view, then recycle it.
+            final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId);
+            dstEvent.recycle();
+            return handled;
+        }
+    }
+
+    /**
      * <p>Wrapper class for a ListView. This wrapper can hijack the focus to
      * make sure the list uses the appropriate drawables and states when
      * displayed on screen within a drop down. The focus is never actually
diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java
index b707cef..7c7df96 100644
--- a/core/java/android/widget/Spinner.java
+++ b/core/java/android/widget/Spinner.java
@@ -30,12 +30,14 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.Gravity;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.ListPopupWindow.ForwardingListener;
 import android.widget.PopupWindow.OnDismissListener;
 
 
@@ -76,7 +78,10 @@
      * Use the theme-supplied value to select the dropdown mode.
      */
     private static final int MODE_THEME = -1;
-    
+
+    /** Forwarding listener used to implement drag-to-open. */
+    private ForwardingListener mForwardingListener;
+
     private SpinnerPopup mPopup;
     private DropDownAdapter mTempAdapter;
     int mDropDownWidth;
@@ -173,7 +178,7 @@
         }
 
         case MODE_DROPDOWN: {
-            DropdownPopup popup = new DropdownPopup(context, attrs, defStyle);
+            final DropdownPopup popup = new DropdownPopup(context, attrs, defStyle);
 
             mDropDownWidth = a.getLayoutDimension(
                     com.android.internal.R.styleable.Spinner_dropDownWidth,
@@ -193,6 +198,20 @@
             }
 
             mPopup = popup;
+            mForwardingListener = new ForwardingListener(context) {
+                @Override
+                public ListPopupWindow getPopup() {
+                    return popup;
+                }
+
+                @Override
+                public boolean onForwardingStarted() {
+                    if (!mPopup.isShowing()) {
+                        mPopup.show(getTextDirection(), getTextAlignment());
+                    }
+                    return true;
+                }
+            };
             break;
         }
         }
@@ -449,6 +468,15 @@
     }
 
     @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mForwardingListener != null && mForwardingListener.onTouch(this, event)) {
+            return true;
+        }
+
+        return super.onTouchEvent(event);
+    }
+
+    @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
diff --git a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java
index 30f9793..5d0a603 100644
--- a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java
+++ b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java
@@ -32,6 +32,8 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.ViewGroup;
 import android.widget.ImageButton;
+import android.widget.ListPopupWindow;
+import android.widget.ListPopupWindow.ForwardingListener;
 
 import com.android.internal.view.ActionBarPolicy;
 import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView;
@@ -562,7 +564,36 @@
             setFocusable(true);
             setVisibility(VISIBLE);
             setEnabled(true);
-            setOnTouchListener(new OverflowForwardListener(context));
+
+            setOnTouchListener(new ForwardingListener(context) {
+                @Override
+                public ListPopupWindow getPopup() {
+                    if (mOverflowPopup == null) {
+                        return null;
+                    }
+
+                    return mOverflowPopup.getPopup();
+                }
+
+                @Override
+                public boolean onForwardingStarted() {
+                    showOverflowMenu();
+                    return true;
+                }
+
+                @Override
+                public boolean onForwardingStopped() {
+                    // Displaying the popup occurs asynchronously, so wait for
+                    // the runnable to finish before deciding whether to stop
+                    // forwarding.
+                    if (mPostedOpenRunnable != null) {
+                        return false;
+                    }
+
+                    hideOverflowMenu();
+                    return true;
+                }
+            });
         }
 
         @Override
@@ -687,56 +718,4 @@
             mPostedOpenRunnable = null;
         }
     }
-
-    private class OverflowForwardListener extends TouchForwardingListener {
-        /** Scaled touch slop, used for detecting movement outside bounds. */
-        private final float mScaledTouchSlop;
-
-        private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
-
-        public OverflowForwardListener(Context context) {
-            mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
-        }
-
-        @Override
-        public boolean onTouchObserved(View src, MotionEvent srcEvent) {
-            if (!src.isEnabled()) {
-                return false;
-            }
-
-            // Always start forwarding events when the source view is touched.
-            mActivePointerId = srcEvent.getPointerId(0);
-            src.performClick();
-            return true;
-        }
-
-        @Override
-        public boolean onTouchForwarded(View src, MotionEvent srcEvent) {
-            final OverflowPopup popup = mOverflowPopup;
-            if (popup != null && popup.isShowing()) {
-                final int activePointerId = mActivePointerId;
-                if (activePointerId != MotionEvent.INVALID_POINTER_ID && src.isEnabled()
-                        && popup.forwardMotionEvent(src, srcEvent, activePointerId)) {
-                    // Handled the motion event, continue forwarding.
-                    return true;
-                }
-
-                final int activePointerIndex = srcEvent.findPointerIndex(activePointerId);
-                if (activePointerIndex >= 0) {
-                    final float x = srcEvent.getX(activePointerIndex);
-                    final float y = srcEvent.getY(activePointerIndex);
-                    if (src.pointInView(x, y, mScaledTouchSlop)) {
-                        // The user is touching the source view. Cancel
-                        // forwarding, but don't dismiss the popup.
-                        return false;
-                    }
-                }
-
-                popup.dismiss();
-            }
-
-            // Cancel forwarding.
-            return false;
-        }
-    }
 }
diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java
index 9b266df..dbb78c2 100644
--- a/core/java/com/android/internal/view/menu/MenuPopupHelper.java
+++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java
@@ -108,6 +108,10 @@
         }
     }
 
+    public ListPopupWindow getPopup() {
+        return mPopup;
+    }
+
     public boolean tryShow() {
         mPopup = new ListPopupWindow(mContext, null, com.android.internal.R.attr.popupMenuStyle);
         mPopup.setOnDismissListener(this);
@@ -159,22 +163,6 @@
         return mPopup != null && mPopup.isShowing();
     }
 
-    /**
-     * Forwards motion events from a source view to the popup window.
-     *
-     * @param src view from which the event was forwarded
-     * @param event forwarded motion event in source-local coordinates
-     * @param activePointerId id of the pointer that activated forwarding
-     * @return whether the event was handled
-     */
-    public boolean forwardMotionEvent(View src, MotionEvent event, int activePointerId) {
-        if (mPopup == null || !mPopup.isShowing()) {
-            return false;
-        }
-
-        return mPopup.onForwardedEvent(src, event, activePointerId);
-    }
-
     @Override
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
         MenuAdapter adapter = mAdapter;
diff --git a/core/java/com/android/internal/view/menu/TouchForwardingListener.java b/core/java/com/android/internal/view/menu/TouchForwardingListener.java
deleted file mode 100644
index d1086de..0000000
--- a/core/java/com/android/internal/view/menu/TouchForwardingListener.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2013 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.
- */
-
-package com.android.internal.view.menu;
-
-import android.view.MotionEvent;
-import android.view.View;
-
-/**
- * Touch listener used to intercept touches and forward them out of a view.
- */
-abstract class TouchForwardingListener implements View.OnTouchListener {
-    /** Whether this listener is currently forwarding touch events. */
-    private boolean mForwarding;
-
-    @Override
-    public boolean onTouch(View v, MotionEvent ev) {
-        final int actionMasked = ev.getActionMasked();
-
-        if (mForwarding) {
-            // Rejecting the event or ending the stream stops forwarding.
-            if (!onTouchForwarded(v, ev) || actionMasked == MotionEvent.ACTION_UP
-                    || actionMasked == MotionEvent.ACTION_CANCEL) {
-                stopForwarding();
-            }
-        } else {
-            if (onTouchObserved(v, ev)) {
-                startForwarding();
-            }
-        }
-
-        return mForwarding;
-    }
-
-    public void startForwarding() {
-        mForwarding = true;
-    }
-
-    public void stopForwarding() {
-        mForwarding = false;
-    }
-
-    /**
-     * Attempts to start forwarding motion events.
-     *
-     * @param v The view that triggered forwarding.
-     * @return True to start forwarding motion events, or false to cancel.
-     */
-    public abstract boolean onTouchObserved(View v, MotionEvent ev);
-
-    /**
-     * Handles forwarded motion events.
-     *
-     * @param v The view from which the event was forwarded.
-     * @param ev The forwarded motion event.
-     * @return True to continue forwarding motion events, or false to cancel.
-     */
-    public abstract boolean onTouchForwarded(View v, MotionEvent ev);
-}