Tutoriel de stockage scoped pour Android 11: plongée en profondeur

Online Coding Courses for Kids

Le stockage limité était l’une des fonctionnalités les plus importantes introduites dans Android 10. Il comprenait un mécanisme de désactivation, mais avec Android 11, il est désormais obligatoire que toutes les applications ciblant cette version aient un stockage limité mis en œuvre.

Ces nouvelles améliorations ont à l’esprit la confidentialité. Alors que l’octroi d’une autorisation de stockage à l’exécution signifie qu’une application peut accéder à presque tous les fichiers sur le disque, le stockage limité vise à éviter cela. Désormais, si une application souhaite accéder à un fichier qu’elle n’a pas créé, un utilisateur doit lui donner un accès explicite. De plus, pour réduire le nombre de fichiers dispersés sur le disque, il existe des dossiers spécifiques pour les stocker. Lors de la désinstallation d’une application, toutes les données en dehors de ces répertoires seront supprimées. C’est quelque chose que votre espace disque libre apprécie. :]

L’objectif de ce didacticiel est de plonger dans le stockage étendu et de vous montrer plus de ses fonctionnalités avancées. Plus précisément, vous apprendrez:

  • Comment migrer les données de votre application.
  • Restreindre et élargir l’accès.
  • Comment les gestionnaires de fichiers et les applications de sauvegarde peuvent accéder au système de fichiers.
  • Limitations des nouvelles exigences.

Vous ferez cela avec Le Memeify, une application qui affiche toutes les images de votre appareil et vous permet de créer des mèmes incroyables. Il combine tous les concepts de stockage étendu et offre une excellente expérience lors des tests. :]N’hésitez pas à partager les meilleurs mèmes avec nous sur Twitter @rwenderlich.

Commencer

Accédez aux fichiers du tutoriel en cliquant sur Télécharger les matériaux en haut ou en bas de cette page. Vous trouverez deux projets dans le fichier ZIP. Entrée a le squelette de l’application que vous allez créer, et Final vous donne quelque chose pour comparer votre code lorsque vous avez terminé.

Galerie d'images de l'application Scoped Storage

L’image ci-dessus montre ce que vous allez créer!

Comprendre la structure du projet

Pour comprendre la structure du projet, ouvrez d’abord le Entrée projet dans Android Studio et attendez qu’il se synchronise.

La structure de répertoires du projet.

Vous verrez un ensemble de sous-dossiers et de fichiers importants:

  • modèle: Il s’agit de l’objet de données utilisé pour représenter une image. Il contient l’URI, la date modifiée, la taille, etc.
  • ui: Dans ce dossier, vous trouverez l’activité, les fragments, les modèles de vue et les adaptateurs que vous utiliserez pour permettre à vos utilisateurs d’afficher et d’interagir avec les données de l’application. Ce dossier comporte également deux sous-dossiers pour les deux écrans principaux de l’application: les détails et les images.
  • ui / actions.kt: Vous trouverez ici les classes scellées Kotlin pour toute action qu’une vue pourrait effectuer. Cela rend les choses belles et ordonnées en énumérant explicitement ces actions.
  • FileOperations.kt: Ce fichier contient la classe qui définit toutes les opérations sur les fichiers.
  • Utils.kt: Ce fichier contient l’ensemble des méthodes utilitaires que vous utiliserez tout au long du projet.

Lancer Le Memeify

Pour accéder aux images sur votre appareil, vous devez accorder l’autorisation de stockage lorsque vous y êtes invité.

Construisez et exécutez. Après avoir accepté les autorisations, vous verrez un écran comme celui-ci:

Galerie d'images et sélection

Votre écran affichera différentes images, bien sûr. :]

Présentation du stockage étendu

Le stockage étendu apporte deux changements majeurs. Tout d’abord, vous n’avez plus accès à un fichier via son chemin. Au lieu de cela, vous devez utiliser son Uri. Deuxièmement, si vous souhaitez modifier un fichier non créé par votre application, vous devez demander l’autorisation à l’utilisateur.

Il y a deux possibilités pour cela. L’un est via le MediaStore API qui vous permet d’interroger l’appareil pour des images, des vidéos et de l’audio. L’autre utilise le Infrastructure d’accès au stockage (SAF), qui ouvre l’explorateur de fichiers natif et vous permet de demander l’accès à un fichier spécifique ou à son dossier racine, selon que vous utilisez ou non l’action ACTION_OPEN_DOCUMENT ou ACTION_OPEN_DOCUMENT_TREE, respectivement.

Nouveautés d’Android 11

Android 11 introduit un nouvel ensemble de fonctionnalités axées sur les fichiers:

  • Opérations en masse
  • Mettre un fichier en vedette
  • Mettre un fichier à la corbeille

Dans les prochaines sections, vous en apprendrez plus sur chacun de ces éléments.

Opérations en masse

L’une des limitations d’Android 10 est le manque de prise en charge des opérations en masse effectuées sur des fichiers. Chaque fois que vous voulez faire quelque chose sur plusieurs fichiers, vous devez parcourir une liste et demander à l’utilisateur son consentement pour chaque fichier individuel.

Android 11 a ajouté deux nouvelles méthodes pour résoudre ce problème:

  • createWriteRequest
  • createDeleteRequest

Les deux méthodes prennent en charge la modification de plusieurs fichiers en même temps avec une seule demande. Actuellement, vous pouvez sélectionner plusieurs fichiers dans l’application et les supprimer, mais elle demande l’autorisation un fichier à la fois. Pour ajouter la possibilité de supprimer plusieurs fichiers et d’accorder une autorisation à la fois, vous allez mettre à jour Le Memeify pour utiliser createDeleteRequest.

Commencez par ajouter la méthode suivante à FileOperations.kt:

@SuppressLint("NewApi") //method only call from API 30 onwards
fun deleteMediaBulk(context: Context, media: List): IntentSender {
  val uris = media.map { it.uri }
  return MediaStore.createDeleteRequest(context.contentResolver, 
                                        uris).intentSender
}

Imaginez qu’un utilisateur souhaite supprimer cinq fichiers qui n’ont pas été créés avec l’application. Pour surmonter la limitation de l’affichage d’une boîte de dialogue pour chaque fichier, au lieu d’appeler contentResolver.delete, vous pouvez utiliser MediaStore.createDeleteRequest, qui permet à l’utilisateur d’accorder l’accès à tous les fichiers avec une seule demande.

Maintenant, mettez à jour deleteMedia à l’intérieur MainViewModel.kt:

fun deleteMedia(media: List) {
  if (hasSdkHigherThan(Build.VERSION_CODES.Q) && media.size > 1) {
    val intentSender = 
      FileOperations.deleteMediaBulk(getApplication(), media)
    _actions.postValue(
        MainAction.ScopedPermissionRequired(
            intentSender, 
            ModificationType.DELETE))
  } else {
    viewModelScope.launch {
      for (item in media) {
        val intentSender = FileOperations.deleteMedia(
                getApplication(), 
                item)
        if (intentSender != null) {
          _actions.postValue(
              MainAction.ScopedPermissionRequired(
                intentSender, 
                ModificationType.DELETE))
        }
      }
    }
  }
}

Le code ci-dessus appellera MediaStore.createDeleteRequest si l’appareil exécute Android 11 ou version ultérieure et que plusieurs fichiers sont sélectionnés pour la suppression.

