Merge "Fix infinite resizing by using RigidWebView." into jb-ub-mail-ur10
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;
+ }
+}