per-message zoom using JavaScript + CSS 3D transforms

When Auto-fit is off, disable built-in WebView zooming, which
is only capable of zooming an entire document (the entire
conversation).

Use ScaleGestureDetector to trigger a CSS 3D transform to zoom
in/out just a single message div.

During the gesture, the elements above and below the message
being scaled are untouched. This is ugly.
TODO: they should either move away (tricky) or at least fade
out.

When the gesture is complete, to avoid leaving overlapping
elements, we force-scale the height and reset the transform
origin to the top left.
TODO: auto-scroll to the correct place to counteract this
perceived jump.

Double-tap is not yet implemented. We'll have to do it
ourselves.

Bug: 7478834
Change-Id: I114e4977304c7060d499d116cc75bc0488967448
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index 28dd44f..c1ebd4a 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -30,7 +30,9 @@
 import android.os.SystemClock;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
+import android.view.ScaleGestureDetector;
 import android.view.View;
+import android.view.ScaleGestureDetector.OnScaleGestureListener;
 import android.view.View.OnLayoutChangeListener;
 import android.view.ViewGroup;
 import android.webkit.ConsoleMessage;
@@ -126,6 +128,8 @@
 
     private final WebViewClient mWebViewClient = new ConversationWebViewClient();
 
+    private final ScaleInterceptor mScaleInterceptor = new ScaleInterceptor();
+
     private ConversationViewAdapter mAdapter;
 
     private boolean mViewsCreated;
@@ -143,6 +147,8 @@
 
     private int  mMaxAutoLoadMessages;
 
+    private int mSideMarginPx;
+
     /**
      * If this conversation fragment is not visible, and it's inappropriate to load up front,
      * this is the reason we are waiting. This flag should be cleared once it's okay to load
@@ -264,6 +270,10 @@
 
         mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages);
 
+        mSideMarginPx = getResources().getDimensionPixelOffset(
+                R.dimen.conversation_view_margin_side) + getResources().getDimensionPixelOffset(
+                R.dimen.conversation_message_content_margin_side);
+
         mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(getActivity()));
 
         // set this up here instead of onCreateView to ensure the latest Account is loaded
@@ -596,11 +606,7 @@
         final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
         final int convHeaderPx = measureOverlayHeight(convHeaderPos);
 
-        final int sideMarginPx = getResources().getDimensionPixelOffset(
-                R.dimen.conversation_view_margin_side) + getResources().getDimensionPixelOffset(
-                R.dimen.conversation_message_content_margin_side);
-
-        mTemplates.startConversation(mWebView.screenPxToWebPx(sideMarginPx),
+        mTemplates.startConversation(mWebView.screenPxToWebPx(mSideMarginPx),
                 mWebView.screenPxToWebPx(convHeaderPx));
 
         int collapsedStart = -1;
@@ -926,14 +932,17 @@
     }
 
     private void setupOverviewMode() {
+        // for now, overview mode means use the built-in WebView zoom and disable custom scale
+        // gesture handling
         final boolean overviewMode = isOverviewMode(mAccount);
         final WebSettings settings = mWebView.getSettings();
         settings.setUseWideViewPort(overviewMode);
         settings.setSupportZoom(overviewMode);
+        settings.setBuiltInZoomControls(overviewMode);
         if (overviewMode) {
-            settings.setBuiltInZoomControls(true);
             settings.setDisplayZoomControls(false);
         }
+        mWebView.setOnScaleGestureListener(overviewMode ? null : mScaleInterceptor);
     }
 
     private class ConversationWebViewClient extends AbstractConversationWebViewClient {
@@ -1348,4 +1357,38 @@
             int heightBefore) {
         mDiff = (expanded ? 1 : -1) * Math.abs(i.getHeight() - heightBefore);
     }
+
+    private class ScaleInterceptor implements OnScaleGestureListener {
+
+        private float getFocusXWebPx(ScaleGestureDetector detector) {
+            return (detector.getFocusX() - mSideMarginPx) / mWebView.getInitialScale();
+        }
+
+        private float getFocusYWebPx(ScaleGestureDetector detector) {
+            return detector.getFocusY() / mWebView.getInitialScale();
+        }
+
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            mWebView.loadUrl(String.format("javascript:onScale(%s, %s, %s);",
+                    detector.getScaleFactor(), getFocusXWebPx(detector),
+                    getFocusYWebPx(detector)));
+            return false;
+        }
+
+        @Override
+        public boolean onScaleBegin(ScaleGestureDetector detector) {
+            mWebView.loadUrl(String.format("javascript:onScaleBegin(%s, %s);",
+                    getFocusXWebPx(detector), getFocusYWebPx(detector)));
+            return true;
+        }
+
+        @Override
+        public void onScaleEnd(ScaleGestureDetector detector) {
+            mWebView.loadUrl(String.format("javascript:onScaleEnd(%s, %s);",
+                    getFocusXWebPx(detector), getFocusYWebPx(detector)));
+        }
+
+    }
+
 }