Programador de trabajos de Android

En esta guía se describe cómo programar trabajos en segundo plano mediante la API del programador de trabajos de Android, que está disponible en dispositivos Android que ejecutan Android 5.0 (nivel de API 21) y versiones posteriores.

Información general

Una de las mejores formas de mantener la respuesta de una aplicación Android al usuario es asegurarse de que el trabajo complejo o de larga duración se realice en segundo plano. Sin embargo, es importante que el trabajo en segundo plano no afecte negativamente a la experiencia del usuario con el dispositivo.

Por ejemplo, un trabajo en segundo plano puede sondear un sitio web cada tres o cuatro minutos para consultar los cambios en un conjunto de datos determinado. Este proceso, aparentemente benigno, tendría un efecto desastroso en la duración de la batería. La aplicación reactivará el dispositivo de forma repetida, elevará la CPU a un estado de energía mayor, encenderá los radios, realizará las solicitudes de red y, luego, procesará los resultados. La situación empeora porque el dispositivo no se apaga inmediatamente y vuelve al estado inactivo de baja energía. Un trabajo en segundo plano mal programado puede mantener involuntariamente el dispositivo en un estado con requisitos de energía innecesarios y excesivos. Esta actividad aparentemente inocente (sondear un sitio web) dejará al dispositivo inutilizable durante un período de tiempo relativamente corto.

Android proporciona las siguientes API para ayudar a realizar trabajos en segundo plano, pero por sí solas no son suficientes para la programación inteligente de trabajos.

  • Servicios de intención: los servicios de intención son excelentes para realizar el trabajo, pero no proporcionan ninguna forma de programarlo.
  • AlarmManager: estas API permiten programar el trabajo, pero no proporcionan ninguna forma de realizarlo. Además, AlarmManager solo permite restricciones basadas en el tiempo, lo que significa que se producirá una alarma en un momento determinado o transcurrido un período de tiempo determinado.
  • Receptores de difusión: una aplicación Android puede configurar receptores de difusión para que realicen algún trabajo en respuesta a eventos o intenciones de todo el sistema. Sin embargo, los receptores de difusión no proporcionan ningún control sobre cuándo se debe ejecutar el trabajo. Además, los cambios en el sistema operativo Android se restringirán cuando funcionen los receptores de difusión o los tipos de trabajo a los que puedan responder.

Hay dos características clave para realizar eficazmente el trabajo en segundo plano (a veces denominado tareas en segundo plano o trabajo):

  1. Programación inteligente del trabajo: es importante que cuando una aplicación realice trabajos en segundo plano lo haga de forma sensata. Lo ideal es que la aplicación no exija que se ejecute un trabajo. En su lugar, la aplicación debe especificar las condiciones que deben cumplirse para que se ejecute el trabajo y, luego, programar ese trabajo con el sistema operativo que lo realizará cuando se cumplan las condiciones. De esta manera, Android puede ejecutar el trabajo para garantizar la máxima eficacia en el dispositivo. Por ejemplo, las solicitudes de red se pueden procesar por lotes para ejecutarse todas al mismo tiempo con el fin de usar al máximo la sobrecarga que conllevan las redes.
  2. Encapsulación del trabajo: el código para realizar el trabajo en segundo plano debe encapsularse en un componente discreto que se pueda ejecutar con independencia de la interfaz de usuario y sea relativamente fácil de volver a programar si el trabajo no se completa.

El programador de trabajos de Android es un marco integrado en el sistema operativo Android que proporciona una API fluida para simplificar la programación de trabajos en segundo plano. El programador de trabajos de Android consta de los siguientes tipos:

  • Android.App.Job.JobScheduler es un servicio del sistema que se usa para programar, ejecutar y, si es necesario, cancelar los trabajos en nombre de una aplicación de Android.
  • Android.App.Job.JobService es una clase abstracta que se debe extender con la lógica que ejecutará el trabajo en el subproceso principal de la aplicación. Esto significa que JobService es responsable de cómo se realice el trabajo de forma asincrónica.
  • Un objeto Android.App.Job.JobInfo contiene los criterios para indicar a Android cuándo se debe ejecutar el trabajo.

