android – Countdown with alarm

Question:

I have a project where I start a 10min action, for example, and so start a countdown.

This counter will be shown on a screen for management (it can add +1 minute, like the native android widget timer ).

When there are 15sec to go before this time expires, an alert will be played. It can have multiple open times.

I wanted to know if AlamManager do this for the persistence of this time, so it can manage to add +1minute.

Note: I still don't have any code to post, it's just a question of how I can do this.

Answer:

The AlarmManager "was not made" for this type of processing.

The documentation itself, anticipating possible misuse, states:

The Alarm Manager is intended for cases where you want to have your application code run at a specific time, even if your application is not currently running. For normal timing operations (ticks, timeouts, etc.) it is easier and much more efficient to use Handler.

AlarmeManager is intended for cases where you want to have your code run at a certain time, even if the application is not running. For normal timing operations (ticking, inaction intervals, etc) it is easier and much more efficient to use a Handler.

Android provides the CountDownTimer class that allows you to schedule a countdown, with notifications at regular intervals, during the countdown.
However, it doesn't fully fit the behavior you want, namely adding more time to the remaining time.

The solution will be to create a class, similar to CountDownTimer , but adapted to our needs:

CountDown.java (GitHubGist)

public class CountDown {

    //Interface a ser implementada por um listener
    public interface CountDownListener {
        //Chamado quando o valor de secondsLeft é alterado,
        //quando for decrementado ou incrementado.
        void onChange(long timeLeft);
        //Chamado quando o contador chegar ao fim.
        void onEnd();
    }

    private long fromSeconds;
    private long secondsLeft;
    private CountDownListener listener;
    private boolean isCounting = false;

    //Valor em milissegundos de um segundo.
    private static final long ONE_SECOND = 1000;
    private static final int MSG = 1;

    //Constrói o contador com o valor inicial de segundos.
    public CountDown(long fromSeconds){

        this.fromSeconds = fromSeconds;
        handler = new CountDownHandler(this);
    }

    //Inicia a contagem, a partir do valor inícial.
    public synchronized void start(){
        if(isCounting){
            return;//ou talvez lançar uma excepção
        }
        isCounting = true;
        secondsLeft = fromSeconds;
        handler.sendMessage(handler.obtainMessage(MSG));
    }

    //Pára a contagem.
    public synchronized void stop(){
        if(!isCounting){
            return;//ou talvez lançar uma excepção
        }
        isCounting = false;
        handler.removeMessages(MSG);
    }

    //Retoma a contagem.
    public synchronized void resume(){
        if(isCounting || secondsLeft == 0){
            return;//ou talvez lançar uma excepção
        }
        isCounting = true;
        handler.sendMessageDelayed(handler.obtainMessage(MSG), ONE_SECOND);
    }

    //Incrementa o valor do contador.
    public synchronized void increaseBy(long value){
        secondsLeft += value;
    }

    //true se o contador estiver contando.
    public boolean isCounting(){
        return isCounting;
    }

    //Guarda um listener.
    public void setCountDownListener(CountDownListener listener){
        this.listener = listener;
    }

    //Método para formatar um valor em segundos em algo tipo "mm:ss" ou "HH:mm:ss".
    public static String secondsToString(long seconds, String format){
        return DateFormat.format(format, seconds * ONE_SECOND).toString();
    }

    private final Handler handler;

    //Handler para controlar o contador
    private static class CountDownHandler extends Handler
    {

        private final WeakReference<CountDown> countDownWeakReference;

        private CountDownHandler(CountDown countDownInstance) {
            countDownWeakReference = new WeakReference<>(countDownInstance);
        }

        @Override
        public void handleMessage(Message msg) {

            CountDown countDown = countDownWeakReference.get();
            if(countDown == null){
                return;
            }

            synchronized (countDown) {

                //Guarda o instante em que inicia o processamento.
                long tickStart = SystemClock.elapsedRealtime();

                //Se tiver sido parado sai.
                if(!countDown.isCounting){
                    return;
                }

                //Notifica o listener com o segundos que faltam para terminar.
                if (countDown.listener != null) {
                    countDown.listener.onChange(countDown.secondsLeft);
                }

                //O contador chegou ao fim, notifica o listener.
                if (countDown.secondsLeft == 0) {
                    countDown.isCounting = false;
                    if (countDown.listener != null) {
                        countDown.listener.onEnd();
                    }
                } else {
                    //decrementa o contador.
                    countDown.secondsLeft--;

                    //Obtém o tempo para o próximo decremento.
                    //Leva em conta o tempo gasto no processamento,
                    //principalmente o eventualmente gasto pela implementação
                    // do método onChange() no listener.
                    long delay = ONE_SECOND - (SystemClock.elapsedRealtime() - tickStart);

                    //Se o tempo gasto for superior a um segundo, ajusta-o para o próximo.
                    //Se o tempo gasto no método onChange() for próximo ou
                    // superior a um segundo ele só será chamado no próximo.
                    while(delay < 0){
                        countDown.secondsLeft--;
                        delay += ONE_SECOND;
                    }
                    //Garante o término se o tempo for excedido
                    if(countDown.secondsLeft < 0){
                        countDown.listener.onEnd();
                    }else {
                        //Agenda o próximo decremento.
                        sendMessageDelayed(obtainMessage(MSG), delay);
                    }
                }
            }
        }
    };
}

The class allows you to create a counter with a specified number of seconds that, when started by the start() method, will count down to zero, notifying a listener , associated by the setCountDownListener() method, when the remaining time value is decremented or incremented through the increaseBy() method and when the counter runs out.
You can stop counting with the stop() method and resume it with the resume() method.

Our counter can now be used to control a class that encapsulates the behavior we want to have while counting.

As I understand it, the behavior you want is:

  • Display the remaining time value on the screen.
  • Trigger an alarm when the remaining time reaches 15 seconds from the end.
  • Being able to add more time to the counter (already implemented in CountDown )

CountDownBehavior.java

public abstract class CountDownBehavior implements CountDown.CountDownListener {

    private final long alarmTime;
    private final String displayFormat;

    public CountDownBehavior(long alarmTime, String displayFormat){

        //Valor em segundos no qual deve ser chamado onAlarm().
        this.alarmTime = alarmTime;
        //Formato da string passada ao displayTimeLeft().
        this.displayFormat = displayFormat;
    }

    @Override
    public void onChange(long timeLeft) {
        //Aqui é implementado o comportamento que queremos ter enquanto
        //o CountDown "conta".

        //Deve informar quando chegar a altura de accionar o alarma.
        if(timeLeft == alarmTime)
        {
            onAlarm();
        }
        //Informa o valor actual do contador, com o formato indicado por displayFormat.
        displayTimeLeft(CountDown.secondsToString(timeLeft, displayFormat));

    }

    //Metodos a implementar em resposta ao comportamento.
    protected abstract void onAlarm();
    protected abstract void displayTimeLeft(String timeLeft);
}

When associated with the counter, through the setCountDownListener() method, the counter will call the onChange() and onEnd() , every time the count is decremented and when it reaches the end.

The behavior response must be implemented in the onAlarm() and displayTimeLeft() , in a derived class or "inline", in an anonymous class.

Usage example:

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private TextView textView;
    private CountDown countDown;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView)findViewById(R.id.textView);

        //Cria o contador com 10 minuto
        countDown = new CountDown(10*60);

        //Cria e atribui um CountDownBehavior ao contador
        countDown.setCountDownListener(new CountDownBehavior(15, "mm:ss") {
            @Override
            public void onEnd() {
                Toast.makeText(MainActivity.this, "terminou", Toast.LENGTH_SHORT).show();
            }

            @Override
            protected void onAlarm() {
                Toast.makeText(MainActivity.this, "alarme", Toast.LENGTH_SHORT).show();
            }

            @Override
            protected void displayTimeLeft(String timeLeft) {
                textView.setText(timeLeft);
            }
        });
    }

    protected void startClick(View v){
        countDown.start();
    }

    protected void addClick(View v){
        countDown.increaseBy(60);
    }

    protected void stopClick(View v){
        countDown.stop();
    }

    protected void resumeClick(View v){
        countDown.resume();
    }

    @Override
    protected void onDestroy() {
        //Antes de sair deve parar o contador caso este esteja a contar
        if(countDown.isCounting()){
            countDown.stop();
        }
        super.onDestroy();
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="CountDown"
        android:id="@+id/textView" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Iniciar"
        android:id="@+id/button1"
        android:onClick="startClick"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Mais 1 minuto"
        android:id="@+id/button2"
        android:onClick="addClick"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Parar"
        android:id="@+id/button"
        android:onClick="stopClick"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="retomar"
        android:id="@+id/button3"
        android:onClick="resumeClick"/>

</LinearLayout>
Scroll to Top