Skip to content

Instantly share code, notes, and snippets.

@bverc
Forked from Gautier/CountDownTimer.java
Created December 18, 2011 07:49
Show Gist options
  • Star 39 You must be signed in to star a gist
  • Fork 17 You must be signed in to fork a gist
  • Save bverc/1492672 to your computer and use it in GitHub Desktop.
Save bverc/1492672 to your computer and use it in GitHub Desktop.
Drop-in alternative for the Android CountDownTimer class, but which you can cancel from within onTick. Modified to include pause and resume functionality.
/*
* Copyright (C) 2008 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 alt.android.os;
import android.os.Handler;
import android.os.SystemClock;
import android.os.Message;
/**
* Schedule a countdown until a time in the future, with
* regular notifications on intervals along the way.
*
* Example of showing a 30 second countdown in a text field:
*
* <pre class="prettyprint">
* new CountdownTimer(30000, 1000) {
*
* public void onTick(long millisUntilFinished) {
* mTextField.setText("seconds remaining: " + millisUntilFinished / 1000);
* }
*
* public void onFinish() {
* mTextField.setText("done!");
* }
* }.start();
* </pre>
*
* The calls to {@link #onTick(long)} are synchronized to this object so that
* one call to {@link #onTick(long)} won't ever occur before the previous
* callback is complete. This is only relevant when the implementation of
* {@link #onTick(long)} takes an amount of time to execute that is significant
* compared to the countdown interval.
*/
public abstract class CountDownTimer {
/**
* Millis since epoch when alarm should stop.
*/
private final long mMillisInFuture;
/**
* The interval in millis that the user receives callbacks
*/
private final long mCountdownInterval;
private long mStopTimeInFuture;
private long mPauseTime;
private boolean mCancelled = false;
private boolean mPaused = false;
/**
* @param millisInFuture The number of millis in the future from the call
* to {@link #start()} until the countdown is done and {@link #onFinish()}
* is called.
* @param countDownInterval The interval along the way to receive
* {@link #onTick(long)} callbacks.
*/
public CountDownTimer(long millisInFuture, long countDownInterval) {
mMillisInFuture = millisInFuture;
mCountdownInterval = countDownInterval;
}
/**
* Cancel the countdown.
*
* Do not call it from inside CountDownTimer threads
*/
public final void cancel() {
mHandler.removeMessages(MSG);
mCancelled = true;
}
/**
* Start the countdown.
*/
public synchronized final CountDownTimer start() {
if (mMillisInFuture <= 0) {
onFinish();
return this;
}
mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
mHandler.sendMessage(mHandler.obtainMessage(MSG));
mCancelled = false;
mPaused = false;
return this;
}
/**
* Pause the countdown.
*/
public long pause() {
mPauseTime = mStopTimeInFuture - SystemClock.elapsedRealtime();
mPaused = true;
return mPauseTime;
}
/**
* Resume the countdown.
*/
public long resume() {
mStopTimeInFuture = mPauseTime + SystemClock.elapsedRealtime();
mPaused = false;
mHandler.sendMessage(mHandler.obtainMessage(MSG));
return mPauseTime;
}
/**
* Callback fired on regular interval.
* @param millisUntilFinished The amount of time until finished.
*/
public abstract void onTick(long millisUntilFinished);
/**
* Callback fired when the time is up.
*/
public abstract void onFinish();
private static final int MSG = 1;
// handles counting down
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
synchronized (CountDownTimer.this) {
if (!mPaused) {
final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();
if (millisLeft <= 0) {
onFinish();
} else if (millisLeft < mCountdownInterval) {
// no tick, just delay until done
sendMessageDelayed(obtainMessage(MSG), millisLeft);
} else {
long lastTickStart = SystemClock.elapsedRealtime();
onTick(millisLeft);
// take into account user's onTick taking time to execute
long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
// special case: user's onTick took more than interval to
// complete, skip to next interval
while (delay < 0) delay += mCountdownInterval;
if (!mCancelled) {
sendMessageDelayed(obtainMessage(MSG), delay);
}
}
}
}
}
};
}
@tzachs
Copy link

tzachs commented Jun 30, 2012

1st, Thank you very much for this code, saved me a lot of trouble :)
2nd, I think there might be a problem with pause and then starting again.

Problem:
If countdown timer is paused and then started again, the counter will not work. The user must resume and then start.
I think it will be better that start should override the pause mode meaning:

