Merge "Conversation view touch event shenanigans"
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index 1466bb0..ed07493 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -16,6 +16,8 @@
 
 package com.android.mail.browse;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Typeface;
@@ -28,18 +30,20 @@
 import android.view.LayoutInflater;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.View.OnClickListener;
+import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
 import android.widget.Toast;
-import android.widget.PopupMenu.OnMenuItemClickListener;
 
 import com.android.mail.ContactInfoSource;
 import com.android.mail.FormattedDateBuilder;
+import com.android.mail.R;
+import com.android.mail.SenderInfoLoader.ContactInfo;
 import com.android.mail.compose.ComposeActivity;
 import com.android.mail.perf.Timer;
 import com.android.mail.providers.Account;
@@ -47,11 +51,8 @@
 import com.android.mail.providers.Attachment;
 import com.android.mail.providers.Message;
 import com.android.mail.providers.UIProvider;
-import com.android.mail.R;
-import com.android.mail.SenderInfoLoader.ContactInfo;
 import com.android.mail.utils.LogUtils;
 import com.android.mail.utils.Utils;
-import com.google.common.annotations.VisibleForTesting;
 
 import java.io.IOException;
 import java.io.StringReader;
@@ -1179,4 +1180,5 @@
             t.pause(MEASURE_TAG);
         }
     }
+
 }
diff --git a/src/com/android/mail/browse/MessageWebView.java b/src/com/android/mail/browse/MessageWebView.java
deleted file mode 100644
index d2fbcd3..0000000
--- a/src/com/android/mail/browse/MessageWebView.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2011 Google Inc.
- * Licensed to 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.mail.browse;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.webkit.WebView;
-
-import com.android.mail.utils.LogUtils;
-
-/**
- * A WebView for HTML messages with custom zoom and scroll behavior.
- *
- */
-public class MessageWebView extends WebView {
-
-    private float mMaxScale = 0;
-    /**
-     * WebView with height=content_wrap doesn't shrink its height when zooming out.
-     * So to force this behavior, when zooming out (scale has shrunken) trick the measurement
-     * of WebView so as to make it think that it *should* fit the height to its bound AND
-     * that its view height is now zero. WebView will then attempt to fit its content into
-     * a zero-height view, and then resize the view height to the 'natural' content height.
-     *
-     * The measurement trick should be turned off on following onLayout to allow WebKit to grow its
-     * height again.
-     */
-    private boolean mShrinkMeasuredHeight;
-    /**
-     * When tricking the WebView to be zero height, views below it will shift up when drawing,
-     * and then shift back down when the WebView does its corrective layout pass.
-     * To avoid this shifting, force the WebView's parent view to keep its height fixed until
-     * the corrective layout pass is over, at which point we can restore the normal WebView height
-     * and the views below will draw in their correct final positions.
-     */
-    private int mParentLayoutHeight;
-    private boolean mCheckedWidePage;
-
-    private static final boolean ENABLE_WIDE_VIEWPORT_MODE = false;
-    private static final String LOG_TAG = new LogUtils().getLogTag();
-
-    public MessageWebView(Context context) {
-        this(context, null);
-    }
-
-    public MessageWebView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    /**
-     * Set the view parent's height and trigger a layout.
-     *
-     * @param measure if true, set the height to the current measured height and ignore the height
-     * parameter
-     * @param height a height to set it to. ignored if measure is true
-     * @return the original height
-     */
-    private int setParentHeight(boolean measure, int height) {
-        int originalHeight = 0;
-        ViewParent parent = getParent();
-        if (parent instanceof ViewGroup) {
-            ViewGroup parentGroup = (ViewGroup) getParent();
-            ViewGroup.LayoutParams parentLayoutParams = parentGroup.getLayoutParams();
-            originalHeight = parentLayoutParams.height;
-            parentLayoutParams.height = (measure) ? parentGroup.getHeight() : height;
-            parentGroup.setLayoutParams(parentLayoutParams);
-        }
-        return originalHeight;
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
-        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
-        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
-        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
-
-        // Allow WebView to measure as it normally would, which sets the HeightCanMeasure flag to
-        // trigger a WebKit layout. Afterwards, override the measured height with zero so that
-        // WebKit layout uses a desired height of zero, which shrinks the view to true content
-        // height.
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
-        if (mShrinkMeasuredHeight && heightMode != MeasureSpec.EXACTLY) {
-            setMeasuredDimension(getMeasuredWidthAndState(), 0);
-        }
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        LogUtils.d(LOG_TAG, "IN %d onLayout, changed=%b w/h=%d/%d l/t/r/b=%d/%d/%d/%d z=%f",
-                (hashCode() % 1000), changed, getWidth(), getHeight(), l, t, r, b , getScale());
-        super.onLayout(changed, l, t, r, b);
-
-        float scale = getScale();
-
-        if (mShrinkMeasuredHeight) {
-            // restore normal measurement behavior on the first layout after overriding height
-            // also restore parent container height
-            setParentHeight(false, mParentLayoutHeight);
-            mShrinkMeasuredHeight = false;
-
-        } else if (getHeight() > 0) {
-            if (scale < mMaxScale && !getSettings().getUseWideViewPort()) {
-                // fix the parent's height to whatever it is now.
-                // (normally the parent would be recalculated as header + body height)
-                // this will prevent messages below from shifting up
-                // and then when it's all done, set it back to the original value
-                // so the below messages just finally adjust now that the height is settled.
-
-                // we expect that WebView will trigger another layout anyway,
-                // and we have to inject this new sizing in before then.
-                LogUtils.d(LOG_TAG, "*** shrinking height of webview=" + (hashCode() % 1000));
-
-                mParentLayoutHeight = setParentHeight(true, 0);
-                mMaxScale = 0;
-                // force all measurements from now until the next layout to claim a zero height
-                mShrinkMeasuredHeight = true;
-
-            } else if (scale > mMaxScale) {
-                mMaxScale = getScale();
-
-                if (ENABLE_WIDE_VIEWPORT_MODE) {
-                    if (!mCheckedWidePage) {
-                        if ((getMeasuredWidthAndState() & MEASURED_STATE_TOO_SMALL) != 0) {
-                            LogUtils.i(LOG_TAG, "*** setting wide page mode for webview=%d",
-                                    (hashCode() % 1000));
-                            getSettings().setUseWideViewPort(true);
-                            // FIXME: wide viewport mode does not grow/shrink height as expected
-                            // maybe override height at this point and turn off reflow so that
-                            // scaling is linear, and we can manually maintain view height.
-                        }
-                        mCheckedWidePage = true;
-                    }
-                }
-            }
-        }
-    }
-
-}
diff --git a/src/com/android/mail/ui/ConversationContainer.java b/src/com/android/mail/ui/ConversationContainer.java
index 0f7ab3c..d57ed68 100644
--- a/src/com/android/mail/ui/ConversationContainer.java
+++ b/src/com/android/mail/ui/ConversationContainer.java
@@ -19,10 +19,13 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.webkit.WebView;
 import android.widget.Adapter;
