From 97d571cb4374495ed71f76ca102623bfed1989da Mon Sep 17 00:00:00 2001 From: Joe Steele Date: Sat, 23 Mar 2013 12:40:22 -0400 Subject: [PATCH] Import RigidWebView from AOSP Email client. Also import prerequisite classes. As contained in current master: https://android.googlesource.com/platform/packages/apps/Email/+/b3c37a31ccffe137cc9b1c1068bb99d3d00a2ee4 --- src/com/android/email/Clock.java | 32 ++++ src/com/android/email/Throttle.java | 180 +++++++++++++++++++ src/com/android/email/view/RigidWebView.java | 103 +++++++++++ 3 files changed, 315 insertions(+) create mode 100644 src/com/android/email/Clock.java create mode 100644 src/com/android/email/Throttle.java create mode 100644 src/com/android/email/view/RigidWebView.java diff --git a/src/com/android/email/Clock.java b/src/com/android/email/Clock.java new file mode 100644 index 000000000..a2b44bb9b --- /dev/null +++ b/src/com/android/email/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.email; + +/** + * 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/email/Throttle.java b/src/com/android/email/Throttle.java new file mode 100644 index 000000000..9edddf141 --- /dev/null +++ b/src/com/android/email/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.email; + +import com.android.emailcommon.Logging; + +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 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(Logging.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; + } +} diff --git a/src/com/android/email/view/RigidWebView.java b/src/com/android/email/view/RigidWebView.java new file mode 100644 index 000000000..1b31c08f2 --- /dev/null +++ b/src/com/android/email/view/RigidWebView.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2011 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.email.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.webkit.WebView; + +import com.android.email.Clock; +import com.android.email.Email; +import com.android.email.Throttle; +import com.android.emailcommon.Logging; +import com.android.emailcommon.utility.Utility; + +/** + * A custom WebView that is robust to rapid resize events in sequence. + * + * This is useful for a WebView which needs to have a layout of {@code WRAP_CONTENT}, since any + * contents with percent-based height will force the WebView to infinitely expand (or shrink). + */ +public class RigidWebView extends WebView { + + public RigidWebView(Context context) { + super(context); + } + public RigidWebView(Context context, AttributeSet attrs) { + super(context, attrs); + } + public RigidWebView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + 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(getClass().getName(), + new Runnable() { + @Override public void run() { + performSizeChangeDelayed(); + } + }, Utility.getMainThreadHandler(), + MIN_RESIZE_INTERVAL, MAX_RESIZE_INTERVAL); + + private int mRealWidth; + private int mRealHeight; + private boolean mIgnoreNext; + private long mLastSizeChangeTime = -1; + + @Override + protected void onSizeChanged(int w, int h, int ow, int oh) { + mRealWidth = w; + mRealHeight = h; + 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) { + if (Email.DEBUG) { + Log.w(Logging.LOG_TAG, "Supressing size change in RigidWebView"); + } + 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()); + } +}