It would be better to add mPaused = false; after mCancelled = false; in line 101

@bverc
Copy link
Author

bverc commented Jul 1, 2012

Good spotting. My implementation didn't require that, so I never noticed it missing. Have made the necessary change. Thanks.

@xomnot
Copy link

xomnot commented Mar 21, 2016

Thanks a lot!! You saved my day!!! :D

@mustafaerturk
Copy link

Thanks :D

@RishabSurana
Copy link

Nice implementation

@sashatru
Copy link

Great! Thanks a lot! It's the simpliest way to replace stock timers without any hard work.

@sbmaggarwal
Copy link

This is so excellent. Simple and great implementation !! This actually deserves to be added to actual Android classes.

@qvk
Copy link

qvk commented May 21, 2017

Hi, it seems like if the mCountDownInterval is long enough and pause() and resume() calls come consecutively from a client in between two adjacent ticks, too frequent onTick() callbacks will be scheduled incorrectly.

Example scenario: with interval = 100; onTick() at t0, MSG scheduled for t100. A client using this timer instance calls pause() and resume() at t40 and t60 respectively. Currently, resume() will schedule a MSG immediately and onTick() is called (at ~t60) and schedule another for t160. The previously scheduled MSG will also be handled and fire another tick at t100 and schedule another for t200 and so on. So, effectively, ticks are received every 60 and 40 ms. Also, there will be two onFinished() calls. More the pair of pause, resume calls between adjacent ticks, more the ticks scheduled, since the handler messages are only started and never stopped.

Looks like adding mHandler.removeMessages(MSG) to pause() should fix this.

@ClaudeHangui
Copy link

Great work...thx mate..
I do have a question: what if you need to use a specific timezone ?? How should I go about with it ?...
My app uses the Canadian timezone (Montreal to be precise....)

@billyjoker
Copy link

Hi, it seems like if the mCountDownInterval is long enough and pause() and resume() calls come consecutively from a client in between two adjacent ticks, too frequent onTick() callbacks will be scheduled incorrectly.

Example scenario: with interval = 100; onTick() at t0, MSG scheduled for t100. A client using this timer instance calls pause() and resume() at t40 and t60 respectively. Currently, resume() will schedule a MSG immediately and onTick() is called (at ~t60) and schedule another for t160. The previously scheduled MSG will also be handled and fire another tick at t100 and schedule another for t200 and so on. So, effectively, ticks are received every 60 and 40 ms. Also, there will be two onFinished() calls. More the pair of pause, resume calls between adjacent ticks, more the ticks scheduled, since the handler messages are only started and never stopped.

Looks like adding mHandler.removeMessages(MSG) to pause() should fix this.

That made the trick!! Thanks a lot!!

@prabhatsdp
Copy link

Thanks a lot! You saved my time, effort and energy.

@kiranshrestha
Copy link

Great Help, Thanks a lot.

@gagangamer
Copy link

It's working fine, but I'm getting this warning where you handles countdown : this handler class should be static or leaks might occur (anonymous android.os.handler)

@ngThu
Copy link

ngThu commented Jun 26, 2021

It's working fine, but I'm getting this warning where you handles countdown : this handler class should be static or leaks might occur (anonymous android.os.handler)

try this:

// handles counting down
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@OverRide
public void handleMessage(Message msg) {
synchronized (CountDownTimerWithPause.this) {
long millisLeft = timeLeft();

            if (millisLeft <= 0) {
                cancel();
                onFinish();
            } else if (millisLeft < mCountdownInterval) {
                // no tick, just delay until done
                sendMessageDelayed(obtainMessage(MSG), millisLeft);
            } else {
                long lastTickStart = SystemClock.elapsedRealtime();
                onTick(millisLeft);

                // take into account user's onTick taking time to execute
                long delay = mCountdownInterval - (SystemClock.elapsedRealtime() - lastTickStart);

                // special case: user's onTick took more than mCountdownInterval to
                // complete, skip to next interval
                while (delay < 0) delay += mCountdownInterval;

                sendMessageDelayed(obtainMessage(MSG), delay);
            }
        }
    }
};

@broscr
Copy link

broscr commented Jul 5, 2022

So, is it possible to add a delay before every resume ?
So what should we do if we want to add a delay before the time starts to decrease ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment