Merge "Implement drag to open overflow menu, lift to select"
diff --git a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java
index 4bb6d06..ff9678c 100644
--- a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java
+++ b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java
@@ -16,9 +16,6 @@
 
 package com.android.internal.view.menu;
 
-import com.android.internal.view.ActionBarPolicy;
-import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView;
-
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -27,11 +24,18 @@
 import android.util.SparseBooleanArray;
 import android.view.ActionProvider;
 import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.SoundEffectConstants;
 import android.view.View;
+import android.view.ViewConfiguration;
 import android.view.View.MeasureSpec;
 import android.view.ViewGroup;
+import android.widget.AbsListView;
 import android.widget.ImageButton;
+import android.widget.ListPopupWindow;
+
+import com.android.internal.view.ActionBarPolicy;
+import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView;
 
 import java.util.ArrayList;
 
@@ -559,6 +563,7 @@
             setFocusable(true);
             setVisibility(VISIBLE);
             setEnabled(true);
+            setOnTouchListener(new OverflowForwardListener(context));
         }
 
         @Override
@@ -572,10 +577,12 @@
             return true;
         }
 
+        @Override
         public boolean needsDividerBefore() {
             return false;
         }
 
+        @Override
         public boolean needsDividerAfter() {
             return false;
         }
@@ -675,4 +682,45 @@
             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 v, MotionEvent ev) {
+            if (ev.getActionMasked() == MotionEvent.ACTION_MOVE && v.isEnabled()
+                    && !v.pointInView(ev.getX(), ev.getY(), mScaledTouchSlop)) {
+                mActivePointerId = ev.getPointerId(0);
+                v.performClick();
+                return true;
+            }
+
+            return false;
+        }
+
+        @Override
+        public boolean onTouchForwarded(View v, MotionEvent ev) {
+            if (!v.isEnabled() || mOverflowPopup == null || !mOverflowPopup.isShowing()) {
+                return false;
+            }
+
+            if (mActivePointerId != MotionEvent.INVALID_POINTER_ID) {
+                if (mOverflowPopup.forwardMotionEvent(v, ev, mActivePointerId)) {
+                    return true;
+                }
+
+                mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+            }
+
+            mOverflowPopup.dismiss();
+            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 6d39860..945f42b 100644
--- a/core/java/com/android/internal/view/menu/MenuPopupHelper.java
+++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java
@@ -22,10 +22,12 @@
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.MeasureSpec;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
+import android.widget.AbsListView;
 import android.widget.AdapterView;
 import android.widget.BaseAdapter;
 import android.widget.FrameLayout;
@@ -46,6 +48,8 @@
 
     static final int ITEM_LAYOUT = com.android.internal.R.layout.popup_menu_item_layout;
 
+    private final int[] mTempLocation = new int[2];
+
     private final Context mContext;
     private final LayoutInflater mInflater;
     private final MenuBuilder mMenu;
@@ -158,6 +162,69 @@
         return mPopup != null && mPopup.isShowing();
     }
 
+    public boolean forwardMotionEvent(View v, MotionEvent ev, int activePointerId) {
+        if (mPopup == null || !mPopup.isShowing()) {
+            return false;
+        }
+
+        final AbsListView dstView = mPopup.getListView();
+        if (dstView == null || !dstView.isShown()) {
+            return false;
+        }
+
+        boolean cancelForwarding = false;
+        final int actionMasked = ev.getActionMasked();
+        switch (actionMasked) {
+            case MotionEvent.ACTION_CANCEL:
+                cancelForwarding = true;
+                break;
+            case MotionEvent.ACTION_UP:
+                cancelForwarding = true;
+                // $FALL-THROUGH$
+            case MotionEvent.ACTION_MOVE:
+                final int activeIndex = ev.findPointerIndex(activePointerId);
+                if (activeIndex < 0) {
+                    return false;
+                }
+
+                final int[] location = mTempLocation;
+                int x = (int) ev.getX(activeIndex);
+                int y = (int) ev.getY(activeIndex);
+
+                // Convert to global coordinates.
+                v.getLocationOnScreen(location);
+                x += location[0];
+                y += location[1];
+
+                // Convert to local coordinates.
+                dstView.getLocationOnScreen(location);
+                x -= location[0];
+                y -= location[1];
+
+                final int position = dstView.pointToPosition(x, y);
+                if (position >= 0) {
+                    final int childCount = dstView.getChildCount();
+                    final int firstVisiblePosition = dstView.getFirstVisiblePosition();
+                    final int index = position - firstVisiblePosition;
+                    if (index < childCount) {
+                        final View child = dstView.getChildAt(index);
+                        if (actionMasked == MotionEvent.ACTION_UP) {
+                            // Touch ended, click highlighted item.
+                            final long id = dstView.getItemIdAtPosition(position);
+                            dstView.performItemClick(child, position, id);
+                        } else if (actionMasked == MotionEvent.ACTION_MOVE) {
+                            // TODO: Highlight touched item, activate after
+                            // long-hover. Consider forwarding events as HOVER and
+                            // letting ListView handle this.
+                        }
+                    }
+                }
+                break;
+        }
+
+        return true;
+    }
+
     @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
new file mode 100644
index 0000000..d1086de
--- /dev/null
+++ b/core/java/com/android/internal/view/menu/TouchForwardingListener.java
@@ -0,0 +1,72 @@
+/*
+ * 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);
+}