Countdown timer is an exciting topic in Android development and it has many practical applications. In Android SDK,
CountdownTimer
is the official class which help us to make a countdown stream. For example, initializing a CountdownTimer
with this code:
new CountDownTimer(30000, 1000) {
public void onTick(long millisUntilFinished) {
mTextField.setText("seconds remaining: " + millisUntilFinished / 1000);
}
public void onFinish() {
mTextField.setText("done!");
}
}.start();
Disadvantage of the "original" CountdownTimer
TextView
after every 1 second, give us a truly "countdown text".
This solution could be acceptable in the desktop/server, but it’s far from acceptable in the Android context: if the app goes in background because user wants to do other works (use other apps installed in the device), the operating system is likely to reclaim the resources and shutdown the app itself. In any case, the device will turn off after a short time. If you think that using a wakelock will solve the problem (make your device screen always on)… it will, but the user won’t be well of all the battery wasted by the screen.Disadvantage of using Service
Service
is an Android component made specifically for this purpose. Your app will stay alive through the whole length of the timer. When the timer is finished, it just has to throw a notification and a broadcast so the user will know that the timer expired.This approach will work, but it has a drawback. Your app (or let’s say at least the service) needs to be running for the whole length of the timer. This is a waste of memory (RAM) and CPU.
The right solution
SharedPreferences
whenever the app goes in background. Whenever the user gets back to the app, you’ll get this value and restart the timer from where it is supposed to start.From the user’s perspective, the timer is running even if the app is in background, because whenever they return to the app they see what they is expecting to see (the time passed). On the other hand, if the timer expires when the app is in background, a notification will remind users the countdown timer was stopped (I'll show
Notification
by using BroadcastReceiver
).
Sample project code
SharedPrefences
instance in a separated class:
PrefUtils.java
A subclass of package info.devexchanges.countdowntimer;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
public class PrefUtils {
private static final String START_TIME = "countdown_timer";
private SharedPreferences mPreferences;
public PrefUtils(Context context) {
mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
}
public int getStartedTime() {
return mPreferences.getInt(START_TIME, 0);
}
public void setStartedTime(int startedTime) {
SharedPreferences.Editor editor = mPreferences.edit();
editor.putInt(START_TIME, startedTime);
editor.apply();
}
}
BroadcastReceiver
to "listen" countdown timer is expired and make notification then:
TimeReceiver.java
In the main activity, we have some important works:package info.devexchanges.countdowntimer;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.support.v7.app.NotificationCompat;
public class TimeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent i = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pIntent = PendingIntent.getActivity(context, 0, i, 0);
NotificationCompat.Builder b = new NotificationCompat.Builder(context);
Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
b.setSound(notification)
.setContentTitle("Countdown Timer Receiver")
.setAutoCancel(true)
.setContentText("Timer has finished!")
.setSmallIcon(android.R.drawable.ic_notification_clear_all)
.setContentIntent(pIntent);
Notification n = b.build();
NotificationManager mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.notify(0, n);
}
}
- When
onPause()
called, canceling theCountdownTimer
instance and setting anAlarmManager
includes aPendingIntent
(initializing withTimeReceiver
). - When
onResume()
called (your activity becomes visible with user), you must canceling theAlarmManager
, more importantly, you must initializing aCountdownTimer
instance with current time value in theSharedPreferences
, this value is updated into aTextView
.
MainActivity.java
This activity layout (xml file):
package info.devexchanges.countdowntimer;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import java.util.Calendar;
public class MainActivity extends AppCompatActivity {
private PrefUtils prefUtils;
private TextView timerText;
private TextView noticeText;
private View btnStart;
private CountDownTimer countDownTimer;
private int timeToStart;
private TimerState timerState;
private static final int MAX_TIME = 12; //Time length is 12 seconds
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
noticeText = (TextView) findViewById(R.id.notice);
timerText = (TextView) findViewById(R.id.timer);
btnStart = findViewById(R.id.button);
prefUtils = new PrefUtils(getApplicationContext());
btnStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (timerState == TimerState.STOPPED) {
prefUtils.setStartedTime((int) getNow());
startTimer();
timerState = TimerState.RUNNING;
}
}
});
}
@Override
protected void onResume() {
super.onResume();
//initializing a countdown timer
initTimer();
updatingUI();
removeAlarmManager();
}
@Override
protected void onPause() {
super.onPause();
if (timerState == TimerState.RUNNING) {
countDownTimer.cancel();
setAlarmManager();
}
}
private long getNow() {
Calendar rightNow = Calendar.getInstance();
return rightNow.getTimeInMillis() / 1000;
}
private void initTimer() {
long startTime = prefUtils.getStartedTime();
if (startTime > 0) {
timeToStart = (int) (MAX_TIME - (getNow() - startTime));
if (timeToStart <= 0) {
// TIMER EXPIRED
timeToStart = MAX_TIME;
timerState = TimerState.STOPPED;
onTimerFinish();
} else {
startTimer();
timerState = TimerState.RUNNING;
}
} else {
timeToStart = MAX_TIME;
timerState = TimerState.STOPPED;
}
}
private void onTimerFinish() {
Toast.makeText(this, "Countdown timer finished!", Toast.LENGTH_SHORT).show();
prefUtils.setStartedTime(0);
timeToStart = MAX_TIME;
updatingUI();
}
private void updatingUI() {
if (timerState == TimerState.RUNNING) {
btnStart.setEnabled(false);
noticeText.setText("Countdown Timer is running...");
} else {
btnStart.setEnabled(true);
noticeText.setText("Countdown Timer stopped!");
}
timerText.setText(String.valueOf(timeToStart));
}
private void startTimer() {
countDownTimer = new CountDownTimer(timeToStart * 1000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
timeToStart -= 1;
updatingUI();
}
@Override
public void onFinish() {
timerState = TimerState.STOPPED;
onTimerFinish();
updatingUI();
}
}.start();
}
public void setAlarmManager() {
int wakeUpTime = (prefUtils.getStartedTime() + MAX_TIME) * 1000;
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(this, TimeReceiver.class);
PendingIntent sender = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
am.setAlarmClock(new AlarmManager.AlarmClockInfo(wakeUpTime, sender), sender);
} else {
am.set(AlarmManager.RTC_WAKEUP, wakeUpTime, sender);
}
}
public void removeAlarmManager() {
Intent intent = new Intent(this, TimeReceiver.class);
PendingIntent sender = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
am.cancel(sender);
}
private enum TimerState {
STOPPED,
RUNNING
}
}
activity_main.xml
Never forget to add your <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="info.devexchanges.countdowntimer.MainActivity">
<TextView
android:id="@+id/timer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
<TextView
android:id="@+id/notice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/timer"
android:layout_marginTop="@dimen/activity_horizontal_margin" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/notice"
android:layout_marginTop="@dimen/activity_horizontal_margin"
android:text="Start" />
</RelativeLayout>
BroadcastReceiver
to AndroidManifest.xml
:
AndroidManifest.xml
Running this application, we'll have this result:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="info.devexchanges.countdowntimer">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".TimeReceiver"/>
</application>
</manifest>
Final thoughts
References:
- Countdown Timer post from Google Developer
- Countdown Timer post from fedepaol.github.io