better scrolling in JS-less single-message conversation view

Allow simultaneous horizontal and vertical scrolling when the WebView is
embedded in a ScrollView by supplementally sending touch events to the
ScrollView when it normally wants to intercept AND the WebView handled
the touch. Touch interception happens as normal on any other child.

Bug: 8368615
Change-Id: I21f8721c673fd8146992b933d0c966d59c376795
diff --git a/res/layout/secure_conversation_view.xml b/res/layout/secure_conversation_view.xml
index da1dae3..26b4920 100644
--- a/res/layout/secure_conversation_view.xml
+++ b/res/layout/secure_conversation_view.xml
@@ -18,7 +18,7 @@
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
-    <ScrollView android:id="@+id/scroll_view"
+    <com.android.mail.browse.MessageScrollView android:id="@+id/scroll_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
         <LinearLayout
@@ -37,18 +37,18 @@
                 android:layout_height="wrap_content"
                 android:layout_below="@id/conv_header" />
             <!-- base WebView layer -->
-            <WebView
+            <com.android.mail.browse.MessageWebView
                 android:id="@+id/webview"
                 android:layout_width="match_parent"
                 android:layout_height="0dp"
-                android:layout_weight="1.0"/>
+                android:layout_weight="1.0" />
             <include layout="@layout/conversation_message_footer"
                 android:id="@+id/message_footer"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:visibility="gone" />
         </LinearLayout>
-    </ScrollView>
+    </com.android.mail.browse.MessageScrollView>
 
     <include layout="@layout/conversation_load_spinner"/>
 
diff --git a/src/com/android/mail/browse/MessageScrollView.java b/src/com/android/mail/browse/MessageScrollView.java
new file mode 100644
index 0000000..250fbd0
--- /dev/null
+++ b/src/com/android/mail/browse/MessageScrollView.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2013 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.MotionEvent;
+import android.widget.ScrollView;
+
+import com.android.mail.utils.LogUtils;
+
+/**
+ * A container that tries to play nice with an internally scrollable {@link Touchable} child view.
+ * The assumption is that the child view can scroll horizontally, but not vertically, so any
+ * touch events on that child view should ALSO be sent here so it can simultaneously vertically
+ * scroll (not the standard either/or behavior).
+ * <p>
+ * Touch events on any other child of this ScrollView are intercepted in the standard fashion.
+ */
+public class MessageScrollView extends ScrollView {
+
+    /**
+     * A View that reports whether onTouchEvent() was recently called.
+     */
+    public interface Touchable {
+        boolean wasTouched();
+        void clearTouched();
+    }
+
+    /**
+     * True when performing "special" interception.
+     */
+    private boolean mWantToIntercept;
+    /**
+     * Whether to perform the standard touch interception procedure. This is set to true when we
+     * want to intercept a touch stream from any child OTHER than {@link #mTouchableChild}.
+     */
+    private boolean mInterceptNormally;
+    /**
+     * The special child that we want to NOT intercept from in the normal way. Instead, this child
+     * will continue to receive the touch event stream (so it can handle the horizontal component)
+     * while this parent will additionally handle the events to perform vertical scrolling.
+     */
+    private Touchable mTouchableChild;
+
+    public static final String LOG_TAG = "MsgScroller";
+
+    public MessageScrollView(Context c) {
+        this(c, null);
+    }
+
+    public MessageScrollView(Context c, AttributeSet attrs) {
+        super(c, attrs);
+    }
+
+    public void setInnerScrollableView(Touchable child) {
+        mTouchableChild = child;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (mInterceptNormally) {
+            LogUtils.d(LOG_TAG, "IN ScrollView.onIntercept, NOW stealing. ev=%s", ev);
+            return true;
+        } else if (mWantToIntercept) {
+            LogUtils.d(LOG_TAG, "IN ScrollView.onIntercept, already stealing. ev=%s", ev);
+            return false;
+        }
+
+        mWantToIntercept = super.onInterceptTouchEvent(ev);
+        LogUtils.d(LOG_TAG, "OUT ScrollView.onIntercept, steal=%s ev=%s", mWantToIntercept, ev);
+        return false;
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        final int action = ev.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                LogUtils.d(LOG_TAG, "IN ScrollView.dispatchTouch, clearing flags");
+                mWantToIntercept = false;
+                mInterceptNormally = false;
+                break;
+        }
+        if (mTouchableChild != null) {
+            mTouchableChild.clearTouched();
+        }
+        final boolean handled = super.dispatchTouchEvent(ev);
+        LogUtils.d(LOG_TAG, "OUT ScrollView.dispatchTouch, handled=%s ev=%s", handled, ev);
+
+        if (mWantToIntercept) {
+            final boolean touchedChild = (mTouchableChild != null && mTouchableChild.wasTouched());
+            if (touchedChild) {
+                // also give the event to this scroll view if the WebView got the event
+                // and didn't stop any parent interception
+                LogUtils.d(LOG_TAG, "IN extra ScrollView.onTouch, ev=%s", ev);
+                onTouchEvent(ev);
+            } else {
+                mInterceptNormally = true;
+                mWantToIntercept = false;
+            }
+        }
+
+        return handled;
+    }
+
+}
diff --git a/src/com/android/mail/browse/MessageWebView.java b/src/com/android/mail/browse/MessageWebView.java
new file mode 100644
index 0000000..253a16b
--- /dev/null
+++ b/src/com/android/mail/browse/MessageWebView.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2013 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.MotionEvent;
+import android.webkit.WebView;
+
+import com.android.mail.utils.LogUtils;
+
+/**
+ * A WebView designed to live within a {@link MessageScrollView}.
+ */
+public class MessageWebView extends WebView implements MessageScrollView.Touchable {
+
+    private boolean mTouched;
+
+    public MessageWebView(Context c) {
+        this(c, null);
+    }
+
+    public MessageWebView(Context c, AttributeSet attrs) {
+        super(c, attrs);
+    }
+
+    @Override
+    public boolean wasTouched() {
+        return mTouched;
+    }
+
+    @Override
+    public void clearTouched() {
+        mTouched = false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        mTouched = true;
+        final boolean handled = super.onTouchEvent(event);
+        LogUtils.d(MessageScrollView.LOG_TAG,"OUT WebView.onTouch, returning handled=%s ev=%s",
+                handled, event);
+        return handled;
+    }
+
+}
diff --git a/src/com/android/mail/ui/SecureConversationViewFragment.java b/src/com/android/mail/ui/SecureConversationViewFragment.java
index eebec55..12b7f00 100644
--- a/src/com/android/mail/ui/SecureConversationViewFragment.java
+++ b/src/com/android/mail/ui/SecureConversationViewFragment.java
@@ -39,6 +39,8 @@
 import com.android.mail.browse.MessageFooterView;
 import com.android.mail.browse.MessageHeaderView;
 import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
