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.mail.utils;
18
19import android.os.Handler;
20import android.util.Log;
21
22import java.util.Timer;
23import java.util.TimerTask;
24
25/**
26 * This class used to "throttle" a flow of events.
27 *
28 * When {@link #onEvent()} is called, it calls the callback in a certain timeout later.
29 * Initially {@link #mMinTimeout} is used as the timeout, but if it gets multiple {@link #onEvent}
30 * calls in a certain amount of time, it extends the timeout, until it reaches {@link #mMaxTimeout}.
31 *
32 * This class is primarily used to throttle content changed events.
33 */
34public class Throttle {
35    public static final boolean DEBUG = false; // Don't submit with true
36
37    public static final int DEFAULT_MIN_TIMEOUT = 150;
38    public static final int DEFAULT_MAX_TIMEOUT = 2500;
39    // exposed for testing
40    public static final int TIMEOUT_EXTEND_INTERVAL = 500;
41
42    private static final String LOG_TAG = LogTag.getLogTag();
43
44    private static Timer TIMER = new Timer();
45
46    private final Clock mClock;
47    private final Timer mTimer;
48
49    /** Name of the instance.  Only for logging. */
50    private final String mName;
51
52    /** Handler for UI thread. */
53    private final Handler mHandler;
54
55    /** Callback to be called */
56    private final Runnable mCallback;
57
58    /** Minimum (default) timeout, in milliseconds.  */
59    private final int mMinTimeout;
60
61    /** Max timeout, in milliseconds.  */
62    private final int mMaxTimeout;
63
64    /** Current timeout, in milliseconds. */
65    private int mTimeout;
66
67    /** When {@link #onEvent()} was last called. */
68    private long mLastEventTime;
69
70    private MyTimerTask mRunningTimerTask;
71
72    /** Constructor with default timeout */
73    public Throttle(String name, Runnable callback, Handler handler) {
74        this(name, callback, handler, DEFAULT_MIN_TIMEOUT, DEFAULT_MAX_TIMEOUT);
75    }
76
77    /** Constructor that takes custom timeout */
78    public Throttle(String name, Runnable callback, Handler handler,int minTimeout,
79            int maxTimeout) {
80        this(name, callback, handler, minTimeout, maxTimeout, Clock.INSTANCE, TIMER);
81    }
82
83    /** Constructor for tests */
84    // exposed for testing
85    public Throttle(String name, Runnable callback, Handler handler,int minTimeout,
86            int maxTimeout, Clock clock, Timer timer) {
87        if (maxTimeout < minTimeout) {
88            throw new IllegalArgumentException();
89        }
90        mName = name;
91        mCallback = callback;
92        mClock = clock;
93        mTimer = timer;
94        mHandler = handler;
95        mMinTimeout = minTimeout;
96        mMaxTimeout = maxTimeout;
97        mTimeout = mMinTimeout;
98    }
99
100    private void debugLog(String message) {
101        Log.d(LOG_TAG, "Throttle: [" + mName + "] " + message);
102    }
103
104    private boolean isCallbackScheduled() {
105        return mRunningTimerTask != null;
106    }
107
108    public void cancelScheduledCallback() {
109        if (mRunningTimerTask != null) {
110            if (DEBUG) debugLog("Canceling scheduled callback");
111            mRunningTimerTask.cancel();
112            mRunningTimerTask = null;
113        }
114    }
115
116    // exposed for testing
117    public void updateTimeout() {
118        final long now = mClock.getTime();
119        if ((now - mLastEventTime) <= TIMEOUT_EXTEND_INTERVAL) {
120            mTimeout *= 2;
121            if (mTimeout >= mMaxTimeout) {
122                mTimeout = mMaxTimeout;
123            }
124            if (DEBUG) debugLog("Timeout extended " + mTimeout);
125        } else {
126            mTimeout = mMinTimeout;
127            if (DEBUG) debugLog("Timeout reset to " + mTimeout);
128        }
129
130        mLastEventTime = now;
131    }
132
133    public void onEvent() {
134        if (DEBUG) debugLog("onEvent");
135
136        updateTimeout();
137
138        if (isCallbackScheduled()) {
139            if (DEBUG) debugLog("    callback already scheduled");
140        } else {
141            if (DEBUG) debugLog("    scheduling callback");
142            mRunningTimerTask = new MyTimerTask();
143            mTimer.schedule(mRunningTimerTask, mTimeout);
144        }
145    }
146
147    /**
148     * Timer task called on timeout,
149     */
150    private class MyTimerTask extends TimerTask {
151        private boolean mCanceled;
152
153        @Override
154        public void run() {
155            mHandler.post(new HandlerRunnable());
156        }
157
158        @Override
159        public boolean cancel() {
160            mCanceled = true;
161            return super.cancel();
162        }
163
164        private class HandlerRunnable implements Runnable {
165            @Override
166            public void run() {
167                mRunningTimerTask = null;
168                if (!mCanceled) { // This check has to be done on the UI thread.
169                    if (DEBUG) debugLog("Kicking callback");
170                    mCallback.run();
171                }
172            }
173        }
174    }
175
176    // exposed for testing
177    public int getTimeoutForTest() {
178        return mTimeout;
179    }
180
181    // exposed for testing
182    public long getLastEventTimeForTest() {
183        return mLastEventTime;
184    }
185}
186