Fix infinite resizing by using RigidWebView.

Ported some code from Email1 to fix the problem
where using wrap_content with zoom causes certain
html to expand infinitely. Fixes b/10542802.

Also turned off overscroll glow for the webview
since the scrollview already handles it for us.

Change-Id: I0ffd2b5475deab2c077cb377644fd56fa2755994
diff --git a/res/layout/secure_conversation_view.xml b/res/layout/secure_conversation_view.xml
index d21ac4b..2e391eb 100644
--- a/res/layout/secure_conversation_view.xml
+++ b/res/layout/secure_conversation_view.xml
@@ -75,7 +75,8 @@
                 <com.android.mail.browse.MessageWebView
                     android:id="@+id/webview"
                     android:layout_width="match_parent"
-                    android:layout_height="wrap_content" />
+                    android:layout_height="wrap_content"
+                    android:scrollbars="none"/>
                 <View
                     android:layout_width="1dp"
                     android:layout_height="match_parent"
diff --git a/src/com/android/mail/browse/MessageWebView.java b/src/com/android/mail/browse/MessageWebView.java
index 253a16b..9f66a8f 100644
--- a/src/com/android/mail/browse/MessageWebView.java
+++ b/src/com/android/mail/browse/MessageWebView.java
@@ -18,19 +18,45 @@
 package com.android.mail.browse;
 
 import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.webkit.WebView;
 
+import com.android.mail.utils.Clock;
+import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Throttle;
 
 /**
  * A WebView designed to live within a {@link MessageScrollView}.
  */
 public class MessageWebView extends WebView implements MessageScrollView.Touchable {
 
+    private static final String LOG_TAG = LogTag.getLogTag();
+
+    private static Handler sMainThreadHandler;
+
     private boolean mTouched;
 
+    private static final int MIN_RESIZE_INTERVAL = 200;
+    private static final int MAX_RESIZE_INTERVAL = 300;
+    private final Clock mClock = Clock.INSTANCE;
+
+    private final Throttle mThrottle = new Throttle("MessageWebView",
+            new Runnable() {
+                @Override public void run() {
+                    performSizeChangeDelayed();
+                }
+            }, getMainThreadHandler(),
+            MIN_RESIZE_INTERVAL, MAX_RESIZE_INTERVAL);
+
+    private int mRealWidth;
+    private int mRealHeight;
+    private boolean mIgnoreNext;
+    private long mLastSizeChangeTime = -1;
+
     public MessageWebView(Context c) {
         this(c, null);
     }
@@ -58,4 +84,51 @@
         return handled;
     }
 
+    @Override
+    protected void onSizeChanged(int w, int h, int ow, int oh) {
+        mRealWidth = w;
+        mRealHeight = h;
+        final long now = mClock.getTime();
+        boolean recentlySized = (now - mLastSizeChangeTime < MIN_RESIZE_INTERVAL);
+
+        // It's known that the previous resize event may cause a resize event immediately. If
+        // this happens sufficiently close to the last resize event, drop it on the floor.
+        if (mIgnoreNext) {
+            mIgnoreNext = false;
+            if (recentlySized) {
+                    LogUtils.w(LOG_TAG, "Suppressing size change in MessageWebView");
+                return;
+            }
+        }
+
+        if (recentlySized) {
+            mThrottle.onEvent();
+        } else {
+            // It's been a sufficiently long time - just perform the resize as normal. This should
+            // be the normal code path.
+            performSizeChange(ow, oh);
+        }
+    }
+
+    private void performSizeChange(int ow, int oh) {
+        super.onSizeChanged(mRealWidth, mRealHeight, ow, oh);
+        mLastSizeChangeTime = mClock.getTime();
+    }
+
+    private void performSizeChangeDelayed() {
+        mIgnoreNext = true;
+        performSizeChange(getWidth(), getHeight());
+    }
+
+    /**
+     * @return a {@link Handler} tied to the main thread.
+     */
+    public static Handler getMainThreadHandler() {
+        if (sMainThreadHandler == null) {
+            // No need to synchronize -- it's okay to create an extra Handler, which will be used
+            // only once and then thrown away.
+            sMainThreadHandler = new Handler(Looper.getMainLooper());
+        }
+        return sMainThreadHandler;
+    }
 }
diff --git a/src/com/android/mail/ui/SecureConversationViewController.java b/src/com/android/mail/ui/SecureConversationViewController.java
index 7bd9254..1f9b13e 100644
--- a/src/com/android/mail/ui/SecureConversationViewController.java
+++ b/src/com/android/mail/ui/SecureConversationViewController.java
@@ -93,6 +93,7 @@
                 mCallbacks.getFragment(), mCallbacks.getHandler());
         mProgressController.instantiateProgressIndicators(rootView);
         mWebView = (MessageWebView) rootView.findViewById(R.id.webview);
+        mWebView.setOverScrollMode(View.OVER_SCROLL_NEVER);
         mWebView.setWebViewClient(mCallbacks.getWebViewClient());
         mWebView.setFocusable(false);
         final WebSettings settings = mWebView.getSettings();
