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 
17 package com.android.mail.utils;
18 
19 import android.os.Handler;
20 import android.util.Log;
21 
22 import java.util.Timer;
23 import 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  */
34 public 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 */
Throttle(String name, Runnable callback, Handler handler)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 */
Throttle(String name, Runnable callback, Handler handler,int minTimeout, int maxTimeout)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
Throttle(String name, Runnable callback, Handler handler,int minTimeout, int maxTimeout, Clock clock, Timer timer)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 
debugLog(String message)100     private void debugLog(String message) {
101         Log.d(LOG_TAG, "Throttle: [" + mName + "] " + message);
102     }
103 
isCallbackScheduled()104     private boolean isCallbackScheduled() {
105         return mRunningTimerTask != null;
106     }
107 
cancelScheduledCallback()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
updateTimeout()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 
onEvent()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
run()154         public void run() {
155             mHandler.post(new HandlerRunnable());
156         }
157 
158         @Override
cancel()159         public boolean cancel() {
160             mCanceled = true;
161             return super.cancel();
162         }
163 
164         private class HandlerRunnable implements Runnable {
165             @Override
run()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
getTimeoutForTest()177     public int getTimeoutForTest() {
178         return mTimeout;
179     }
180 
181     // exposed for testing
getLastEventTimeForTest()182     public long getLastEventTimeForTest() {
183         return mLastEventTime;
184     }
185 }
186