Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Owner 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

This comment has been minimized.

Copy link

xomnot commented Mar 21, 2016

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

@mustafaerturk

This comment has been minimized.

Copy link

mustafaerturk commented Apr 4, 2016

Thanks :D

@RishabSurana

This comment has been minimized.

Copy link

RishabSurana commented Jun 14, 2016

Nice implementation

@sashatru

This comment has been minimized.

Copy link

sashatru commented Jun 27, 2016

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

@sbmaggarwal

This comment has been minimized.

Copy link

sbmaggarwal commented Oct 22, 2016

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

@qvk

This comment has been minimized.

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

This comment has been minimized.

Copy link

ClaudeHangui commented May 27, 2017

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

This comment has been minimized.

Copy link

billyjoker commented Sep 14, 2018

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!!

@iPrabhat404

This comment has been minimized.

Copy link

iPrabhat404 commented Feb 7, 2019

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

@kiranshrestha

This comment has been minimized.

Copy link

kiranshrestha commented May 16, 2019

Great Help, Thanks a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.