Enfin, mettez à jour onActivityResult à l’intérieur MainFragment.kt:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  super.onActivityResult(requestCode, resultCode, data)
  when (requestCode) {
    REQUEST_PERMISSION_DELETE -> {
      if (resultCode == Activity.RESULT_OK) { 
        val multiSelection = tracker.selection.size() > 1
        if (!multiSelection || !hasSdkHigherThan(Build.VERSION_CODES.Q)) {
          delete()
        }

Appelez seulement delete lorsque les conditions ci-dessus ne sont pas remplies. En d’autres termes, puisque vous êtes déjà autorisé à supprimer tous les fichiers sélectionnés, il n’est pas nécessaire de demander chaque fichier individuellement.

Créez et exécutez l’application et supprimez plusieurs images. Soyez prudent et sélectionnez les pires! :]

Supprimer plusieurs images

Maintenant que vous savez comment effectuer des opérations groupées sur des fichiers, il est temps d’apprendre à mettre un fichier en étoile.

Mettre un fichier en vedette

Ceci est particulièrement pratique pour définir les priorités sur une liste. Vous allez d’abord ajouter la possibilité de définir un élément comme favori, puis créer un filtre qui n’affiche que les médias favoris.

Définition d’un élément comme favori

Pour ajouter la possibilité de marquer un élément favori, ajoutez ce qui suit dans action_main.xml:


Ajoutez maintenant la logique suivante à onActionItemClicked, lequel est dedans MainFragment.kt:

R.id.action_favorite -> {
  addToFavorites()
  true
}

addToFavorites est appelé lorsqu’il y a des fichiers multimédias sélectionnés via une pression longue et que l’utilisateur sélectionne cette option dans le menu contextuel.

Ajoutez la méthode pour ajouter / supprimer des éléments des favoris dans MainFragment.kt:

private fun addToFavorites() {
  //1
  if (!hasSdkHigherThan(Build.VERSION_CODES.Q)) {
    Snackbar.make(
               binding.root, 
               R.string.not_available_feature, 
               Snackbar.LENGTH_SHORT).show()
    return
  }

  //2
  val media = imageAdapter.currentList.filter {
    tracker.selection.contains("${it.id}")
  }

  //3
  val state = !(media.isNotEmpty() && media[0].favorite)
  //4
  viewModel.requestFavoriteMedia(media, state)
  //5
  actionMode?.finish()
}

Voici une description étape par étape de cette logique:

  1. Cette fonctionnalité n’est disponible que sur Android 11. Si l’application est exécutée sur une version inférieure, elle affichera un message et utilisera return pour quitter la méthode.
  2. Pour ajouter des fichiers multimédias aux favoris, vous devez d’abord savoir quels fichiers mettre à jour. Récupérez la liste en filtrant tous les médias avec le IDs des fichiers sélectionnés.
  3. Vous pouvez sélectionner à la fois les images qui sont déjà suivies et celles qui ne le sont pas. La valeur de la première image sélectionnée est prioritaire. En d’autres termes, si la première image sélectionnée est déjà un favori, elle sera supprimée de cette liste. Sinon, il sera ajouté.
  4. Appel requestFavoriteMedia pour ajouter / supprimer ces fichiers des favoris.
  5. Fermez le mode action.

Affichage des favoris

Maintenant que les images peuvent être ajoutées aux favoris, l’application a besoin d’un moyen de les afficher. Pour récupérer une liste filtrée de favoris, accédez à MainViewModel.kt et ajouter requestFavoriteMedia:

fun requestFavoriteMedia(media: List, state: Boolean) {
  val intentSender = FileOperations.addToFavorites(
          getApplication(), 
          media, 
          state)
  _actions.postValue(
          MainAction.ScopedPermissionRequired(
                  intentSender, 
                  ModificationType.FAVORITE))
}

Avec un stockage limité, pour apporter une modification à un fichier non créé par l’application elle-même, il est nécessaire de demander l’autorisation à l’utilisateur. C’est pourquoi il y a un intentSender objet retourné le addToFavorites.

Ajouter addToFavorites à FileOperations.kt:

@SuppressLint("NewApi") //method only call from API 30 onwards
fun addToFavorites(context: Context, media: List, state: Boolean): IntentSender {
  val uris = media.map { it.uri }
  return MediaStore.createFavoriteRequest(
             context.contentResolver, 
             uris, 
             state).intentSender
}

Le code ci-dessus appelle MediaStore.createFavoriteRequest afin que les fichiers puissent être ajoutés ou supprimés des favoris en fonction de la valeur de state. Ajoutez la valeur pour FAVORITE à ModificationType dans actions.kt:

FAVORITE,

Ajoutez ensuite la vérification suivante à requestScopedPermission à l’intérieur MainFragment.kt:

ModificationType.FAVORITE -> REQUEST_PERMISSION_FAVORITE

Le code ci-dessus demande la permission à l’utilisateur.

Ajoutez maintenant un nouveau filtre pour récupérer uniquement les images ajoutées aux favoris. Commencez par ouvrir menu_main.xml et en ajoutant ce qui suit:


Ce sera un nouveau point d’entrée affiché dans le menu contextuel. Ouvert MainFragment.kt, et en onOptionsItemSelected, ajouter:

R.id.filter_favorite -> {
  loadFavorites()
  true
}

Lorsque l’utilisateur sélectionne cette option, seuls les médias favoris apparaissent. Maintenant, définissez la méthode correspondante:

private fun loadFavorites() {
  if (!hasSdkHigherThan(Build.VERSION_CODES.Q)) {
    Snackbar.make(
               binding.root, 
               R.string.not_available_feature, 
               Snackbar.LENGTH_SHORT).show()
    return
  }
  viewModel.loadFavorites()
}

Si l’application s’exécute sur un appareil dont la version ne prend pas en charge les favoris, un message avec le texte “Fonctionnalité disponible uniquement sur Android 11” s’affiche. Vous pouvez également masquer cette option.

loadFavoritesest défini dans MainViewModel:

@RequiresApi(Build.VERSION_CODES.R)
fun loadFavorites() {
  viewModelScope.launch {
    val mediaList = FileOperations.queryFavoriteMedia(
                                     getApplication())
    _actions.postValue(MainAction.FavoriteChanged(mediaList))
  }
}

Le code ci-dessus appelle FileOperations.queryFavoriteMedia pour charger uniquement les fichiers favoris. Utilisation RequiresApi pour avertir le développeur que cette méthode ne doit être appelée que sur Android 11 et supérieur.

Ouvert FileOperations.kt et ajoutez cette fonction:

@RequiresApi(Build.VERSION_CODES.R)
suspend fun queryFavoriteMedia(context: Context): List {
  val favorite = mutableListOf()
  withContext(Dispatchers.IO) {
    val selection = "${MediaStore.MediaColumns.IS_FAVORITE} = 1"
    favorite.addAll(queryImagesOnDevice(context, selection))
    favorite.addAll(queryVideosOnDevice(context, selection))
  }
  return favorite
}

Au lieu de récupérer toutes les images et vidéos, ajoutez une condition pour ne récupérer que celles avec l’attribut IS_FAVORITE définir comme 1 sur MediaStore. Cela garantit que la requête est optimisée pour ne renvoyer que les données souhaitées. Aucune vérification supplémentaire n’est nécessaire.

Vous avez défini la requête. Maintenant, ajoutez une nouvelle classe de données, FavoriteChanged, à MainAction à l’intérieur de actions.kt:

data class FavoriteChanged(val favorites: List) : MainAction()

Lorsque la liste des favoris est disponible, informez l’interface utilisateur de recharger la galerie avec ce nouveau favorites liste. Dans MainFragment.kt, mettre à jour handleAction:

private fun handleAction(action: MainAction) {
  when (action) {
    is MainAction.FavoriteChanged -> {
      imageAdapter.submitList(action.favorites)
      if (action.favorites.isEmpty()) {
        Snackbar.make(binding.root, R.string.no_favorite_media, 
                      Snackbar.LENGTH_SHORT).show()
      }
    }
  }
}

Il est temps de tester cette nouvelle fonctionnalité! Appuyez sur compiler et exécutez et ajoutez vos meilleurs mèmes à vos favoris.

Ajouter des images aux favoris

Maintenant que vous savez comment ajouter un fichier à une étoile, il est temps d’apprendre à en mettre un dans la corbeille.

Mettre un fichier à la corbeille

La corbeille d’un fichier n’est pas la même chose qu’une opération de suppression. La suppression d’un fichier le supprime complètement du système, tandis que la suppression d’un fichier l’ajoute à une corbeille temporaire, comme ce qui se passe sur un ordinateur. Le fichier y restera pendant 30 jours, et si aucune autre action n’est entreprise, le système le supprimera automatiquement après cette période.

La logique derrière la corbeille d’un fichier est comme celle de mettant en vedette un fichier, comme vous le verrez.

Ajout d’un fichier à la corbeille
Donnez à l’application la possibilité de placer d’abord un fichier dans la corbeille. Ouvert action_main.xml et ajouter:


Le code ci-dessus ajoute le point d’entrée pour la corbeille d’un fichier. Ouvert MainFragment.kt, et sur onActionItemClicked, définissez son action:

R.id.action_trash -> {
  addToTrash()
  true
}

Cela appellera addToTrash pour supprimer le fichier. Après cette méthode, ajoutez addToTrash:

private fun addToTrash() {
  //1
  if (!hasSdkHigherThan(Build.VERSION_CODES.Q)) {
    Snackbar.make(
               binding.root, 
               R.string.not_available_feature, 
               Snackbar.LENGTH_SHORT).show()
    return
  }

  //2
  val media = imageAdapter.currentList.filter {
    tracker.selection.contains("${it.id}")
  }

  //3
  val state = !(media.isNotEmpty() && media[0].trashed)
  //4
  viewModel.requestTrashMedia(media, state)
  //5
  actionMode?.finish()
}

Analysons ce code étape par étape:

  1. Cette fonctionnalité n’est disponible que sur Android 11. Si l’application exécute une version inférieure, elle affiche un message indiquant que le système d’exploitation actuel ne prend pas en charge la corbeille d’un fichier.
  2. Sélectionnez la liste des fichiers multimédias à envoyer dans la corbeille.
  3. Après avoir récupéré la liste, identifiez les fichiers qui seront restaurés ou supprimés. Regardez l’état du premier fichier de la liste. S’il se trouve déjà dans la corbeille, tous les fichiers seront restaurés. Sinon, ils seront tous supprimés.
  4. Appel requestTrashMedia pour restaurer / supprimer ces fichiers.
  5. Fermez le mode action.

Maintenant, définissez requestTrashMedia dans MainViewModel.kt:

fun requestTrashMedia(media: List, state: Boolean) {
  val intentSender = FileOperations.addToTrash(
                                      getApplication(), 
                                      media, 
                                      state)
  _actions.postValue(MainAction.ScopedPermissionRequired(
                                  intentSender, 
                                  ModificationType.TRASH))
}

N’oubliez pas que si vous essayez de modifier un fichier que votre application n’a pas créé, vous devez demander l’autorisation. Pour obtenir la permission, addToTrash Retour intentSender pour inviter l’utilisateur.

Ajouter addToTrash à FileOperations.kt:

@SuppressLint("NewApi") //method only call from API 30 onwards
fun addToTrash(context: Context, media: List, state: Boolean): 
  IntentSender {
  val uris = media.map { it.uri }
  return MediaStore.createTrashRequest(
                      context.contentResolver, 
                      uris, 
                      state).intentSender
}

Pour faire l’appel à MediaStore.createTrashRequest, récupérez les fichiers » Uris de la liste des médias, ainsi que l’État, qui est true si les fichiers seront mis dans la corbeille, et false autrement.

Ouvert actions.kt et mettre à jour ModificationType pour contenir ce nouveau type de mise à jour, TRASH:

TRASH

Sur MainFragment.kt, ajoutez ce qui suit à requestScopedPermission:

ModificationType.TRASH -> REQUEST_PERMISSION_TRASH

Le code ci-dessus invitera l’utilisateur à accorder l’autorisation à ces fichiers.

Maintenant que vous avez ajouté la logique pour ajouter / supprimer un fichier dans / de la corbeille, l’étape suivante consiste à ajouter un nouveau filtre pour voir tous les fichiers marqués pour suppression.

Affichage des fichiers dans la corbeille

Pour filtrer les fichiers et afficher uniquement les fichiers mis dans la corbeille, ajoutez d’abord l’option au menu. Dans menu_main.xml ajouter un nouvel élément:


Cela crée un point d’entrée. Ouvert MainFragment.kt, et sur onOptionsItemSelected, définissez son action:

R.id.filter_trash -> {
  loadTrashed()
  true
}

Le code ci-dessus appellera loadTrashed. Après onOptionsItemSelected, ajouter:

private fun loadTrashed() {
  if (!hasSdkHigherThan(Build.VERSION_CODES.Q)) {
    Snackbar.make(
               binding.root, 
               R.string.not_available_feature, 
               Snackbar.LENGTH_SHORT).show()
    return
  }

  viewModel.loadTrashed()
}

Si l’appareil est équipé d’Android 11, l’application chargera tous les éléments de la corbeille via l’appel à loadTrashed.

Dans MainViewModel.kt, ajoutez la fonction suivante:

@RequiresApi(Build.VERSION_CODES.R)
fun loadTrashed() {
  viewModelScope.launch {
    val mediaList = FileOperations.queryTrashedMedia(
                                     getApplication())
    _actions.postValue(MainAction.TrashedChanged(mediaList))
  }
}

Cela interroge le système pour tous les médias mis dans la corbeille et lorsque vous recevez cette liste, l’interface utilisateur recharge la galerie pour afficher la vue filtrée.

La logique pour implémenter cette requête est très différente de celle de l’interrogation d’images. Aller vers FileOperations.kt et ajouter queryTrashedMedia:

@RequiresApi(Build.VERSION_CODES.R)
suspend fun queryTrashedMedia(context: Context): List {
  val trashed = mutableListOf()

  withContext(Dispatchers.IO) {
    trashed.addAll(queryTrashedMediaOnDevice(
                     context, 
                     MediaStore.Images.Media.EXTERNAL_CONTENT_URI))
    trashed.addAll(queryTrashedMediaOnDevice(
                     context, 
                     MediaStore.Video.Media.EXTERNAL_CONTENT_URI))
  }
  return trashed
}

Dans le code ci-dessus, au lieu d’avoir deux méthodes distinctes pour interroger les médias mis dans la corbeille, vous utiliserez la même méthode – queryTrashedMediaOnDevice – et envoyer différents EXTERNAL_CONTENT_URIs en fonction du type de requête.

Maintenant, ajoutez queryTrashedMediaOnDevice à Opérations de fichiers.

@RequiresApi(Build.VERSION_CODES.R)
suspend fun queryTrashedMediaOnDevice(context: Context, contentUri: Uri): List {
  val media = mutableListOf()
  withContext(Dispatchers.IO) {
    //1
    val projection = arrayOf(MediaStore.MediaColumns._ID,
        MediaStore.MediaColumns.RELATIVE_PATH,
        MediaStore.MediaColumns.DISPLAY_NAME,
        MediaStore.MediaColumns.SIZE,
        MediaStore.MediaColumns.MIME_TYPE,
        MediaStore.MediaColumns.WIDTH,
        MediaStore.MediaColumns.HEIGHT,
        MediaStore.MediaColumns.DATE_MODIFIED,
        MediaStore.MediaColumns.IS_FAVORITE,
        MediaStore.MediaColumns.IS_TRASHED)

    //2
    val bundle = Bundle()
    bundle.putInt("android:query-arg-match-trashed", 1)
    bundle.putString("android:query-arg-sql-selection", 
                       "${MediaStore.MediaColumns.IS_TRASHED} = 1")
    bundle.putString("android:query-arg-sql-sort-order", 
                       "${MediaStore.MediaColumns.DATE_MODIFIED} DESC")

    //3
    context.contentResolver.query(
        contentUri,
        projection,
        bundle,
        null
    )?.use { cursor ->

      //4
      while (cursor.moveToNext()) {
        val id = cursor.getLong(cursor.getColumnIndex(
                                  MediaStore.MediaColumns._ID))
        val path = cursor.getString(cursor.getColumnIndex(
                                  MediaStore.MediaColumns.RELATIVE_PATH))
        val name = cursor.getString(cursor.getColumnIndex(
                                  MediaStore.MediaColumns.DISPLAY_NAME))
        val size = cursor.getString(cursor.getColumnIndex(
                                  MediaStore.MediaColumns.SIZE))
        val mimeType = cursor.getString(cursor.getColumnIndex(
                                  MediaStore.MediaColumns.MIME_TYPE))
        val width = cursor.getString(cursor.getColumnIndex(
                                  MediaStore.MediaColumns.WIDTH))
        val height = cursor.getString(cursor.getColumnIndex(
                                  MediaStore.MediaColumns.HEIGHT))
        val date = cursor.getString(cursor.getColumnIndex(
                                  MediaStore.MediaColumns.DATE_MODIFIED))
        val favorite = cursor.getString(cursor.getColumnIndex(
                                  MediaStore.MediaColumns.IS_FAVORITE))
        val uri = ContentUris.withAppendedId(contentUri, id)
        // Discard invalid images that might exist on the device
        if (size == null) {
          continue
        }
        media += Media(id, 
                   uri, 
                   path, 
                   name, 
                   size, 
                   mimeType, 
                   width, 
                   height, 
                   date, 
                   favorite == "1", 
                   true)
      }
      cursor.close()
    }
  }
  return media
}

Décomposons la logique du code ci-dessus en petites étapes:

  1. le projection définit les attributs extraits du MediaStore les tables. Il y a un autre IS_TRASHED utilisé en interne pour sélectionner uniquement les éléments de la corbeille.
  2. Par rapport à la requête d’images et de vidéos, celle-ci est un peu différente. Les images et les vidéos ne tiennent pas compte des éléments de la corbeille, et comme vous les voulez, vous devrez suivre une approche différente. C’est la raison pour laquelle vous créez cette fonction. Utilisation bundle avec ces arguments définis pour obtenir tous les médias mis à la corbeille sur le disque.
  3. Exécutez la requête avec tous les paramètres ci-dessus définis.
  4. Récupérez tous les médias, parcourez le fichier renvoyé cursor et enregistrez ces données pour mettre à jour l’interface utilisateur.

Enfin, ajoutez TrashedChanged à MainAction à l’intérieur actions.kt:

sealed class MainAction {
  data class TrashedChanged(val trashed: List) : MainAction()
}

Cela informera l’interface utilisateur de la nouvelle trashed liste à afficher. Dans MainFragment.kt, mettre à jour handleAction:

private fun handleAction(action: MainAction) {
  when (action) {
    is MainAction.TrashedChanged -> {
      imageAdapter.submitList(action.trashed)
      if (action.trashed.isEmpty()) {
        Snackbar.make(binding.root, R.string.no_trashed_media, 
                      Snackbar.LENGTH_SHORT).show()
      }
    }
  }
}

Terminé! Construisez et exécutez. :]

Ajouter des images à la corbeille

Migrer les données de votre application

Se familiariser avec l’API File Paths

La refactorisation d’applications et de bibliothèques entières qui utilisaient des chemins de fichiers pour diverses opérations peut prendre plusieurs mois, voire plusieurs années. Pour aggraver les choses, certaines bibliothèques natives n’ont probablement plus de support. Pour surmonter cela, Android a mis à jour le API de fichier qui vous permet de continuer à utiliser les API Java Files ou les bibliothèques C / C ++ natives avec un stockage limité sans avoir besoin d’apporter d’autres modifications. L’accès au chemin du fichier est délégué au MediaStore API, qui gérera toutes les opérations.

Comprendre les limites
Android 11 a mis en œuvre quelques limitations supplémentaires pour respecter les fichiers privés d’un utilisateur. Avec ACTION_OPEN_DOCUMENT et ACTION_OPTION_DOCUMENT_TREE, les applications n’ont plus accès à:

  • Dossiers racine du stockage interne, cartes SD et Téléchargements /
  • Android / données et Android / obb

En fonction de l’ensemble des fonctionnalités de votre application, vous devrez peut-être migrer ses fichiers / répertoires. Vous avez deux options qui couvrent les scénarios les plus courants.

Le premier est preserveLegacyExternalStorage. Android 11 introduit ce nouvel attribut dans le AndroidManifest.xml. Il permet à votre application d’accéder à votre ancien répertoire de fichiers lorsque l’application est mise à jour et jusqu’à ce qu’elle soit désinstallée. Lors d’une nouvelle installation, cet indicateur n’a aucun impact sur l’application:


Le second est le MediaStore API. Vous pouvez utiliser les arguments de sélection de contentResolver.query pour récupérer tous les fichiers multimédias de vos répertoires précédents, utilisez MediaStore.createWriteRequest pour les déplacer vers un nouveau dossier, puis utilisez contentResolver.update mettre à jour MediaStore. Un exemple de méthode de migration est présenté ci-dessous:

/**
 * We're using [Environment.getExternalStorageState] dir that has been 	 	 
 * deprecated to migrate files from the old location to the new one.
 */	 	 
@Suppress("deprecation")
suspend fun migrateFiles(context: Context): IntentSender? {
 val images = mutableListOf()
 var result: IntentSender? = null
 withContext(Dispatchers.IO) {
   //1
   val externalDir = Environment.getExternalStorageDirectory().path
   val dirSrc = File(externalDir, context.getString(R.string.app_name))
   if (!dirSrc.exists() || dirSrc.listFiles() == null) {
     return@withContext
   }
   //2
   val projection = arrayOf(MediaStore.Images.Media._ID)
   val selection = MediaStore.Images.Media.DATA + " LIKE ? AND " +
         MediaStore.Images.Media.DATA + " NOT LIKE ? "
   val selectionArgs = arrayOf("%" + dirSrc.path + "%", 
                               "%" + dirSrc.path + "/%/%")
   val sortOrder = "${MediaStore.Images.Media.DATE_MODIFIED} DESC"
        context.contentResolver.query(
                  MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                  projection,
                  selection,
                  selectionArgs,
                  sortOrder)?.use { cursor ->
     //3
     while (cursor.moveToNext()) {
        val id = 
        cursor.getLong(cursor.getColumnIndex(
                       MediaStore.Images.Media._ID))
        val uri = 
          ContentUris.withAppendedId(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 
            id)
        images.add(uri)
      }
      cursor.close()
    }
    //4
    val uris = images.filter {
          context.checkUriPermission(it, Binder.getCallingPid(), Binder
            .getCallingUid(), 
             Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != PackageManager
             .PERMISSION_GRANTED
    }
    //5
    if (uris.isNotEmpty()) {
      result = MediaStore.createWriteRequest(context.contentResolver, 
                                             uris).intentSender
      return@withContext
    }
    //6
    val dirDest = File(Environment.DIRECTORY_PICTURES, 
                       context.getString(R.string.app_name))
    //7
    val values = ContentValues().apply {
      put(MediaStore.MediaColumns.RELATIVE_PATH, 
          "$dirDest${File.separator}")
    }
    for (image in images) {
      context.contentResolver.update(image, values, null) 	 
    }
  }
  return result
}

Voici une description étape par étape de la logique ci-dessus:

  1. Les fichiers ont été enregistrés dans storage/emulated/0/Le Memeify. Si ce dossier est vide, il n’y a aucun fichier à migrer.
  2. Si le dossier contient des fichiers à migrer, il est nécessaire d’obtenir les fichiers » Uris. Pour filtrer uniquement les fichiers d’un dossier spécifique, utilisez selection et selectionArgs.
  3. Seulement le Uri est nécessaire pour trouver le fichier, qui est stocké dans une liste pour un accès ultérieur.
  4. Avant de démarrer la mise à jour, vérifiez si l’application a accès à ces fichiers.
  5. Si l’application ne dispose pas d’un accès en écriture, demandez à l’utilisateur une autorisation. Faites cela via createWriteRequest, qui renvoie un intentSender qui doit être invoqué.
  6. Créez un nouveau répertoire pour migrer les fichiers. Pour respecter les exigences de stockage étendues, placez-le à l’intérieur DCIM ou Des photos. Toutes les images seront déplacées vers Photos / Le Memeify.
  7. Mettez à jour le chemin précédent vers le nouveau et appelez contentResolver propager ce changement.

Cette opération pouvant prendre un certain temps, vous pouvez ajouter une boîte de dialogue pour informer l’utilisateur qu’une mise à jour se produit en arrière-plan. Par exemple:

Migrer des données

Pour tester une migration, ouvrez une image et sélectionnez les détails pour confirmer que tous les fichiers ont été déplacés avec succès. :]

Restreindre l’accès

Avec le stockage limité, plus de restrictions peuvent affecter d’autres applications nécessitant un accès plus élevé au stockage de l’appareil. Les explorateurs de fichiers et les applications de sauvegarde en sont un exemple. S’ils n’ont pas un accès complet au disque, ils ne fonctionneront pas correctement.

Limitation de l’accès à l’emplacement des médias

Lorsque vous prenez une photo, dans la plupart des cas, vous disposez également des coordonnées GPS de votre position. Jusqu’à présent, ces informations étaient facilement accessibles à n’importe quelle application en les demandant lors du chargement à partir du disque.

Il s’agit d’une vulnérabilité majeure, car ces informations peuvent révéler l’emplacement d’un utilisateur. Cela peut être intéressant si, par exemple, vous voulez voir tous les pays que vous avez visités sur une carte, mais cela peut être dangereux quand quelqu’un d’autre utilise ces informations pour identifier votre lieu de résidence ou de travail.

Pour résoudre ce problème, il existe une nouvelle autorisation sur l’API 29 que vous devrez déclarer dans AndroidManifest.xml pour accéder à ces informations:


Ajoutez cette autorisation, puis mettez à jour setImageLocation sur DétailsFragment.kt:

@SuppressLint("NewApi")
private fun setImageLocation() {
  val photoUri = MediaStore.setRequireOriginal(image.uri)
  activity?.contentResolver?.openInputStream(photoUri).use { stream ->
    ExifInterface(stream!!).run {
      if (latLong == null) {
        binding.tvLocation.visibility = View.GONE
      } else {
        binding.tvLocation.visibility = View.VISIBLE
        val coordinates = latLong!!.toList()
        binding.tvLocation.text = getString(
                                    R.string.image_location, 
                                    coordinates[0], 
                                    coordinates[1])
      }
    }
  }
}

Puisque vous accédez aux fichiers via Uris avec un espace de stockage limité, vous devrez appeler contentResolver?.openInputStream afin que vous puissiez utiliser ExifInterface pour récupérer les coordonnées du fichier.

Construisez et exécutez. Sélectionnez une image et cliquez sur l’icône d’information. Vous verrez différentes données d’image: date, stockage, coordonnées GPS, taille et résolution.

Emplacement de l'image

Ci-dessus, une photo prise à Coimbra, au Portugal. :]

Demander un accès plus large

Le stockage limité introduit plusieurs restrictions sur la façon dont les applications peuvent accéder aux fichiers. Dans cet exemple, une fois que l’utilisateur a accordé l’accès, vous pouvez créer, mettre à jour, lire et supprimer des fichiers. Mais il y a des scénarios où cela ne suffit pas.

Bien que la section suivante soit hors de portée pour Le Memeify, les concepts suivants sont importants à connaître.

Gestionnaires de fichiers et applications de sauvegarde

Les gestionnaires de fichiers et les applications de sauvegarde doivent accéder à l’ensemble du système de fichiers, vous devrez donc apporter d’autres modifications. Déclarez l’autorisation suivante dans AndroidManifest.xml:


Par mesure de sécurité, l’ajout de cette autorisation ne suffit pas; l’utilisateur doit toujours accorder manuellement l’accès à l’application. Appel:

fun openSettingsAllFilesAccess(activity: AppCompatActivity) {
  val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
  activity.startActivity(intent)
}

Cela ouvre l’écran des paramètres natifs dans la section «Accès à tous les fichiers», qui répertorie toutes les applications autorisées à accéder à l’ensemble du système de fichiers. Pour accorder cet accès, l’utilisateur doit sélectionner à la fois l’application et l’option «Autorisé».

Si votre application nécessite cet accès, vous devrez le déclarer le jeu de Google.

Limites

Bien que vous obteniez un accès plus large au système de fichiers avec ces solutions, il y aura toujours des limitations sur ce à quoi votre application peut accéder. Le stockage spécifique à l’application est sensible et, pour cette raison, l’accès aux fichiers internes et externes de l’application n’est pas disponible. Il n’est pas possible de leur accorder un accès en lecture / écriture.

Il existe souvent une fonctionnalité dans les explorateurs de fichiers qui vous permet de vérifier l’utilisation du disque et de libérer de l’espace en effaçant les caches d’autres applications. Le stockage limité ne permet pas d’effectuer cette opération dans l’application. Au lieu de cela, vous devez appeler:

fun openNativeFileExplorer(activity: AppCompatActivity) {
  val intent = Intent(StorageManager.ACTION_MANAGE_STORAGE)
  activity.startActivity(intent)
}

Cela lance l’application de fichiers natifs. Puis appelez:

fun clearAppsCacheFiles(activity: AppCompatActivity) {
  val intent = Intent(StorageManager.ACTION_CLEAR_APP_CACHE)
  activity.startActivity(intent)
}

Cela libère de l’espace disque.

Où aller en partant d’ici?

Téléchargez les fichiers de projet terminés en cliquant sur le Télécharger les matériaux bouton en haut ou en bas du didacticiel.

Toutes nos félicitations! Vous avez terminé ce didacticiel. Vous avez appris à tirer parti du stockage limité dans Android 11 et vous avez amélioré l’expérience de votre application!

Maintenant que vous maîtrisez le stockage étendu, pourquoi ne pas vous plonger plus profondément dans Android en explorant certaines de ses autres fonctionnalités? Par exemple, le didacticiel Bubbles peut être un défi amusant. Ou pensez à découvrir les applications de réalité augmentée dans ARCore avec Kotlin!

Nous espérons que vous avez apprécié ce tutoriel. Si vous avez des questions ou des commentaires, rejoignez la discussion du forum ci-dessous!


Close Menu