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. La implementación de estos pasos dependerá de la versión de Android en la que ejecutemos la app y vamos distinguir entre versiones iguales o posteriores a Lollipop y versiones anteriores. Esta distinción es imprescindible si queremos que nuestra app sea compatible con Android O (Api 26) ya que la implementación “tradicional” basada en servicios que veremos no funciona en esta versión de Android debido a los cambios en la gestión de procesos en segundo plano y veremos en el log el siguiente error:

system_process W/ActivityManager: Background start not allowed: service Intent { flg=0x4 cmp=com.danielme.android.autostartservice/.BackgroundIntentService (has extras) } to com.danielme.android.autostartservice/.BackgroundIntentService from pid=-1 uid=10085 pkg=com.danielme.android.autostartservice

Cursos aplicaciones móviles

pre-Lollipop: 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 encuentran 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" />

Lollipop : JobService

En Lollipop, tal y como veremos más adelante, utilizaremos JobScheduler, y en lugar de Intent Service vamos a emplear un JobService. En el método onStartJob implementaremos la misma lógica que acabamos de ver para el método onHandleIntent del IntentSevice. El JobService, a diferencia del IntentService, se ejecuta en el main thread de la aplicación por lo que si se ejecuta una tarea que no sea inmediata deberemos lanzarla de forma asíncrona para evitar bloqueos. Asimismo, tras su ejecución programamos la siguiente, esto lo veremos más adelante.

package com.danielme.android.autostartservice;

import android.app.job.JobParameters;
import android.app.job.JobService;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.RequiresApi;
import android.util.Log;
import android.widget.Toast;


@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class BackgroundJobService extends JobService {

    @Override
    public boolean onStartJob(JobParameters params) {
        Log.d(this.getClass().getSimpleName(),"onStartJob");
        Handler mHandler = new Handler(getMainLooper());
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(),R.string.service_message, Toast.LENGTH_SHORT).show();
            }
        });

        BootReceiver.scheduleJob(getApplicationContext());
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters jobParameters) {
        return false;
    }

}

El JobService debe ser definido en el manifest. Si se nos olvida, se produce la excepción java.lang.IllegalArgumentException: No such service ComponentInfo.

  <service
            android:name=".BackgroundJobService"
            android:permission="android.permission.BIND_JOB_SERVICE"/>

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.

pre-Lollipop: Programación del servicio con 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.

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. No obstante, para Lollipop usaremos la técnica que implementaremos en la sección siguiente.

Lollipop: Programación del servicio con JobScheduler

A partir de Lollipop disponemos del servicio de sistema JobScheduler desarrollado por Google con la mira puesta en optimizar el rendimiento de las tareas programadas para reducir el consumo de memoria y batería. Las tareas no sólo se pueden lanzar utilizando criterios de tiempo, sino que también se pueden imponer ciertas condiciones, como la disponibilidad de networking. Nosotros haremos un uso muy básico de JobScheduler ya que simplemente lanzaremos de forma periódica el JobService que vimos anteriormente.

Al igual que AlarmManager, el JobScheduler lo obtenemos como un servicio del sistema. La tarea a realizar se la proporcionamos en un JobInfo que crearemos mediante un builder. Podemos definir que la tarea se ejecute de forma periódica dentro de un intervalo de tiempo con el método setPeriodic aunque el intervalo mínimo es de 15 minutos. En el ejemplo vamos a usar un sistema alternativo consistente en programar la ejecución una vez durante una ventana de tiempo entre setMinimumLatency y setOverrideDeadline, y cuando se ejecute volvemos a programar una nueva ejecución. Puesto que esto último lo hacemos en la clase JobService, vamos crear un método público y estático en el BootReceiver para poder invocarlo desde nuestro JobService.

Utilizando AlarmManager para las versiones de Android pre-Lollipop, esto es, sin JobScheduler, nuestro BootReceiver queda tal y como sigue.

package com.danielme.android.autostartservice;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.RequiresApi;
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();

        if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            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);
        } else {
            scheduleJob(context);
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public static void scheduleJob(Context context) {
        ComponentName serviceComponent = new ComponentName(context, BackgroundJobService.class);
        JobInfo.Builder builder = new JobInfo.Builder(0, serviceComponent);
        //builder.setPeriodic(PERIOD_MS);
        builder.setMinimumLatency(PERIOD_MS);
        builder.setOverrideDeadline(PERIOD_MS);
        JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        jobScheduler.schedule(builder.build());
    }
}

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 o job y el servicio se ejecutará cada 5 segundos. El siguiente log pertenece a Android KitKat.

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

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.

Master Pyhton, Java, Scala or Ruby

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: