The AlarmManager was not "made" for this type of processing.
documentation , anticipating possible bad uses, refers to:
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.
AlarmManager is intended for cases where you want to have your code run at any given 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 class CountDownTimer that allows you to schedule a countdown, with interval notifications during the count.
However, it does not entirely fit the behavior you want, namely adding more time to the remaining time.
The solution will be to create a class, like CountDownTimer , but tailored 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 by notifying a listener , associated with the setCountDownListener()
, when the value of the remaining time is decremented or incremented by the increaseBy()
method and when the counter reaches the end.
It is possible to stop counting with the stop()
method and retake it with the resume()
method.
Our counter can now be used to control a class that encapsulates the behavior we wish to have during counting.
From what I understand, the behavior you want is:
- Display the amount of time remaining on the screen.
- Activate an alarm when the remaining time reaches 15 seconds.
- 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, using the setCountDownListener()
method, the counter will call the methods onChange()
and onEnd()
, every time the count is decremented and when it reaches the end.
The behavior response should be implemented in methods onAlarm()
and displayTimeLeft()
, in a derived class or "in line", in an anonymous class.
Example usage:
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>