diff --git a/src/com/android/mail/utils/Clock.java b/src/com/android/mail/utils/Clock.java
new file mode 100644
index 0000000..accd4e2
--- /dev/null
+++ b/src/com/android/mail/utils/Clock.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010 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.utils;
+
+/**
+ * A class provide the current time (like {@link System#currentTimeMillis()}).
+ * It's intended to be mocked out for unit tests.
+ */
+public class Clock {
+    public static final Clock INSTANCE = new Clock();
+
+    protected Clock() {
+    }
+
+    public long getTime() {
+        return System.currentTimeMillis();
+    }
+}
diff --git a/src/com/android/mail/utils/Throttle.java b/src/com/android/mail/utils/Throttle.java
new file mode 100644
index 0000000..0dea3bc
--- /dev/null
+++ b/src/com/android/mail/utils/Throttle.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2010 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.utils;
+
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * This class used to "throttle" a flow of events.
+ *
+ * When {@link #onEvent()} is called, it calls the callback in a certain timeout later.
+ * Initially {@link #mMinTimeout} is used as the timeout, but if it gets multiple {@link #onEvent}
+ * calls in a certain amount of time, it extends the timeout, until it reaches {@link #mMaxTimeout}.
+ *
+ * This class is primarily used to throttle content changed events.
+ */
+public class Throttle {
+    public static final boolean DEBUG = false; // Don't submit with true
+
+    public static final int DEFAULT_MIN_TIMEOUT = 150;
+    public static final int DEFAULT_MAX_TIMEOUT = 2500;
+    /* package */ static final int TIMEOUT_EXTEND_INTERVAL = 500;
+
+    private static final String LOG_TAG = LogTag.getLogTag();
+
+    private static Timer TIMER = new Timer();
+
+    private final Clock mClock;
+    private final Timer mTimer;
+
+    /** Name of the instance.  Only for logging. */
+    private final String mName;
+
+    /** Handler for UI thread. */
+    private final Handler mHandler;
+
+    /** Callback to be called */
+    private final Runnable mCallback;
+
+    /** Minimum (default) timeout, in milliseconds.  */
+    private final int mMinTimeout;
+
+    /** Max timeout, in milliseconds.  */
+    private final int mMaxTimeout;
+
+    /** Current timeout, in milliseconds. */
+    private int mTimeout;
+
+    /** When {@link #onEvent()} was last called. */
+    private long mLastEventTime;
+
+    private MyTimerTask mRunningTimerTask;
+
+    /** Constructor with default timeout */
+    public Throttle(String name, Runnable callback, Handler handler) {
+        this(name, callback, handler, DEFAULT_MIN_TIMEOUT, DEFAULT_MAX_TIMEOUT);
+    }
+
+    /** Constructor that takes custom timeout */
+    public Throttle(String name, Runnable callback, Handler handler,int minTimeout,
+            int maxTimeout) {
+        this(name, callback, handler, minTimeout, maxTimeout, Clock.INSTANCE, TIMER);
+    }
+
+    /** Constructor for tests */
+    /* package */ Throttle(String name, Runnable callback, Handler handler,int minTimeout,
+            int maxTimeout, Clock clock, Timer timer) {
+        if (maxTimeout < minTimeout) {
+            throw new IllegalArgumentException();
+        }
+        mName = name;
+        mCallback = callback;
+        mClock = clock;
+        mTimer = timer;
+        mHandler = handler;
+        mMinTimeout = minTimeout;
+        mMaxTimeout = maxTimeout;
+        mTimeout = mMinTimeout;
+    }
+
+    private void debugLog(String message) {
+        Log.d(LOG_TAG, "Throttle: [" + mName + "] " + message);
+    }
+
+    private boolean isCallbackScheduled() {
+        return mRunningTimerTask != null;
+    }
+
+    public void cancelScheduledCallback() {
+        if (mRunningTimerTask != null) {
+            if (DEBUG) debugLog("Canceling scheduled callback");
+            mRunningTimerTask.cancel();
+            mRunningTimerTask = null;
+        }
+    }
+
+    /* package */ void updateTimeout() {
+        final long now = mClock.getTime();
+        if ((now - mLastEventTime) <= TIMEOUT_EXTEND_INTERVAL) {
+            mTimeout *= 2;
+            if (mTimeout >= mMaxTimeout) {
+                mTimeout = mMaxTimeout;
+            }
+            if (DEBUG) debugLog("Timeout extended " + mTimeout);
+        } else {
+            mTimeout = mMinTimeout;
+            if (DEBUG) debugLog("Timeout reset to " + mTimeout);
+        }
+
+        mLastEventTime = now;
+    }
+
+    public void onEvent() {
+        if (DEBUG) debugLog("onEvent");
+
+        updateTimeout();
+
+        if (isCallbackScheduled()) {
+            if (DEBUG) debugLog("    callback already scheduled");
+        } else {
+            if (DEBUG) debugLog("    scheduling callback");
+            mRunningTimerTask = new MyTimerTask();
+            mTimer.schedule(mRunningTimerTask, mTimeout);
+        }
+    }
+
+    /**
+     * Timer task called on timeout,
+     */
+    private class MyTimerTask extends TimerTask {
+        private boolean mCanceled;
+
+        @Override
+        public void run() {
+            mHandler.post(new HandlerRunnable());
+        }
+
+        @Override
+        public boolean cancel() {
+            mCanceled = true;
+            return super.cancel();
+        }
+
+        private class HandlerRunnable implements Runnable {
+            @Override
+            public void run() {
+                mRunningTimerTask = null;
+                if (!mCanceled) { // This check has to be done on the UI thread.
+                    if (DEBUG) debugLog("Kicking callback");
+                    mCallback.run();
+                }
+            }
+        }
+    }
+
+    /* package */ int getTimeoutForTest() {
+        return mTimeout;
+    }
+
+    /* package */ long getLastEventTimeForTest() {
+        return mLastEventTime;
+    }
+}