1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email;
18
19import com.android.emailcommon.Logging;
20
21import android.os.Handler;
22import android.util.Log;
23
24import java.util.Timer;
25import java.util.TimerTask;
26
27/**
28 * This class used to "throttle" a flow of events.
29 *
30 * When {@link #onEvent()} is called, it calls the callback in a certain timeout later.
31 * Initially {@link #mMinTimeout} is used as the timeout, but if it gets multiple {@link #onEvent}
32 * calls in a certain amount of time, it extends the timeout, until it reaches {@link #mMaxTimeout}.
33 *
34 * This class is primarily used to throttle content changed events.
35 */
36public class Throttle {
37    public static final boolean DEBUG = false; // Don't submit with true
38
39    public static final int DEFAULT_MIN_TIMEOUT = 150;
40    public static final int DEFAULT_MAX_TIMEOUT = 2500;
41    /* package */ static final int TIMEOUT_EXTEND_INTERVAL = 500;
42
43    private static Timer TIMER = new Timer();
44
45    private final Clock mClock;
46    private final Timer mTimer;
47
48    /** Name of the instance.  Only for logging. */
49    private final String mName;
50
51    /** Handler for UI thread. */
52    private final Handler mHandler;
53
54    /** Callback to be called */
55    private final Runnable mCallback;
56
57    /** Minimum (default) timeout, in milliseconds.  */
58    private final int mMinTimeout;
59
60    /** Max timeout, in milliseconds.  */
61    private final int mMaxTimeout;
62
63    /** Current timeout, in milliseconds. */
64    private int mTimeout;
65
66    /** When {@link #onEvent()} was last called. */
67    private long mLastEventTime;
68
69    private MyTimerTask mRunningTimerTask;
70
71    /** Constructor with default timeout */
72    public Throttle(String name, Runnable callback, Handler handler) {
73        this(name, callback, handler, DEFAULT_MIN_TIMEOUT, DEFAULT_MAX_TIMEOUT);
74    }
75
76    /** Constructor that takes custom timeout */
77    public Throttle(String name, Runnable callback, Handler handler,int minTimeout,
78            int maxTimeout) {
79        this(name, callback, handler, minTimeout, maxTimeout, Clock.INSTANCE, TIMER);
80    }
81
82    /** Constructor for tests */
83    /* package */ Throttle(String name, Runnable callback, Handler handler,int minTimeout,
84            int maxTimeout, Clock clock, Timer timer) {
85        if (maxTimeout < minTimeout) {
86            throw new IllegalArgumentException();
87        }
88        mName = name;
89        mCallback = callback;
90        mClock = clock;
91        mTimer = timer;
92        mHandler = handler;
93        mMinTimeout = minTimeout;
94        mMaxTimeout = maxTimeout;
95        mTimeout = mMinTimeout;
96    }
97
98    private void debugLog(String message) {
99        Log.d(Logging.LOG_TAG, "Throttle: [" + mName + "] " + message);
100    }
101
102    private boolean isCallbackScheduled() {
103        return mRunningTimerTask != null;
104    }
105
106    public void cancelScheduledCallback() {
107        if (mRunningTimerTask != null) {
108            if (DEBUG) debugLog("Canceling scheduled callback");
109            mRunningTimerTask.cancel();
110            mRunningTimerTask = null;
111        }
112    }
113
114    /* package */ void updateTimeout() {
115        final long now = mClock.getTime();
116        if ((now - mLastEventTime) <= TIMEOUT_EXTEND_INTERVAL) {
117            mTimeout *= 2;
118            if (mTimeout >= mMaxTimeout) {
119                mTimeout = mMaxTimeout;
120            }
121            if (DEBUG) debugLog("Timeout extended " + mTimeout);
122        } else {
123            mTimeout = mMinTimeout;
124            if (DEBUG) debugLog("Timeout reset to " + mTimeout);
125        }
126
127        mLastEventTime = now;
128    }
129
130    public void onEvent() {
131        if (DEBUG) debugLog("onEvent");
132
133        updateTimeout();
134
135        if (isCallbackScheduled()) {
136            if (DEBUG) debugLog("    callback already scheduled");
137        } else {
138            if (DEBUG) debugLog("    scheduling callback");
139            mRunningTimerTask = new MyTimerTask();
140            mTimer.schedule(mRunningTimerTask, mTimeout);
141        }
142    }
143
144    /**
145     * Timer task called on timeout,
146     */
147    private class MyTimerTask extends TimerTask {
148        private boolean mCanceled;
149
150        @Override
151        public void run() {
152            mHandler.post(new HandlerRunnable());
153        }
154
155        @Override
156        public boolean cancel() {
157            mCanceled = true;
158            return super.cancel();
159        }
160
161        private class HandlerRunnable implements Runnable {
162            @Override
163            public void run() {
164                mRunningTimerTask = null;
165                if (!mCanceled) { // This check has to be done on the UI thread.
166                    if (DEBUG) debugLog("Kicking callback");
167                    mCallback.run();
168                }
169            }
170        }
171    }
172
173    /* package */ int getTimeoutForTest() {
174        return mTimeout;
175    }
176
177    /* package */ long getLastEventTimeForTest() {
178        return mLastEventTime;
179    }
180}
181