+import android.widget.ScrollView;
 
 import com.android.mail.ui.ScrollNotifier.ScrollListener;
 import com.android.mail.utils.LogUtils;
@@ -33,20 +36,69 @@
  */
 public class ConversationContainer extends ViewGroup implements ScrollListener {
 
-    private Adapter mOverlayAdapter;
-    private int[] mOverlayTops;
-
     private static final String TAG = new LogUtils().getLogTag();
 
-    private int mOffsetY;
+    private Adapter mOverlayAdapter;
+    private int[] mOverlayTops;
+    private ConversationWebView mWebView;
+
+    /**
+     * Current document zoom scale per {@link WebView#getScale()}. It does not already account for
+     * display density, but by a happy coincidence, this makes the arithmetic for overlay placement
+     * easier.
+     */
     private float mScale;
 
+    /**
+     * System touch-slop distance per {@link ViewConfiguration#getScaledTouchSlop()}.
+     */
+    private final int mTouchSlop;
+    /**
+     * Current scroll position, as dictated by the background {@link WebView}.
+     */
+    private int mOffsetY;
+    /**
+     * Original pointer Y for slop calculation.
+     */
+    private float mLastMotionY;
+    /**
+     * Original pointer ID for slop calculation.
+     */
+    private int mActivePointerId;
+    /**
+     * Track pointer up/down state to know whether to send a make-up DOWN event to WebView.
+     * WebView internal logic requires that a stream of {@link MotionEvent#ACTION_MOVE} events be
+     * preceded by a {@link MotionEvent#ACTION_DOWN} event.
+     */
+    private boolean mTouchIsDown = false;
+    /**
+     * Remember if touch interception was triggered on a {@link MotionEvent#ACTION_POINTER_DOWN},
+     * so we can send a make-up event in {@link #onTouchEvent(MotionEvent)}.
+     */
+    private boolean mMissedPointerDown;
+
     public ConversationContainer(Context c) {
         this(c, null);
     }
 
     public ConversationContainer(Context c, AttributeSet attrs) {
         super(c, attrs);
+
+        mTouchSlop = ViewConfiguration.get(c).getScaledTouchSlop();
+
+        // Disabling event splitting fixes pinch-zoom when the first pointer goes down on the
+        // WebView and the second pointer goes down on an overlay view.
+        // Intercepting ACTION_POINTER_DOWN events allows pinch-zoom to work when the first pointer
+        // goes down on an overlay view.
+        setMotionEventSplittingEnabled(false);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mWebView = (ConversationWebView) getChildAt(0);
+        mWebView.addScrollListener(this);
     }
 
     public void setOverlayAdapter(Adapter a) {
@@ -61,14 +113,77 @@
         return getChildAt(i + 1);
     }
 
-    private WebView getBackgroundView() {
-        return (WebView) getChildAt(0);
+    private void forwardFakeMotionEvent(MotionEvent original, int newAction) {
+        MotionEvent newEvent = MotionEvent.obtain(original);
+        newEvent.setAction(newAction);
+        mWebView.onTouchEvent(newEvent);
+        LogUtils.v(TAG, "in Container.OnTouch. fake: action=%d x/y=%f/%f pointers=%d",
+                newEvent.getActionMasked(), newEvent.getX(), newEvent.getY(),
+                newEvent.getPointerCount());
+    }
+
+    /**
+     * Touch slop code was copied from {@link ScrollView#onInterceptTouchEvent(MotionEvent)}.
+     */
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        boolean intercept = false;
+        switch (ev.getActionMasked()) {
+            case MotionEvent.ACTION_POINTER_DOWN:
+                intercept = true;
+                mMissedPointerDown = true;
+                break;
+
+            case MotionEvent.ACTION_DOWN:
+                mLastMotionY = ev.getY();
+                mActivePointerId = ev.getPointerId(0);
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                final float y = ev.getY(pointerIndex);
+                final int yDiff = (int) Math.abs(y - mLastMotionY);
+                if (yDiff > mTouchSlop) {
+                    mLastMotionY = y;
+                    intercept = true;
+                }
+                break;
+        }
+
+        LogUtils.v(TAG, "in Container.InterceptTouch. action=%d x/y=%f/%f pointers=%d result=%s",
+                ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount(), intercept);
+        return intercept;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        final int action = ev.getActionMasked();
+
+        if (action == MotionEvent.ACTION_UP) {
+            mTouchIsDown = false;
+        } else if (!mTouchIsDown &&
+                (action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_POINTER_DOWN)) {
+
+            forwardFakeMotionEvent(ev, MotionEvent.ACTION_DOWN);
+            if (mMissedPointerDown) {
+                forwardFakeMotionEvent(ev, MotionEvent.ACTION_POINTER_DOWN);
+                mMissedPointerDown = false;
+            }
+
+            mTouchIsDown = true;
+        }
+
+        final boolean webViewResult = mWebView.onTouchEvent(ev);
+
+        LogUtils.v(TAG, "in Container.OnTouch. action=%d x/y=%f/%f pointers=%d",
+                ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount());
+        return webViewResult;
     }
 
     @Override
     public void onNotifierScroll(int x, int y) {
         mOffsetY = y;
-        mScale = getBackgroundView().getScale();
+        mScale = mWebView.getScale();
         LogUtils.v(TAG, "*** IN on scroll, x/y=%d/%d zoom=%f", x, y, mScale);
         layoutOverlays();
 
@@ -77,8 +192,9 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        LogUtils.d(TAG, "*** IN header container onMeasure");
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        LogUtils.d(TAG, "*** IN header container onMeasure spec for w/h=%d/%d", widthMeasureSpec,
+                heightMeasureSpec);
 
         measureChildren(widthMeasureSpec, heightMeasureSpec);
     }
@@ -86,9 +202,8 @@
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         LogUtils.d(TAG, "*** IN header container onLayout");
-        final View backgroundView = getBackgroundView();
-        backgroundView.layout(0, 0, backgroundView.getMeasuredWidth(),
-                backgroundView.getMeasuredHeight());
+        mWebView.layout(0, 0, mWebView.getMeasuredWidth(),
+                mWebView.getMeasuredHeight());
 
         layoutOverlays();
     }
@@ -111,7 +226,7 @@
         }
     }
 
-    // TODO: add margin support for children that want it (e.g. tablet headers)
+    // TODO: add margin support for children that want it (e.g. tablet headers?)
 
     public void onGeometryChange(int[] messageTops) {
         LogUtils.d(TAG, "*** got message tops:");
@@ -132,7 +247,7 @@
             // to position bottom-anchored content like attachments
         }
 
-        mScale = getBackgroundView().getScale();
+        mScale = mWebView.getScale();
 
     }
 
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index a64ce29..4e173e4 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -148,8 +148,6 @@
                 .findViewById(R.id.conversation_container);
         mWebView = (ConversationWebView) rootView.findViewById(R.id.webview);
 
-        mWebView.addScrollListener(mConversationContainer);
-
         mWebView.addJavascriptInterface(mJsBridge, "mail");
 
         mWebView.setWebChromeClient(new WebChromeClient() {
diff --git a/src/com/android/mail/ui/ConversationWebView.java b/src/com/android/mail/ui/ConversationWebView.java
index edf337b..72852dd 100644
--- a/src/com/android/mail/ui/ConversationWebView.java
+++ b/src/com/android/mail/ui/ConversationWebView.java
@@ -19,6 +19,7 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.view.MotionEvent;
 import android.webkit.WebView;
 
 import java.util.Set;
@@ -56,5 +57,16 @@
         }
     }
 
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        boolean result = super.onTouchEvent(ev);
+
+        if (result) {
+            // Events handled by the WebView should not be monkeyed with by any overlay interceptor
+            requestDisallowInterceptTouchEvent(true);
+        }
+
+        return result;
+    }
 
 }