The principle of using CountdownTimer in Android


   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

    As you can see at the code above, the timer value will be updated to a 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

    Another solution is keeping the app running in the background. A 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

    The right way is to take advantage of what the OS offers. The idea here is to run the countdown timer as long as the app is foregrounded, showing the progress to the user one second at the time, but saving the current time value to 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

    Declaring a SharedPrefences instance in a separated class:
PrefUtils.java
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();
    }
}
    A subclass of BroadcastReceiver to "listen" countdown timer is expired and make notification then:
TimeReceiver.java
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);
    }
}
    In the main activity, we have some important works:
  • When onPause() called, canceling the CountdownTimer instance and setting an AlarmManager includes a PendingIntent (initializing with TimeReceiver).
  • When  onResume() called (your activity becomes visible with user), you must canceling the AlarmManager, more importantly, you must initializing a CountdownTimer instance with current time value in the SharedPreferences, this value is updated into a TextView.
    And this is the main activity code:
MainActivity.java

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
    }
}
    This activity layout (xml file):
activity_main.xml
<?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>
    Never forget to add your BroadcastReceiver to AndroidManifest.xml:
AndroidManifest.xml
<?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>
    Running this application, we'll have this result:

Final thoughts

    Now you've learned the way to use countdown timer in Android without wasting your device memory or CPU. Remember that a good application is not consuming much RAM or CPU, hope this post is useful with your work, readers!
    References:

Share


Previous post
« Prev Post
Next post
Next Post »