+import com.android.mail.browse.MessageScrollView;
+import com.android.mail.browse.MessageWebView;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Conversation;
 import com.android.mail.providers.Message;
@@ -50,12 +52,12 @@
 public class SecureConversationViewFragment extends AbstractConversationViewFragment implements
         MessageHeaderViewCallbacks {
     private static final String LOG_TAG = LogTag.getLogTag();
-    private WebView mWebView;
+    private MessageWebView mWebView;
     private ConversationViewHeader mConversationHeaderView;
     private MessageHeaderView mMessageHeaderView;
     private MessageFooterView mMessageFooterView;
     private ConversationMessage mMessage;
-    private ScrollView mScrollView;
+    private MessageScrollView mScrollView;
 
     private final WebViewClient mWebViewClient = new AbstractConversationWebViewClient() {
         @Override
@@ -113,12 +115,12 @@
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
         View rootView = inflater.inflate(R.layout.secure_conversation_view, container, false);
-        mScrollView = (ScrollView) rootView.findViewById(R.id.scroll_view);
+        mScrollView = (MessageScrollView) rootView.findViewById(R.id.scroll_view);
         mConversationHeaderView = (ConversationViewHeader) rootView.findViewById(R.id.conv_header);
         mMessageHeaderView = (MessageHeaderView) rootView.findViewById(R.id.message_header);
         mMessageFooterView = (MessageFooterView) rootView.findViewById(R.id.message_footer);
         instantiateProgressIndicators(rootView);
-        mWebView = (WebView) rootView.findViewById(R.id.webview);
+        mWebView = (MessageWebView) rootView.findViewById(R.id.webview);
         mWebView.setWebViewClient(mWebViewClient);
         mWebView.setFocusable(false);
         final WebSettings settings = mWebView.getSettings();
@@ -130,6 +132,8 @@
         settings.setBuiltInZoomControls(true);
         settings.setDisplayZoomControls(false);
 
+        mScrollView.setInnerScrollableView(mWebView);
+
         return rootView;
     }