Loupe

Android Oreo : migrer vers le JobScheduler (et arrêter les background service)

Nous l'avons déjà vu dans un précédent article sur les notifications locales, Android Oreo 8.0 apporte certains changements qui nécessite de modifier le code de nos applications sous peine de non fonctionnement sur cette nouvelle mouture. Les services en tâche de fond (background service) ne sont tout simplement plus supportés et il faut migrer vers une nouvelle API : le JobScheduler.

Qu'est ce que c'est ?

Le JobScheduler est un nouveau composant système apparu dans la version 5 (niveau d'API 21) du framework. L'idée est de proposer une infrastructure au niveau de l'OS afin de permettre la meilleure optimisation possible des traitements en tâche de fond et ainsi préserver les performances et la batterie de nos chers téléphones et tablettes. 

Le SDK propose ainsi des APIs permettant de programmer un traitement en fonction de plusieurs critères (périodiquement, sur un changement de contenu, etc.) et de s'assurer que certaines conditions sont bien remplies (connectivité au réseau, périphérique branché sur le secteur, etc.). Les développeurs Windows (Store ou UWP) devront retrouver des habitudes de programmation ici car cela ressemble fortement aux background tasks UWP.

Autre point important: la plateforme est suffisamment aboutie pour mettre en place un mécanisme de ré-essai automatique : si elle n'arrive pas à effectuer le traitement (c'est vous qui indiquez le succès ou l'échec bien sûr) alors elle va ré-essayer d'elle-même le traitement ! Par défaut, un algoritme exponentiel est utilisé mais il est possible de spécifier le votre si cela est souhaité.

Comment faisait-on avant ?

Une méthode couramment utilisée auparavant consiste à utiliser le système d'alarme pour diffuser un événément (Broadcaster) à une date précise et à partir d'un gestionnaire d'événement (BroadCastReceiver) on déclenche un service en tâche de fond (IntentService). Ce type de mécanisme est dorénavant interdit et provoque un crash sur Android 8.0 et plus faisant apparaître ce type d'erreur dans vos logs sur la console PlayStore :
Capture d’écran 2018-04-04 à 10.51.00.jpg

La création de l'événement périodique est faite de cette manière :

var context = Application.Context.ApplicationContext;

var alarmManager = (AlarmManager)context
                          .GetSystemService(Context.AlarmService);

// BroadcastReceiver à lancer
var launchBroadcastReceiverIntent = new Intent(
                      context, 
                      typeof(BackgroundUpdateBroadcastReceiver));

// intent = notre événement
var targetIntent = PendingIntent.GetBroadcast(
                              context, 
                              133333 /* not used*/, 
                              launchBroadcastReceiverIntent, 
                              PendingIntentFlags.UpdateCurrent);

// Calcul du différentiel entre epoch (Java) et ticks (.NET)
var t = new DateTime(1970, 1, 1) - DateTime.MinValue;
var epochDifferenceInSeconds = t.TotalSeconds;

// Conversion des ticks vers milliseconds
var utcAlarmTimeInMs = DateTime.UtcNow
                          .AddSeconds(-epochDifferenceInSeconds)
                          .Ticks / 10000;

// déclenchement toutes les 2 heures (120 minutes)
var recurrenceMs = 1000 * 60 * 120;

alarmManager.SetInexactRepeating(AlarmType.Rtc, 
                                  utcAlarmTimeInMs, 
                                  recurrenceMs, 
                                  targetIntent);

Le gestionnaire d'événement se contente alors de démarrer le service dans la surcharge de la méthode OnRecieve en appelant la méthode StartService du contexte passé en paramètre. Un service en tâche de fond étant simplement une classe dérivant d'IntentService. L'extrait de code ci-dessous utilise les attributs pour faire l'enregistrement des classes dans le manifest mais vous saurez facilement le reproduire si nécessaire :

[BroadcastReceiver( Enabled = true)]
public class BackgroundUpdateBroadcastReceiver : BroadcastReceiver
  {
   public override void OnReceive(Context context, Intent intent)
   {
      var serviceIntent = new Intent(context, 
                 typeof(BackgroundUpdateService));
      context.StartService(serviceIntent);
    }
}

[Service(Enabled = true, Exported = true)]
public class BackgroundUpdateService : IntentService
{
   protected override  void OnHandleIntent(Intent intent)
   {
    PerformBackgroundSyncAsync();
   }
}

Créer le JobService

La nouvelle façon de faire commence par la création d'un JobService qui n'est rien d'autre qu'une classe dérivant de ... JobService!  Cette classe nécessite de définir 2 méthodes :

  • OnStartJob : appellée par l'OS pour demander le début du traitement.
  • OnStopJob : appelée par l'OS pour indiquer que les conditions demandées pour le traitement ne sont plus disponibles.

Il est aussi nécessaire de demander la permission "BIND_JOB_SERVICE" pour ce service. Cela est fait simplement via un attribut en Xamarin ou directement dans le manifest : 

 <service android:name="MyJobService"
   android:permission="android.permission.BIND_JOB_SERVICE" >

Votre traitement peut avoir la forme que vous souhaitez et durer le temps nécessaire à celui-ci. Il y a 2 méthodes pour indiquer à l'infrastructure que le travail est terminé : retourner false dans la méthode OnStartJob ou retourner true pour indiquer que le travail continue sur un thread à part (par exemple) et appeler la méthode JobFinished une fois le traitement terminé.

Il est de votre responsabilité de créer un thread pour effectuer un traitement à part. La méthode JobFinished prends un second paramètre indiquant s'il y a eu une erreur pendant le traitement et s'il faut replanifier celui-ci. Voici un exemple enfantin d'implémentation avec Xamarin :

[Android.App.Service(
   Permission = "android.permission.BIND_JOB_SERVICE")]
public class BackgroundUpdateJobService : JobService
{
 private bool _success;

 public BackgroundUpdateJobService()
 { }

 protected BackgroundUpdateJobService(
  IntPtr javaReference,
  JniHandleOwnership transfer)
  : base(javaReference, transfer)
 { }

 public override bool OnStartJob(
  JobParameters parameters)
 {
  BackgroundWorkAsync(parameters);

  // true = traitement restant en tâche de fond
  return true;
 }

 private async Task BackgroundWorkAsync(JobParameters parameters)
 {
  try
  {

   // traitement métier spécifique sur un autre thread
   await Task.Run(PerformBackgroundSyncAsync);

   // pas d'erreur = pas de replanification
   this.JobFinished(parameters, false);
  }
  catch (Exception e)
  {
   // erreur = demande de replanification
   this.JobFinished(parameters, true);
  }
 }

 private Task PerformBackgroundSyncAsync()
 {

  // ... travail
 }
}

Finalement, l'OS peut appeler la méthode OnStopJob pour indiquer qu'il faut terminer votre traitement (conditions requises non remplies, besoin de libérer de la mémoire, etc.). Dans ce cas là il est nécessaire de libérer les ressources éventuellement acquises et de stopper le traitement. Cette méthode doit retourner un booléen indiquant si le traitement annulé devra être réessayer par l'OS plus tard. Avec Xamarin on peut utilser un CancellationTokenSource pour modéliser ce fonctionnement :

private CancellationTokenSource _cancellation 
            = new CancellationTokenSource();
public override bool OnStopJob(JobParameters @params)
{
   _cancellation.Cancel();

   // replanification
   return true;
}

Programmer le JobService

Une fois notre tâche de fond écrite, il faut la planifier. Pour cela on va instancier un objet JobInfo à l'aide de l'utilitaire JobInfo.Builder. On pourra alors, à l'aide d'une API fluent, décrire quand doit être executé la tâche et sous quelle condition. Vous devrez aussi fournir un identifiant pour notre planification et cela permettra une éventuelle annulation par la suite.

Voici une liste non-exhaustive des API intéressantes du JobInfo.Builder concernant la planification :

  • SetPersisted : est-ce que la tâche doit être reprogrammé automatiquement après un redémarrage du téléphone. Attention, il faut ajouter la permission RECEIVE_BOOT_COMPLETED si activation.
  • SetMinimumLatency : durée minimum à attendre avant de déclencher le traitement si l'on ne veut pas qu'il soit lancé tout de suite.
  • SetOverrideDeadline : durée maximum à attendre avant de lancer le traitement.
  • SetPeriodic : est-ce que l'on doit déclencher ce traitement de fond régulièrement. Les 2 propriétés précédentes sont incompatibles avec celle-ci et lève une exception si utilisée.
  • SetBackoffCriteria : permet de configurer le mécanisme de ré-essai (durée et algorithme linéaire ou exponentielle).

Voici une liste non-exhaustive des API intéressantes du JobInfo.Builder concernant les critère de lancement :

  • setRequiredNetworkType : permet d'indiquer le type de réseau du périphérique - wifi, cellulaire, aucun, etc.
  • setRequiresBatteryNotLow : demande à ce que la batterie soit chargé un minimum.
  • setRequiresCharging : demande que le device soit en charge.
  • setRequiresDeviceIdle : demande que l'utilisateur ne manipule pas son device pour lancer la tâche.
  • setRequiresStorageNotLow : demande d'avoir un miniumum de place disponible.

À noter que la dernière version du SDK propose de pouvoir déclencher un JobService à partir d'un ContentProvider. Vous vous souvenez, nous en avions parlé pour créer des notifications distantes. Pour mettre cela en place on utilise la méthode addTriggerContentUri pour indiquer des URIs à surveiller.

Si l'on reprends le code du début de l'article, on va planifier notre tâche de fond avec cette méthode :

var backgroundJobId = 1234;
var recurrenceMs = 1000 * 60 * 120;

var javaClass = Java.Lang.Class
       .FromType(typeof(BackgroundUpdateJobService));
var component = new ComponentName(context, javaClass);

var builder = new JobInfo.Builder(backgroundJobId, component)
    .SetPersisted(true)
    .SetPeriodic(recurrenceMs)
    .SetRequiredNetworkType(NetworkType.Any);
    
JobInfo jobInfo = builder.Build();

Et bien sûr, il ne faut pas oublier la planification elle-même en utilisant le JobSchedulerService récupéré à partir d'un contexte : 

var jobSchedulerService = (JobScheduler)context
   .GetSystemService(Context.JobSchedulerService);

var success = jobSchedulerService.Schedule(jobInfo);

Avoir un seul code pour tous mes traitements : Firebase JobDispatcher !

Il est parfois (toujours ?) intéressant d'avoir une seule base de code. Pour cela il est possible d'utiliser le Firebase JobDispatcher. Cette librairie Open-Source fonctionne avec le code natif sur les versions Android >= 8.0 et se base sur le Google Play Services pour les version antérieures de l'OS.

 

Happy coding :)

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus