CountDown Timer in Android

CountDown Timer for Android App

Hey Guys. This is another useful post which solves another common problem. If you frequently need to use a countdown timer inside your apps for building tracking,verification or delay screens then this CountDown Timer I developed is tailored to your needs. You can create as many timer objects needed.

(Java 8 or Kotlin), Android (CountDown Timer,Handler,Handler Thread and basics of Multi-threading). The Android portion is only necessary if you want to understand the timer code in details.

This project is available for both Java and Kotlin including examples. But for the majority of readers I will be explaining the java example here.

Let’s get started

First, You will need to create an instance/object of SimpleCountDownTimer class in your activity or fragment. Since the timer object doesn’t rely on activity or fragment life-cycle it can be safely declared as a final object like a field below or in a constructor. When you create the timer object it is required to provide countdown minutes,seconds,delay and a countdown listener as well to the constructor.

private final SimpleCountDownTimer simpleCountDownTimer = new SimpleCountDownTimer(0, 10, 1, this);

Then by calling simpleCountDownTimer.start(false) method you can start the timer and registered listener will receive updates from ticks with the latest time after provided delay. For example, the listener will receive ticking event for a countdown of 10 seconds with delay of 1 second in above example. After 1 second below callback will be invoked on the main thread by default. And when the countdown is finished the subsequent one is invoked once.

   @Override
public void onCountDownActive(String time)
{

}
@Override
public void onCountDownFinished()
{


}

Let’s understand the java activity example below.

public class Main2Activity extends AppCompatActivity implements SimpleCountDownTimer.OnCountDownListener {
private TextView textView;
private Button start;
private Button resume;

private final SimpleCountDownTimer simpleCountDownTimer = new SimpleCountDownTimer(0, 10, 1, this);

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.tv);
start = findViewById(R.id.startBtn);
resume = findViewById(R.id.resumeBtn);
Button pause = findViewById(R.id.pauseBtn);

resume.setEnabled(false);

start.setOnClickListener(view -> {
simpleCountDownTimer.start(false);

start.setEnabled(false);

});

resume.setOnClickListener(view -> {
simpleCountDownTimer.start(true);
simpleCountDownTimer.runOnBackgroundThread();
});
pause.setOnClickListener(view -> {
simpleCountDownTimer.pause();
simpleCountDownTimer.setTimerPattern("s");
resume.setEnabled(true);
});


}


@Override
public void onCountDownActive(String time) {

textView.post(() -> textView.setText(time));

Toast.makeText(this, "Seconds = " + simpleCountDownTimer.getSecondsTillCountDown() + " Minutes=" + simpleCountDownTimer.getMinutesTillCountDown(), Toast.LENGTH_SHORT).show();

}

@Override
public void onCountDownFinished() {
textView.post(() -> {
textView.setText("Finished");
start.setEnabled(true);
resume.setEnabled(false);
});


}

A simple activity demonstrating how the countdown timer works with 3 buttons.

When start button clicked, The timer starts from the provided minutes and seconds and the button is disabled to prevent abnormal operation.

When resume button clicked, The timer resumes operation from the time it was paused at. You would have noticed that the start(true) method of the timer takes a true/false parameter. It’s self-explanatory but when you pass true to the method it resumes the timer whereas false starts/restarts it. Only on completion or paused, the start method can be invoked again with false parameter to restart the timer.

When pause button clicked, pause() method is invoked and the countdown pauses. It’s safe to call this method whenever required.

setTimerPattern(String pattern): The method is used to set the appearance of countdown time the patterns can be found in its documentation. This method can be called anytime safely.

runOnBackgroundThread(): Only a single call to this method will move entire countdown operation to a background thread and the countdown listener callbacks onCountDownActive(String time) and onCountDownFinished() will be invoked on background thread only that is why view’s post method is used in example above which will post countdown time to main thread’s message queue asynchronously. For now,you cannot switch the timer to main thread again after a call to this method is made for a timer instance. Also note the timer keeps running whether on main thread or not even if activity is paused due to usage of handler.If your timer is already running on a background thread then more calls to this method will have no effect.

What’s the abnormal operation ?

This timer works on recursive approach. Improper use of start method can lead to multi-recursions/messages. This behaviour is identical to improper use of start method of official countdown timer.

Thing to note: There might be a possibility that the occurrence of this identical behaviour is indicative of same recursive approach being used in the android’s official countdown timer.

That covers up all about the timer. If you are interested in learning how it works, then keep reading.

The code for the simple countdown timer is below.

/**
* A simple countdown timer identical to android.os.CountDownTimer but with simplified usage and additional functionality to pause and resume.
* Note: This timer runs on UI thread by default but that can be changed by calling runOnBackgroundThread at any time.
*
@author Mobin Munir
*/
public final class SimpleCountDownTimer {
private OnCountDownListener onCountDownListener;
private long fromMinutes;
private long fromSeconds;
private long delayInSeconds = 1;
private Calendar calendar = Calendar.getInstance();
private long seconds, minutes;
private boolean finished;
private Handler handler = new Handler();
private HandlerThread handlerThread;
private boolean isBackgroundThreadRunning;
private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss", Locale.getDefault());
private Runnable runnable = this::decrementMinutes;

/**
*
@param fromMinutes minutes to countdown.
*
@param fromSeconds seconds to countdown.
*
@param onCountDownListener A listener for countdown ticks.
*
@param delayInSeconds optional delay in seconds for a tick to execute default is 1 second.
*/
public SimpleCountDownTimer(long fromMinutes, long fromSeconds, long delayInSeconds, OnCountDownListener onCountDownListener) {

if (fromMinutes <= 0 && fromSeconds <= 0)
throw new IllegalStateException(getClass().getSimpleName() + " can't work in state 0:00");

if (delayInSeconds > 1)
this.delayInSeconds = delayInSeconds;

this.onCountDownListener = onCountDownListener;


setCountDownValues(fromMinutes, fromSeconds);


}

/**
* This method sets business logic for countdown operation before it starts.
*/
private void setCountDownValues(long fromMinutes, long fromSeconds) {
this.fromMinutes = fromMinutes;
this.fromSeconds = fromSeconds;
minutes = this.fromMinutes;

if (fromMinutes > 0 && fromSeconds <= 0) {
seconds = 0;
return;
}

if (fromSeconds <= 0 || fromSeconds > 59) {
seconds = 59;
return;
}

seconds = this.fromSeconds;

}


/**
*
@return This method returns seconds till countdown.
*/
public long getSecondsTillCountDown() {
return seconds;
}

/**
*
@return This method returns minutes till countdown.
*/
public long getMinutesTillCountDown() {
return minutes;
}

/**
* Sets a new pattern for SimpleDateFormat for time returned on each tick.
*
*
@param pattern only acceptable "mm:ss","m:s","mm","ss","m","s".
*/

public void setTimerPattern(String pattern) {
if (pattern.equalsIgnoreCase("mm:ss") || pattern.equalsIgnoreCase("m:s") || pattern.equalsIgnoreCase("mm") ||
pattern.equalsIgnoreCase("ss") || pattern.equalsIgnoreCase("m") || pattern.equalsIgnoreCase("s"))
simpleDateFormat.applyPattern(pattern);
}

/**
* This method call will permanently move the timer to run in background thread for this instance.
* A new thread is created which is then bound to timer's handler of main thread's message queue therefore overwriting it.
* This method can be invoked at any time.
* Note: onCountDownListener callbacks will not be invoked on main thread.
*/
public final void runOnBackgroundThread() {

if (isBackgroundThreadRunning)
return;

handlerThread = new HandlerThread(getClass().getSimpleName());

startBackgroundThreadIfNotRunningAndEnabled();

handler = new Handler(handlerThread.getLooper());


}

private void startBackgroundThreadIfNotRunningAndEnabled() {
if (handlerThread != null && !handlerThread.isAlive()) {
handlerThread.start();
isBackgroundThreadRunning = true;
}
}

/**
* No need to quit background thread once started.
* Quitting it kills it. Threads don't restart.
* This is just left here if needed for any reason in future.
*/
/* private void quitBackgroundThreadSafelyIfRunning() {

if (!isBackgroundThreadRunning)
return;

isBackgroundThreadRunning = !handlerThread.quitSafely();
}*/
@NotNull
private String getCountDownTime() {
calendar.set(Calendar.MINUTE, (int) minutes);
calendar.set(Calendar.SECOND, (int) seconds);
return simpleDateFormat.format(calendar.getTime());
}

private void decrementMinutes() {

seconds--;

if (minutes == 0 && seconds == 0) {
finish();
}

if (seconds < 0) {
if (minutes > 0) {
seconds = 59;
minutes--;
}

}


runCountdown();


}

private void finish() {
onCountDownListener.onCountDownFinished();
finished = true;
pause();
}

private void decrementSeconds() {
handler.postDelayed(runnable, TimeUnit.SECONDS.toMillis(delayInSeconds));

}

/**
* A method to start/resume countdown.
*
*
@param resume if true it will resume from where its paused else from start.
*/
public final void start(boolean resume) {

if (!resume) {
setCountDownValues(fromMinutes, fromSeconds);
finished = false;
}

runCountdown();


}

private void runCountdown() {
if (!finished) {
updateUI();
decrementSeconds();
}
}

private void updateUI() {
onCountDownListener.onCountDownActive(getCountDownTime());
}

/**
* A method to pause/stop countdown.
*/
public final void pause() {
handler.removeCallbacks(runnable);
}


/**
* A countdown listener to be used to listen for ticks and finish.
*/
public interface OnCountDownListener {
/**
* A method continuously called on ticking.
*
*
@param time The time at tick.
*/
void onCountDownActive(String time);

/**
* A method called once when countdown is finished.
*/
void onCountDownFinished();
}
}

When the call to the constructor is made, It first validates the given minutes and seconds. On success, All countdown values are adjusted for countdown by method setCountDownValues(long minutes,long seconds) else an illegalStateException is thrown. This is how countdown timer gets ready for use.

How does recursion works ?

When start() method is called, the runCountdown() method is also invoked. It updates UI for ticking by calling updateUI() and then calls decrementSeconds() method in which the handler is used to post a delayed runnable to decrement seconds after the specified delay which was provided in constructor. This then leads to decrementMinutes() method being called inside the run() method. When this happens, the seconds are decremented, then its checked whether the decrements have caused the minutes and seconds to be equal to 0 if they are then the countdown is finished by calling finish() method which marks the timer finished by setting the finish boolean flag to true which breaks recursion.

The basic formula involved is that when seconds field value reaches 0 minutes field value is decremented by 1 and runCountdown() method is called again this keeps recurring until both minutes and seconds field values equal to 0.

In the whole process, If pause() method is called it removes the runnable callback from handler therefore breaking the recursion.The method start(true) is a resume call and start(false) on finish or pause restarts the timer.

How does background thread work ?

When runOnBackgroundThread() method is called, a new Handler thread is created and started which is then used to recreate the handler with it. The method is validated to create the background thread only once and start it only if it is not already running a boolean flag isBackgroundThreadRunning is set to true with the 1st call to this method. Then each time this method is called again the flag is used to verify if its true it returns from method immediately nothing happens.

How does timer is formatted for appearance ?

Calendar API is used to set minutes and seconds on the timer and then return the time set by formatting with SimpleDateFormat. The method setTimerPattern() also uses the above mentioned object to format time appearance as per requirement and validates the provided pattern against supported ones. Default pattern is used if validation fails.

You can find the project on Github. It contains a Java and Kotlin activity as samples. You can clone the project in Android Studio to run it.

Last Step

Many thanks to everyone who read this. You can follow me here for updates and/or reach me at LinkedIn or Facebook. You can greatly help by starring and forking on github.

Lead Software Engineer (Android) @ Careem || Fitness Freak for 9 years

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store