Para programar el trabajo con el programador de trabajos de Android, una aplicación de Xamarin.Android debe encapsular el código en una clase que extienda la clase JobService. JobService tiene tres métodos de ciclo de vida a los que se puede llamar durante la vigencia del trabajo:

  • bool OnStartJob(JobParameters parameters): JobScheduler llama a este método para realizar el trabajo y se ejecuta en el subproceso principal de la aplicación. Es responsabilidad de JobService realizar el trabajo de forma asincrónica y devolver true si queda trabajo o false si se ha completado.

    Cuando JobScheduler llama a este método, solicitará y conservará un wakelock de Android mientras dure el trabajo. Una vez finalizado el trabajo, es responsabilidad de JobService indicar a JobScheduler este hecho mediante la llamada al método JobFinished (descrito a continuación).

  • JobFinished(JobParameters parameters, bool needsReschedule): este método se debe llamar mediante JobService para indicar a JobScheduler que el trabajo se ha completado. Si no se llama a JobFinished, JobScheduler no quitará el wakelock, lo que provocará un desgaste innecesario de la batería.

  • bool OnStopJob(JobParameters parameters): se llama cuando Android detiene el trabajo antes de tiempo. Debe devolver true si el trabajo se debe volver a programar en función de los criterios de reintento (que se describen a continuación con más detalle).

Es posible especificar restricciones o desencadenadores que controlarán cuándo un trabajo puede o debe ejecutarse. Por ejemplo, es posible restringir un trabajo para que solo se ejecute cuando el dispositivo se esté cargando o iniciar un trabajo cuando se haga una foto.

En esta guía se explica de forma detallada cómo implementar una clase JobService y programarla con JobScheduler.

Requisitos

El programador de trabajos de Android requiere el nivel de API 21 de Android (Android 5.0) o posterior.

Uso del programador de trabajos de Android

Para usar la API JobScheduler de Android, siga estos tres pasos:

  1. Implemente un tipo JobService para encapsular el trabajo.
  2. Use un objeto JobInfo.Builder para crear el objeto JobInfo que contendrá los criterios de JobScheduler para ejecutar el trabajo.
  3. Programe el trabajo mediante JobScheduler.Schedule.

Implementación de JobService

Todo el trabajo realizado por la biblioteca del programador de trabajos de Android debe realizarse en un tipo que extienda la clase abstracta Android.App.Job.JobService. Crear un objeto JobService es muy parecido a crear un objeto Service con el marco de Android:

  1. Extienda la clase JobService.
  2. Decore la subclase con ServiceAttribute y establezca el parámetro Name en una cadena formada por el nombre del paquete y el nombre de la clase (vea el ejemplo siguiente).
  3. Establezca la propiedad Permission de ServiceAttribute en la cadena android.permission.BIND_JOB_SERVICE.
  4. Invalide el método OnStartJob, y agregue el código para realizar el trabajo. Android invocará este método en el subproceso principal de la aplicación para ejecutar el trabajo. El trabajo que tarde más de unos milisegundos se debería ejecutar en un subproceso para evitar que se bloquee la aplicación.
  5. Cuando se complete el trabajo, JobService debe llamar al método JobFinished. Este método es el modo en que JobService indica a JobScheduler que se ha completado el trabajo. Si no se llama a JobFinished, JobService someterá al dispositivo a exigencias innecesarias, lo que acortará la duración de la batería.
  6. También es una buena idea reemplazar el método OnStopJob. Android llama a este método cuando se está cerrando el trabajo antes de que finalice y proporciona a JobService la oportunidad de eliminar los recursos correctamente. Este método debe devolver true si es necesario volver a programar el trabajo o false si no es aconsejable volver a ejecutarlo.

El código siguiente es un ejemplo del objeto JobService más sencillo para una aplicación, donde se usa TPL para realizar de forma asincrónica algo de trabajo:

[Service(Name = "com.xamarin.samples.downloadscheduler.DownloadJob", 
         Permission = "android.permission.BIND_JOB_SERVICE")]
public class DownloadJob : JobService
{
    public override bool OnStartJob(JobParameters jobParams)
    {            
        Task.Run(() =>
        {
            // Work is happening asynchronously
                      
            // Have to tell the JobScheduler the work is done. 
            JobFinished(jobParams, false);
        });

        // Return true because of the asynchronous work
        return true;  
    }

    public override bool OnStopJob(JobParameters jobParams)
    {
        // we don't want to reschedule the job if it is stopped or cancelled.
        return false; 
    }
}

Creación de JobInfo para programar un trabajo

Las aplicaciones de Xamarin.Android no crean instancias de JobService directamente, sino que pasarán un objeto JobInfo a JobScheduler. JobScheduler creará una instancia del objeto JobService solicitado, y programará y ejecutará JobService según los metadatos de JobInfo. Un objeto JobInfo debe contener la siguiente información:

  • JobId: es un valor de int que se utiliza para identificar un trabajo en JobScheduler. Al reutilizar este valor se actualizarán los trabajos existentes. El valor debe ser único para la aplicación.
  • JobService: este parámetro es un objeto ComponentName que identifica explícitamente el tipo que debe usar JobScheduler para ejecutar un trabajo.

Este método de extensión muestra cómo crear un objeto JobInfo.Builder con un elemento Context de Android, como es el caso de una actividad:

public static class JobSchedulerHelpers
{
    public static JobInfo.Builder CreateJobBuilderUsingJobId<T>(this Context context, int jobId) where T:JobService
    {
        var javaClass = Java.Lang.Class.FromType(typeof(T));
        var componentName = new ComponentName(context, javaClass);
        return new JobInfo.Builder(jobId, componentName);
    }
}

// Sample usage - creates a JobBuilder for a DownloadJob and sets the Job ID to 1.
var jobBuilder = this.CreateJobBuilderUsingJobId<DownloadJob>(1);

var jobInfo = jobBuilder.Build();  // creates a JobInfo object.

Una eficaz característica del programador de trabajos de Android es la posibilidad de controlar cuándo se ejecuta un trabajo o en qué condiciones se puede ejecutar. En la tabla siguiente se describen algunos de los métodos de JobInfo.Builder que permiten a una aplicación influir en cuándo se puede ejecutar un trabajo:

Method Descripción
SetMinimumLatency Especifica que se debe observar un retraso (en milisegundos) antes de que se ejecute un trabajo.
SetOverridingDeadline Declara que el trabajo debe ejecutarse antes de que transcurra este tiempo (en milisegundos).
SetRequiredNetworkType Especifica los requisitos de red de un trabajo.
SetRequiresBatteryNotLow El trabajo solo se puede ejecutar cuando el dispositivo no muestra una advertencia de "batería baja" al usuario.
SetRequiresCharging El trabajo solo se puede ejecutar cuando la batería se está cargando.
SetDeviceIdle El trabajo se ejecutará cuando el dispositivo esté ocupado.
SetPeriodic Especifica que el trabajo debe ejecutarse con regularidad.
SetPersisted El trabajo debe conservarse entre reinicios del dispositivo.

En SetBackoffCriteria se proporcionan instrucciones sobre cuánto tiempo debe esperar JobScheduler antes de intentar ejecutar de nuevo un trabajo. Los criterios de retroceso constan de dos partes: un retroceso en milisegundos (valor predeterminado de 30 segundos) y el tipo de retroceso que se debe usar (en ocasiones conocido como directiva de retroceso o directiva de reintentos). Las dos directivas se encapsulan en la enumeración Android.App.Job.BackoffPolicy:

  • BackoffPolicy.Exponential: una directiva de retroceso exponencial aumentará el valor de retroceso inicial de forma exponencial después de cada error. La primera vez que se produce un error en un trabajo, la biblioteca esperará el intervalo inicial especificado antes de volver a programar el trabajo; por ejemplo, 30 segundos. La segunda vez que se produce un error en el trabajo, la biblioteca esperará al menos 60 segundos antes de intentar ejecutar el trabajo. Después del tercer intento con error, la biblioteca esperará 120 segundos y así sucesivamente. Este es el valor predeterminado.
  • BackoffPolicy.Linear: esta estrategia es un retroceso lineal de que el trabajo se debe volver a programar para que se ejecute a intervalos establecidos (hasta que se realice correctamente). El retroceso lineal es más adecuado para el trabajo que debe realizarse lo antes posible o para los problemas que se resolverán rápidamente por sí solos.

Para más información sobre cómo crear un objeto JobInfo, lea la documentación de Google sobre la clase JobInfo.Builder.

Pasar parámetros a un trabajo a través de JobInfo

Los parámetros se pasan a un trabajo mediante la creación de un objeto PersistableBundle que se pasa junto con el método Job.Builder.SetExtras:

var jobParameters = new PersistableBundle();
jobParameters.PutInt("LoopCount", 11);

var jobBuilder = this.CreateJobBuilderUsingJobId<DownloadJob>(1)
                     .SetExtras(jobParameters)
                     .Build();

El acceso a PersistableBundle se realiza desde la propiedad Android.App.Job.JobParameters.Extras del método OnStartJob de un objeto JobService:

public override bool OnStartJob(JobParameters jobParameters)
{
    var loopCount = jobParams.Extras.GetInt("LoopCount", 10);
    
    // rest of code omitted
} 

Programación de un trabajo

Para programar un trabajo, una aplicación de Xamarin.Android obtendrá una referencia al servicio del sistema JobScheduler y llamará al método JobScheduler.Schedule con el objeto JobInfo creado en el paso anterior. JobScheduler.Schedule se devolverá inmediatamente con uno de dos valores enteros:

  • JobScheduler.ResultSuccess: el trabajo se ha programado correctamente.
  • JobScheduler.ResultFailure: el trabajo no se pudo programar. Esto se debe normalmente a conflictos de los parámetros JobInfo.

Este código es un ejemplo de programación de un trabajo y notificación al usuario de los resultados del intento de programación:

var jobScheduler = (JobScheduler)GetSystemService(JobSchedulerService);
var scheduleResult = jobScheduler.Schedule(jobInfo);

if (JobScheduler.ResultSuccess == scheduleResult)
{
    var snackBar = Snackbar.Make(FindViewById(Android.Resource.Id.Content), Resource.String.jobscheduled_success, Snackbar.LengthShort);
    snackBar.Show();
}
else
{
    var snackBar = Snackbar.Make(FindViewById(Android.Resource.Id.Content), Resource.String.jobscheduled_failure, Snackbar.LengthShort);
    snackBar.Show();
}

Cancelación de un trabajo

Es posible cancelar todos los trabajos programados o uno solo mediante el método JobsScheduler.CancelAll() o el método JobScheduler.Cancel(jobId):

// Cancel all jobs
jobScheduler.CancelAll(); 

// to cancel a job with jobID = 1
jobScheduler.Cancel(1)

Resumen

En esta guía se describe cómo usar el programador de trabajos de Android para realizar un trabajo en segundo plano de forma inteligente. Se explica cómo encapsular el trabajo para que se realice como un objeto JobService y cómo usar JobScheduler para programar ese trabajo, y especificar los criterios con JobTrigger; también se describe cómo se deben administrar los errores con RetryStrategy.