Android: Servicio periódico con inicio automático

android

En este tutorial vamos a ver una solución para un escenario en el que necesitamos que una aplicación realice una tarea en segundo plano de forma periódica y sin intervención por parte del usuario, por ejemplo para realizar una sincronización de datos con un servidor. Esta tarea puede ser implementada con un servicio, “un componente de una aplicación que puede realizar operaciones de larga ejecución en segundo plano y que no proporciona una interfaz de usuario”. Además de implementar este servicio, tenemos que encontrar un modo de programar su ejecución periódica y asegurar que se ejecute aunque la aplicación no se esté en ejecutando en ese momento.

Vamos a implementar esta funcionalidad descomponiéndola en tres pasos: implementar el servicio con las operaciones que queremos realizar, detectar el arranque del dispositivo y programar la ejecución periódica del servicio en el arranque.

Servicio (IntentService)

Un servicio se define especializando en una clase pública la case Service o una de sus subclases. En la práctica se suelen implementar dos tipos de servicios: el propio Service o bien IntentService. El primero se ejecuta en segundo plano pero en el hilo principal de la aplicación, mientras que el segundo se ejecuta en un hilo independiente y se detiene automáticamente tras finalizar su ejecución. En nuestro ejemplo vamos a utilizar el segundo.

Tal y como hemos visto, crear nuestro servicio será tan sencillo como heredar de IntentService. Tenemos que implementar un constructor que simplemente devuelva un nombre para el servicio y el método onHandleIntent en el que se implementan las operaciones que deba realizar el servicio. Este método recibe un Intent que podemos utilizar para pasar parámetros al servicio.

Nota: Si usamos Dagger, podemos inyectar las dependencias que queramos en un servicio como si fuera una Activity o Fragment dentro de su método onCreate.

Para la aplicación demo de este tutorial, nuestro IntentService escribirá en el log y mostrará un Toast. Sin embargo esto plantea un problema ya que un IntentService no se ejecuta en el main thread de la aplicación sino en un hilo propio y el Toast queda asociado a este hilo. Si el IntentService termina su ejecución y el Toast se está mostrando tendremos un error como este:

Handler (android.os.Handler) {a508a720} sending message to a Handler on a dead thread
java.lang.RuntimeException: Handler (android.os.Handler) {a508a720} sending message to a Handler on a dead threadveStart.run(Native Method)

Este problema y su solución se encuentra en stackoverflow.

package com.danielme.android.autostartservice;

import android.app.IntentService;
import android.content.Intent;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.util.Log;
import android.widget.Toast;


public class BackgroundIntentService extends IntentService {

    public BackgroundIntentService() {
        super(BackgroundIntentService.class.getName());
    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        Log.d(this.getClass().getSimpleName(),"onHandleIntent");
        Handler mHandler = new Handler(getMainLooper());
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(),R.string.service_message, Toast.LENGTH_SHORT).show();

            }
        });
    }

}

Por último, para poder utilizar un servicio hay que definirlo en el manifest.

 <service android:name=".BackgroundIntentService" />
Detección de arranque (Broadcast receiver)

Android envía un mensaje de broadcast al terminar el arranque del sistema (ACTION_BOOT_COMPLETED) así que lo que haremos será “escucharlo” con un Broadcast Receiver. Esto ya la vimos en el tutorial Android Broadcast Receiver, por lo aquí sólo se indican los pasos a seguir.

  1. Implementar el broadcast receiver.
    package com.danielme.android.autostartservice;
    
    import android.content.BroadcastReceiver;
    import android.content.Context;
    import android.content.Intent;
    import android.widget.Toast;
    
    public class BootReceiver extends BroadcastReceiver {
    
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, R.string.boot_message, Toast.LENGTH_SHORT).show();
        }
        
    }
  2. Solicitar el permiso para recibirlo.
        <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    
  3. Registrar el broadcast receiver para el evento.
         <receiver android:name=".BootReceiver">
                <intent-filter>
                    <action android:name="android.intent.action.BOOT_COMPLETED" />
                </intent-filter>
            </receiver>
    

Por último, no podemos olvidar que para Android llame a nuestro BootReceiver la aplicación debe haber sido ejecutada al menos una vez.

Programación del servicio (AlarmManager)

El servicio AlarmManager permite programar la ejecución de código de nuestra aplicación y repetirla cada cierto intervalo de tiempo, incluso si la aplicación no está en ejecución en ese momento.

Nota: para Lollipop y superior podemos utilizar JobScheduler en lugar de AlarmManager que sacrifica precisión en la ejecución para hacer un uso más eficiente de los recursos del dispositivo.

Esta ejecución se realiza mediante un PendingIntent que debemos proporcionar al método setInexactRepeating, además del momento a partir del cual se debe registrar la ejecución de la alarma y los milisegundos para cada intervalo de repetición. Vamos a usar el modo de alarma RTC que no “despierta” el dispositivo si esté se encuentra suspendido en el momento de registrarse la alarma y espera a que el dispositivo despierte.

package com.danielme.android.autostartservice;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;

public class BootReceiver extends BroadcastReceiver {

    private static final int PERIOD_MS = 5000;

    @Override
    public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, R.string.boot_message, Toast.LENGTH_SHORT).show();

            Intent newIntent = new Intent(context, BackgroundIntentService.class);
            PendingIntent pendingIntent = PendingIntent.getService(context, 1, newIntent,
                    PendingIntent.FLAG_CANCEL_CURRENT);

            AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
            manager.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis(), PERIOD_MS,
                    pendingIntent);
    }
}

A partir de Lollipop, el intervalo mínimo de periodicidad entre ejecuciones es de un minuto. En nuestro ejemplo hemos puesto 5 segundos, así que en la práctica el intervalo en Lollipop y superior es de un minuto.

Probando el resultado

Ya tenemos implementado nuestro servicio periódico que se estará ejecutando siempre en segundo plano y sólo nos queda probarlo. Tras ejecutarse la aplicación por primera vez en todos los siguientes inicios del dispositivo se registrará nuestra alarma y el servicio se ejecutará cada 5 segundos antes de Lollipop:

07-09 14:06:46.001 1989-2006/com.danielme.android.autostartservice D/BackgroundIntentService: onHandleIntent
07-09 14:06:50.991 1989-2075/com.danielme.android.autostartservice D/BackgroundIntentService: onHandleIntent
07-09 14:06:56.001 1989-2150/com.danielme.android.autostartservice D/BackgroundIntentService: onHandleIntent

y cada minuto en Lollipop y superior:

07-08 00:11:14.755 2362-3147/com.danielme.android.autostartservice D/BackgroundIntentService: onHandleIntent
07-08 00:12:14.756 2362-4030/com.danielme.android.autostartservice D/BackgroundIntentService: onHandleIntent
07-08 00:13:14.757 2362-4909/com.danielme.android.autostartservice D/BackgroundIntentService: onHandleIntent
Proyecto de ejemplo

El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: