Android : télécharger des fichiers avec le DownloadManager
Dans un précédent article nous avions vu comment utiliser les JobServices Android pour effectuer du travail en tâche de fond. Continuons la série en découvrant le DownloadManager qui permet de télécharger des fichiers de manière efficace, robuste et complètement intégrée dans Android.
Qu'est ce que c'est ?
Il s'agit d'un composant de la plateforme Android disponible depuis les API 9 (2.3 - GingerBread - la préhistoire) optimisé pour le téléchargement de fichier HTTP. Il va s'occuper pour vous de choisir le meilleur moment, de gérer les échecs, de gérer la reprise sur erreur et d'afficher la progression à l'utilisateur. Il s'agit donc du composant rêvé pour effectuer des téléchargements dans une application lorsque l'on connaît les limitations d'exécution (pour le bien de l'utilisateur) introduites avec Android 8.0 et plus.
Bien sûr, il est capable de télécharger un ou plusieurs fichiers à la fois :
Comment ça marche ?
Plusieurs composants sont impliqués :
- DownloadManager : service applicatif Android permettant de déclencher des téléchargements et en connaître le status.
- DownloadManager.Request : une demande de téléchargement que l'on peut paramétrer.
- Un BroadcastReceiver : permet de recevoir des notifications lorsque les téléchargements changent de statuts.
Pour créer un téléchargement il va donc falloir paramétrer le téléchargement au travers d'un DownloadManager.Request que l'on va fournir au DownloadManager et enregistrer un BroadcastReceiver pour savoir quand le téléchargement est terminé.
Paramétrer et lancer le téléchargement
Le minimum à avoir est bien sûr l'URL du fichier à télécharger. Il est ensuite possible de configurer plusieurs éléments :
- SetAllowedOverMetered permet d'indiquer si le téléchargement est autorisé sur une connexion cellulaire.
- SetTitle permet de spécifier le titre affiché.
- SetNotificationVisibility permet d'indiquer si une notification avec affichage de la progression est créée pour ce téléchargement.
- SetVisibleInDownloadsUi permet d'indiquer si le résultat sera visible dans le gestionnaire de téléchargements système.
- AddRequestHeader permet d'ajouter un header à votre demande de téléchargement.
La configuration la plus importante est peut-être l'endroit où sera téléchargé votre fichier ! Pour cela je vous recommande d'utiliser la méthode SetDestinationInExternalFilesDir qui permet de choisir un téléchargement dans le dossier des fichiers de l'application en dehors de son stockage privé : ce dossier est supprimé si votre application l'est, les fichiers sont accessibles à l'utilisateur (depuis son explorateur de fichier) et aux autres applications (si elles en ont demandé la permission). Pour votre application et ce choix, il n'y a pas de permission Android spécifique à demander. Dans tous les cas, je n'ai pas trouvé de moyen de demander un téléchargement directement dans le dossier privé de l'application.
Le code est donc assez simple à produire :
// création de la demande de téléchargement var request = new DownloadManager.Request(Android.Net.Uri.Parse(from)); request.SetAllowedOverMetered(true); request.SetTitle(title); request.SetNotificationVisibility(DownloadVisibility.Visible); request.SetVisibleInDownloadsUi(false); request.SetDestinationInExternalFilesDir(Application.Context, null, AppFolderInExternalStoragDirectory + to); // ajout de la demande var downloadManager = (DownloadManager)Application.Context. GetSystemService(Context.DownloadService); var androidDldId = downloadManager.Enqueue(request); // stockage du mapping pour plus tard CrossSettings.Current.AddOrUpdateValue (androidDldId.ToString(), downloadIdentifier, DroidDownloadServiceMappingsSettingsName);
Le résultat de l'ajout du téléchargement est un identifiant de type long correspondant au téléchargement. Il est nécessaire de le conserver afin de pouvoir identifier plus tard l'objet "métier cible" correspondant au téléchargement. Dans mon cas par exemple, je l'associe à l'identifiant d'un épisode de Podcast (downloadIdentifier) que je stocke via le package Nuget "Plugin.Settings" dans un fichier de paramètre dont le nom (DroidDownloadServiceMappingsSettingsName) est stocké dans une constante. Nous utiliserons cette information plus tard.
Connaître le statut d'avancement
Une fois le téléchargement déclenché, c'est le système qui prend le relai et votre application pourra être tuée sans que cela ne pose de problème. Lorsque le téléchargement évolue, l'OS vous notifie au moyen d'un Broadcast que vous pouvez intercepter facilement avec le bon filtre d'Intent. En voici la définition Xamarin basique :
[BroadcastReceiver] [IntentFilter(new[] { DownloadManager.ActionDownloadComplete })] public class DownloadServiceBroadcastReceiver : BroadcastReceiver { ... }
Qui se traduit au niveau du manifest par ce joli XML :
<receiver android:name="md5b43a508b1.DownloadServiceBroadcastReceiver"> <intent-filter> <action android:name="android.intent.action.DOWNLOAD_COMPLETE" /> </intent-filter> </receiver>
Sous le capot, le DownloadManager spécifie cibler le package ayant demandé le téléchargement et vous ne serez donc pas perturbé par les limitations introduites par Oreo.
Lors de la réception d'un Intent correspondant à ce BroadCast vous pouvez alors lire l'identifiant du téléchargement correspondant. Cet identifiant est alors à utiliser pour exécuter une Query sur le DownloadManager pour en connaitre les informations relatives. Le résultat d'une Query étant un ICursor, il y a un peu de code glue à écrire pour bien gérer tous les cas.
public override async void OnReceive(Context context, Intent intent) { // Si Xamarin Forms n'est pas initialisé, je ne fais rien if (false == Forms.IsInitialized) { return; } // je lis l'identifiant du téléchargement var androidDownloadId = intent .GetLongExtra(DownloadManager.ExtraDownloadId, -1L); if (androidDownloadId < 0) { return; } // est-ce que j'ai un mapping pour ce téléchargement ? var targetEpisodeId = CrossSettings.Current .GetValueOrDefault(androidDownloadId.ToString(), string.Empty, DroidDownloadService.DroidDownloadServiceMappingsSettingsName); if (string.IsNullOrEmpty(targetEpisodeId)) {return; } // je récupérer un service propre à mon application var episodeDldService = DependencyService .Resolve<IEpisodeDownloadService>(); var targetDld = episodeDldService.Downloads .LastOrDefault(dld => dld.TargetEpisode.Guid == targetEpisodeId); if (targetDld == null) { return; } // récupération du service var downloadManager = (DownloadManager)context .GetSystemService(Context.DownloadService); // création de la query pour ce téléchargement var query = new DownloadManager.Query() .SetFilterById(androidDownloadId); var queryResult = downloadManager.InvokeQuery(query); // est-ce qu'il y a un résultat ? if (queryResult.Count <= 0 || !queryResult.MoveToFirst()) { // Correspond à un échec. targetDld.SetAsFailed(); return; } // dans quelle colonne est le status ? int statusColumnIndex = queryResult .GetColumnIndex(DownloadManager.ColumnStatus); // lecture du status var status = (DownloadStatus)queryResult.GetInt(statusColumnIndex); // mise à jour du téléchargement "métier" en fonction // du status ! }
Ici, on se contente de lire le statu du téléchargement mais il est aussi possible d'avoir l'intégralité des informations renseignées lors du déclenchement du téléchargement. Plus important, il est aussi possible de lire :
- le nombre total de bytes à télécharger avec la colonne TOTAL_SIZE_BYTES.
- Le nombre total de bytes téléchargé avec la colonne BYTES_DOWNLOADED_SO_FAR.
Dans mon cas j'utilise XamarinForms et je décide de ne rien faire lorsqu'il n'est pas initialisé car cela correspond au cas où mon application n'est pas lancée. Je préfère gérer la mise à jour au prochain lancement de l'application. Pour cela il suffira, au lancement de l'application, de créer une Query retournant l'intégralité des téléchargements en cours et d'effectuer le traitement que je juge opportun pour mon application.
Les contraintes
Il existe quelques contraintes à connaître avant de se lancer tête baissée sur ce mécanisme. Voici les plus critiques selon moi :
- Impossible de spécifier un emplacement de téléchargement "privé".
- Impossible de spécifier des priorités aux téléchargements.
- Aucun contrôle sur l'ordre de téléchargement ni la façon dont cela est fait : l'OS gère tout.
- À priori pas de support d'HTTPS ! Bon en fait, après vérification, c'est supporté :)
- Pour afficher la progression dans votre application, il faut faire le mécanisme de de lecture de la progression vous-même à la main.
Happy coding !
Commentaires