Android Services desde cero (II): servicios iniciados

`Decíamos en el primer post de esta serie que uno de los modos de ejecución clásicos de los componentes de tipo Service es el de servicio iniciado (1).  Los servicios iniciados soportan la misión fundamental de la clase Service, garantizando que el servicio permanecerá activo indefinidamente hasta que él mismo indique que ha terminado su trabajo o hasta que lo pare explícitamente otro componente.

Veamos en esta ocasión un ejemplo sencillo de cómo definir y utilizar un servicio en modo iniciado.

Arrancando motores

En este repositorio puedes encontrar el proyecto StartedService, que usaremos para ilustrar este post. El proyecto contiene una única actividad llamada TheActivity que se inicia al arrancar la aplicación y muestra una interfaz con un campo de entrada numérico y un botón con el texto Start. El usuario puede introducir un número en el campo de entrada y, al pulsar el botón, el número se envía a un servicio en segundo plano que cuenta de uno en uno hasta llegar al número introducido, volcando la cuenta en el log de la aplicación y esperando un segundo entre dos números consecutivos.

La acción comienza aquí:

 findViewById(R.id.startButton).setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       int target = 0;
       String input = ((TextView)findViewById(R.id.numberInput)).getText().toString();
       if (input.length() > 0) {
           target = Integer.parseInt(input);
       }
       CountService.startCount(target, TheActivity.this);
   }
 });

Este bloque de código incluye el método llamado cuando el usuario presiona el botón Start. En la última línea se llama al método estático startCount de la clase CountService, pasando como parámetros el número introducido por el usuario (target) y una referencia a la instancia de TheActivity.

El método startCount oculta a los componentes cliente los detalles relativos a la construcción y envío de peticiones de operación al servicio. Evita que cada clase cliente tenga que conocer la «ceremonia» necesaria. Veamos su contenido.

 public static void startCount(int countTarget, Context clientContext) {
   Intent requestIntent = new Intent(clientContext, CountService.class);
   requestIntent.setAction(ACTION_COUNT_TO);
   requestIntent.putExtra(EXTRA_COUNT_TARGET, countTarget);
   clientContext.startService(requestIntent);
 }

Para lanzar una petición a un servicio en modo iniciado se utiliza el método startService de la clase Context. En la última línea de startCount se efectúa esta llamada, pasando como único parámetro un objeto Intent creado a tal efecto. Dicho Intent actúa como contenedor de toda la información necesaria para expresar una petición de trabajo al servicio. En nuestro caso la información incluye:

  • La clase que implementa el servicio que procesará la petición, CountService.class, indicada en el constructor del Intent.
  • También en el constructor, la instancia de TheActivity recibida en el parámetro clientContext, la actividad que realiza la petición. En este punto se usa sólo para determinar la aplicación que contiene la clase de servicio. Podría utilizarse cualquier otra instancia de Context de la misma aplicación.
  • Con el método setAction se identifica la acción que se espera que ejecute CounService en segundo plano. Actúa como un identificador de comando. En nuestro ejemplo, CountService sólo sabe ejecutar esta acción, pero de forma general un servicio podría ejecutar cualquier número de operaciones, comandos o acciones que necesitemos.
  • En los extras del Intent añadimos los parámetros asociados a la acción a ejecutar. En este caso, se adjunta el número hasta el que se desea que cuente el servicio, procedente de la interfaz de usuario.

Una vez empaquetada toda la información de la petición en el Intent, el contexto cliente puede enviarla al servicio con startService.

Bajo el capó

Para implementar un servicio Android la clase CountService debe extender Service, ya sea directamente o extendiendo alguna de sus clases herederas. En este ejemplo la extensión es directa.

import android.app.Service;
... 
public class CountService extends Service {

Al extender Service es necesario implementar el método abstracto onBind. Éste método es fundamental para lanzar servicios en modo enlazado, pero ése no es nuestro objetivo ahora, así que simplemente lo completamos devolviendo null.

 @Nullable
 @Override
 public IBinder onBind(Intent intent) {
   // no binding today, null is OK
   return null;
 }

Otro punto indispensable es declarar el servicio en el fichero de manifiesto de la aplicación. Cuidado con olvidarlo, la compilación de la aplicación no falla en tal caso. Es muy sencillo olvidar incluirlo y, más tarde, volverse loco intentando averiguar por qué el servicio no hace nada en tiempo de ejecución.

<application
...
>
  <service android:name=".CountService" />
</application>

El método clave a sobrecargar para un servicio en modo iniciado es onStartCommand. Cuando se lanza una petición de servicio a través de startService el sistema garantiza que el objeto Intent entregado se hará llegar a una llamada al método onStartCommand de una instancia del servicio. En caso de que ya exista una instancia viva, la llamada se ejecuta en la misma. En caso contrario, el framework automáticamente crea un nuevo objeto de la clase CountService, lo inicializa ejecutando su método onCreate, y finalmente llama a onStartCommand.

Hilando fino

Un detalle muy importante a considerar es que el método onStartCommand se ejecuta en el hilo principal de la aplicación. Por ello, si nuestro servicio inicia la tarea solicitada directamente desde él, lo más probable es que produzca un error ANR (Application Not Responding) en nuestra aplicación. Cuando se crea un servicio extendiendo la clase Service directamente, es responsabilidad de nuestro código garantizar que la tarea ejecutada ocurre en un hilo en segundo plano.

En nuestro ejemplo se aborda el problema desde el fondo de las trincheras.

@Override
 public int onStartCommand(Intent intent, int flags, int startId) {
   Log.i(TAG, "Received start id " + startId + ": " + intent);
   Log.i(TAG, "Current thread is " + Thread.currentThread().getName());

   Message msg = mServiceHandler.obtainMessage();
   msg.arg1 = startId;
   msg.obj = intent;
   mServiceHandler.sendMessage(msg);

   return START_NOT_STICKY;
 }

La implementación de onStartCommand introduce en un mensaje (objeto de la clase Message) tanto el Intent recibido con la información sobre el trabajo solicitado, como el parámetro startId, un identificador generado automáticamente por el framework de Android para cada petición hecha a un mismo objeto servicio. El mensaje se envía al mismo objeto del que se obtiene, mServiceHandler, un elemento que se crea durante la inicialización del servicio sobrecargando el método onCreate.

@Override
  public void onCreate() {
    Log.i(TAG, "Creating...");

    HandlerThread backgroundThread = new HandlerThread(
      "CounterThread", 
      Process.THREAD_PRIORITY_BACKGROUND
    );
    backgroundThread.start();

    mServiceHandler = new CountHandler(backgroundThread.getLooper());
}

El manejador de servicio se crea pasando como parámetro un Looper asociado al hilo en segundo plano. Éste se crea como una instancia de HandlerThread, un tipo de hilo proporcionado por el framework de Android que ejecuta indefinidamente un bucle que en cada iteración puede procesar un mensaje o un objeto ejecutable enviado desde otros hilos. En el método onCreate de CounterService se crea el nuevo hilo, se arranca, y se usa su Looper para conectarlo con el manejador de servicio, una instancia de CountHandler, clase a la que se delega la responsabilidad de ejecutar las tareas solicitadas a CountService.

CountHandler extiende Handler, y lo hace sobrecargando el método handleMessage, que se ejecuta siempre en el hilo en segundo plano. El mensaje enviado desde el método onStartCommand cada vez que se recibe una petición en CountService se recibe en handleMessage, que es el punto apropiado para iniciar la tarea solicitada.

@Override
 public void handleMessage(Message msg) {
   Log.i(TAG, "Current thread is " + Thread.currentThread().getName());

   Intent request = (Intent) msg.obj;
   if (ACTION_COUNT_TO.equals(request.getAction())) {
     int target = request.getIntExtra(EXTRA_COUNT_TARGET, 0);
     countTo(target);
     reportResult(target);
   }

   // Stop the service using the startId, so that we don't stop
   // the service in the middle of handling another job
   stopSelf(msg.arg1);
 }

 private void countTo(int target) {
   for (int i=0; i<target; i++) {
     try {
       Thread.sleep(1000);
       Log.i(TAG, "" + (i+1));
     } catch (InterruptedException e) {
       Thread.currentThread().interrupt();
     }
   }
 }

En nuestro ejemplo se ve cómo se extrae el Intent con la petición de servicio del parámetro msg, se comprueba que corresponde a un tipo de acción conocida, se extraen los parámetros de la acción y finalmente se inicia la tarea solicitada en el método countTo: contar de uno en uno hasta el número introducido por el usuario.

El ojo del observador

Bonito, ¿verdad? ¿No? La belleza es tan subjetiva…

Lo que no es tan subjetivo es la contradicción de tener un componente, Service, supuestamente diseñado para ejecutar trabajos de larga duración en segundo plano y que cada vez que queramos sacarle partido tengamos que encargarnos personalmente de que realmente funcione en segundo plano.

Esta misma idea hizo clic en la cabeza de alguien en Google hace ya mucho tiempo, y llevó a la introducción de otra clase en el framework de Android diseñada para evitarnos pelear cuerpo a cuerpo con ningún hilo sólo para arrancar un servicio mínimamente funcional en modo iniciado. En nuestro ejemplo de hoy no la utilizamos. ¿Te atreves a buscarla? Cuéntanos qué encuentras en los comentarios.

Todo lo que empieza tiene que acabar

Hasta aquí hemos visto cómo ejecutar nuestra tarea de larga duración en segundo plano de forma independiente de la interfaz gráfica. El usuario de nuestro ejemplo puede realizar múltiples peticiones para contar hasta distintos números desde TheActivity sin tener que esperar a que la cuenta anterior haya terminado. O puede abandonar la aplicación, y dejar que siga contando el tiempo que necesite.

Tan independientes son TheActivity y CounterService que no hay forma de obtener una referencia de cualquiera de ellos en el otro.

Entonces, ¿cómo podemos mostrar al usuario los resultados o el progreso de las tareas en un servicio? Hay varias posibilidades para conseguirlo. CounterHandler incluye tres alternativas distintas para mostrar el resultado de nuestro trabajo, todas en el método reportResult. En futuros post hablaremos con detalle sobre resultados, pero hoy os invito a que veáis los tres métodos de este ejemplo en el primer vídeo de Solid Gear Bootcamp sobre servicios Android.

Otro detalle importante que debemos conocer ya sobre la terminación de tareas es la llamada al método stopSelf, justo al final de handleMessage. Decíamos al principio que un servicio en modo iniciado «permanecerá activo indefinidamente hasta que él mismo indique que ha terminado su trabajo o hasta que lo pare explícitamente otro componente».

Normalmente es preferible terminar el trabajo sin que nos interrumpan, y cuando así ocurre el servicio debe llamar al método stopSelf para indicar al sistema que ha terminado de procesar la petición que se arrancó con el identificador indicado en startId. Cuando se llama a stopSelf con el identificador de la última petición que ha llegado a onStartCommand, el sistema asume que CounterService no tiene trabajo pendiente y lo destruye.

Continuará

Se pueden comentar más cosas acerca de servicios en modo iniciado. En futuras entregas del blog probablemente tocaremos algunas de ellas, pero no os encariñéis demasiado con este modo de operación. Google está acabando con él.

Existen advertencias de ello desde hace cierto tiempo. Google recomienda explícitamente los servicios programados (2)  para dispositivos con Android 5 o superior, y existen perlas como ésta enterradas en el fondo de la documentación oficial. Ahora, con Android O a la vista, Google advierte que los servicios iniciados en segundo plano tendrán un tiempo de ejecución limitado, y el sistema los destruirá si lo superan aunque no se encuentre en una situación de escasez de recursos. Esto supone acabar con este modo de ejecución, puesto que su principal razón de existencia es la ejecución de tareas de larga duración.

Afortunadamente, hay más herramientas con las que trabajar en el segundo plano de Android. La siguiente de la que hablaremos en este blog son los servicios enlazados (3).

Otros enlaces relacionados

Solid GEAR Bootcamp: Servicios Android desde cero

Android Services desde cero

Usa la librería Android Priority Job Queue para tus tareas en segundo plano

 

(1) Started service.

(2) Scheduled services.

(3) Bound services.

Acerca de David A. Velasco

Arquitecto software y desarrollador móvil nativo con cierto favoritismo hacia Android. Obsesionado con la creación de valor para el usuario y la prevención de riesgos durante el desarrollo. Nada me emociona más que priorizar la pila de producto.

Deja un comentario

Responsable » Solidgear.
Finalidad » Gestionar los comentarios.
Legitimación » Tu consentimiento.
Destinatarios » Los datos que me facilitas estarán ubicados en los servidores SolidgearGroup dentro de la UE.
Derechos » Podrás ejercer tus derechos, entre otros, a acceder, rectificar, limitar y suprimir tus datos.

¿Necesitas una estimación?

Calcula ahora