Sur les appareils équipés d'Android 4.4 (niveau d'API 19) ou version ultérieure, votre application peut interagir avec un fournisseur de documents, y compris avec des volumes de stockage externe et un espace de stockage cloud, à l'aide de Storage Access Framework. Ce framework permet aux utilisateurs d'interagir avec un sélecteur système pour choisir un fournisseur de documents, et sélectionner des documents spécifiques et d'autres fichiers que votre application doit créer, ouvrir et modifier.
Étant donné que l'utilisateur participe à la sélection des fichiers ou des répertoires auxquels votre application peut accéder, ce mécanisme ne nécessite aucune autorisation système. Le contrôle utilisateur et la confidentialité s'en trouvent également renforcés. De plus, ces fichiers, qui sont stockés en dehors d'un répertoire spécifique à l'application et en dehors du magasin multimédia (Media Store), restent sur l'appareil après la désinstallation de votre application.
L'utilisation du framework comprend les étapes suivantes :
- Une application appelle un intent qui contient une action liée au stockage. Cette action correspond à un cas d'utilisation spécifique rendu disponible par le framework.
- L'utilisateur voit un sélecteur système, ce qui lui permet de parcourir un fournisseur de documents et de choisir un emplacement ou un document où est effectuée l'action liée au stockage.
- L'application obtient un accès en lecture et en écriture à un URI qui représente l'emplacement ou le document choisi par l'utilisateur. À l'aide de cet URI, l'application peut effectuer des opérations sur l'emplacement sélectionné.
Pour permettre l'accès aux fichiers multimédias sur les appareils équipés d'Android 9 (niveau d'API 28) ou version antérieure, déclarez l'autorisation READ_EXTERNAL_STORAGE
et définissez maxSdkVersion
sur 28
.
Ce guide décrit les différents cas d'utilisation acceptés par le framework pour utiliser des fichiers et d'autres documents. Il explique également comment effectuer des opérations sur l'emplacement sélectionné par l'utilisateur.
Cas d'utilisation pour accéder à des documents et à d'autres fichiers
Storage Access Framework prend en charge les cas d'utilisation suivants pour accéder à des fichiers et à d'autres documents.
- Créer un fichier
- L'action d'intent
ACTION_CREATE_DOCUMENT
permet aux utilisateurs d'enregistrer un fichier à un emplacement spécifique. - Ouvrir un document ou un fichier
- L'action d'intent
ACTION_OPEN_DOCUMENT
permet aux utilisateurs de sélectionner un document ou un fichier spécifique à ouvrir. - Accorder l'accès au contenu d'un répertoire
- L'action d'intent
ACTION_OPEN_DOCUMENT_TREE
, disponible sur Android 5.0 (niveau d'API 21) ou version ultérieure, permet aux utilisateurs de sélectionner un répertoire spécifique, autorisant ainsi votre application à accéder à l'ensemble des fichiers et sous-répertoires de ce répertoire.
Les sections suivantes expliquent comment configurer chaque cas d'utilisation.
Créer un fichier
Utilisez l'action d'intent ACTION_CREATE_DOCUMENT
pour charger le sélecteur de fichier système et autoriser l'utilisateur à choisir l'emplacement d'écriture du contenu d'un fichier. Ce processus est semblable à celui appliqué dans les boîtes de dialogue "Enregistrer sous" utilisées par d'autres systèmes d'exploitation.
Remarque : ACTION_CREATE_DOCUMENT
ne peut pas écraser un fichier existant. Si votre application tente d'enregistrer un fichier portant le même nom, le système ajoute un numéro entre parenthèses à la fin du nom du fichier.
Par exemple, si votre application tente d'enregistrer un fichier nommé confirmation.pdf
dans un répertoire contenant déjà un fichier portant ce nom, le système enregistre le nouveau fichier sous le nom confirmation(1).pdf
.
Lors de la configuration de l'intent, spécifiez le type MIME et le nom du fichier, et éventuellement l'URI du fichier ou du répertoire que le sélecteur de fichier doit afficher lors de son premier chargement en utilisant l'extra d'intent EXTRA_INITIAL_URI
.
L'extrait de code suivant montre comment créer et appeler l'intent pour créer un fichier :
Kotlin
// Request code for creating a PDF document. const val CREATE_FILE = 1 private fun createFile(pickerInitialUri: Uri) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/pdf" putExtra(Intent.EXTRA_TITLE, "invoice.pdf") // Optionally, specify a URI for the directory that should be opened in // the system file picker before your app creates the document. putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) } startActivityForResult(intent, CREATE_FILE) }
Java
// Request code for creating a PDF document. private static final int CREATE_FILE = 1; private void createFile(Uri pickerInitialUri) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("application/pdf"); intent.putExtra(Intent.EXTRA_TITLE, "invoice.pdf"); // Optionally, specify a URI for the directory that should be opened in // the system file picker when your app creates the document. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri); startActivityForResult(intent, CREATE_FILE); }
Ouvrir un fichier
Il se peut que votre application utilise des documents comme unité de stockage dans laquelle les utilisateurs saisissent des données qu'ils souhaitent peut-être partager avec d'autres personnes ou importer dans d'autres documents. Par exemple : un utilisateur ouvre un document de productivité ou bien un livre enregistré sous la forme d'un fichier EPUB.
Dans ce cas, autorisez l'utilisateur à choisir le fichier à ouvrir en appelant l'intent ACTION_OPEN_DOCUMENT
qui ouvre l'application de sélection de fichier du système. Pour n'afficher que les types de fichiers compatibles avec votre application, spécifiez un type MIME. Vous pouvez également spécifier l'URI du fichier que le sélecteur de fichier doit afficher lors du premier chargement. Pour ce faire, utilisez l'extra d'intent EXTRA_INITIAL_URI
.
L'extrait de code suivant montre comment créer et appeler l'intent pour ouvrir un document PDF :
Kotlin
// Request code for selecting a PDF document. const val PICK_PDF_FILE = 2 fun openFile(pickerInitialUri: Uri) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/pdf" // Optionally, specify a URI for the file that should appear in the // system file picker when it loads. putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) } startActivityForResult(intent, PICK_PDF_FILE) }
Java
// Request code for selecting a PDF document. private static final int PICK_PDF_FILE = 2; private void openFile(Uri pickerInitialUri) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("application/pdf"); // Optionally, specify a URI for the file that should appear in the // system file picker when it loads. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri); startActivityForResult(intent, PICK_PDF_FILE); }
Restrictions d'accès
Sur Android 11 (niveau d'API 30) ou version ultérieure, vous ne pouvez pas utiliser l'action d'intent ACTION_OPEN_DOCUMENT
pour demander à l'utilisateur de sélectionner des fichiers dans les répertoires suivants :
- Répertoire
Android/data/
et tous ses sous-répertoires. - Répertoire
Android/obb/
et tous ses sous-répertoires.
Accorder l'accès au contenu d'un répertoire
Les applications de gestion de fichiers et de création multimédia gèrent généralement des groupes de fichiers dans une hiérarchie de répertoire. Pour fournir cette fonctionnalité dans votre application, utilisez l'action d'intent ACTION_OPEN_DOCUMENT_TREE
qui permet à l'utilisateur d'accorder l'accès à l'intégralité d'une arborescence de répertoire, avec toutefois quelques exceptions à partir d'Android 11 (niveau d'API 30). Votre application peut alors accéder à n'importe quel fichier du répertoire sélectionné, ainsi qu'à tous ses sous-répertoires.
Lorsque l'action d'intent ACTION_OPEN_DOCUMENT_TREE
est utilisée, votre application n'a accès qu'aux fichiers du répertoire sélectionné par l'utilisateur. Les fichiers d'autres applications qui résident en dehors de ce répertoire ne sont pas accessibles. Cet accès contrôlé par l'utilisateur lui permet de choisir précisément le contenu qu'il souhaite partager avec votre application.
Vous pouvez éventuellement spécifier l'URI du répertoire que le sélecteur de fichier doit afficher lors de son premier chargement en utilisant l'extra d'intent EXTRA_INITIAL_URI
.
L'extrait de code suivant montre comment créer et appeler l'intent pour ouvrir un répertoire :
Kotlin
fun openDirectory(pickerInitialUri: Uri) { // Choose a directory using the system's file picker. val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { // Optionally, specify a URI for the directory that should be opened in // the system file picker when it loads. putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) } startActivityForResult(intent, your-request-code) }
Java
public void openDirectory(Uri uriToLoad) { // Choose a directory using the system's file picker. Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); // Optionally, specify a URI for the directory that should be opened in // the system file picker when it loads. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uriToLoad); startActivityForResult(intent, your-request-code); }
Restrictions d'accès
Sur Android 11 (niveau d'API 30) ou version ultérieure, vous ne pouvez pas utiliser l'action d'intent ACTION_OPEN_DOCUMENT_TREE
pour demander l'accès aux répertoires suivants :
- Répertoire racine du volume de stockage interne.
- Répertoire racine de chaque volume de carte SD que le fabricant de l'appareil considère comme fiable, que la carte soit émulée ou amovible. Un volume fiable est un volume auquel une application peut accéder sans problème la plupart du temps.
- Répertoire
Download
De plus, sur Android 11 (niveau d'API 30) ou version ultérieure, vous ne pouvez pas utiliser l'action d'intent ACTION_OPEN_DOCUMENT_TREE
pour demander à l'utilisateur de sélectionner des fichiers dans les répertoires suivants :
- Répertoire
Android/data/
et tous ses sous-répertoires. - Répertoire
Android/obb/
et tous ses sous-répertoires.
Effectuer des opérations sur l'emplacement sélectionné
Une fois que l'utilisateur a sélectionné un fichier ou un répertoire à l'aide du sélecteur de fichier du système, vous pouvez récupérer l'URI de l'élément sélectionné à l'aide du code suivant dans onActivityResult()
:
Kotlin
override fun onActivityResult( requestCode: Int, resultCode: Int, resultData: Intent?) { if (requestCode == your-request-code && resultCode == Activity.RESULT_OK) { // The result data contains a URI for the document or directory that // the user selected. resultData?.data?.also { uri -> // Perform operations on the document using its URI. } } }
Java
@Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { if (requestCode == your-request-code && resultCode == Activity.RESULT_OK) { // The result data contains a URI for the document or directory that // the user selected. Uri uri = null; if (resultData != null) { uri = resultData.getData(); // Perform operations on the document using its URI. } } }
En obtenant une référence à l'URI de l'élément sélectionné, votre application peut effectuer plusieurs opérations sur cet élément. Vous pouvez, par exemple, accéder aux métadonnées de l'élément, le modifier à son emplacement actuel et le supprimer.
Les sections suivantes expliquent comment effectuer des actions sur les fichiers sélectionnés par l'utilisateur.
Déterminer les opérations prises en charge par un fournisseur
Les opérations qu'il est possible d'effectuer sur un document, comme le copier ou afficher la vignette correspondante, varient en fonction des fournisseurs de contenu. Pour déterminer les opérations acceptées par un fournisseur donné, vérifiez la valeur de Document.COLUMN_FLAGS
.
L'UI de votre application peut alors afficher uniquement les options acceptées par le fournisseur.
Autorisations persistantes
Lorsque votre application ouvre un fichier en lecture ou en écriture, le système lui accorde une autorisation d'URI sur ce fichier. Cette autorisation reste valable jusqu'au redémarrage de l'appareil de l'utilisateur. Supposons maintenant que vous proposiez une application de retouche d'image et que vous souhaitiez que les utilisateurs puissent accéder aux cinq dernières images qu'ils ont modifiées, et ce, directement depuis votre application. Si l'appareil a redémarré, vous devrez rediriger l'utilisateur vers le sélecteur système pour rechercher les fichiers.
Pour conserver l'accès aux fichiers entre deux redémarrages de l'appareil et améliorer l'expérience utilisateur, votre application peut "prendre" l'octroi d'autorisation d'URI persistante proposé par le système, comme indiqué dans l'extrait de code suivant :
Kotlin
val contentResolver = applicationContext.contentResolver val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION // Check for the freshest data. contentResolver.takePersistableUriPermission(uri, takeFlags)
Java
final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // Check for the freshest data. getContentResolver().takePersistableUriPermission(uri, takeFlags);
Examiner les métadonnées d'un document
Lorsque vous disposez de l'URI d'un document, vous avez accès à ses métadonnées. Cet extrait de code récupère les métadonnées d'un document spécifié par l'URI et les consigne :
Kotlin
val contentResolver = applicationContext.contentResolver fun dumpImageMetaData(uri: Uri) { // The query, because it only applies to a single document, returns only // one row. There's no need to filter, sort, or select fields, // because we want all fields for one document. val cursor: Cursor? = contentResolver.query( uri, null, null, null, null, null) cursor?.use { // moveToFirst() returns false if the cursor has 0 rows. Very handy for // "if there's anything to look at, look at it" conditionals. if (it.moveToFirst()) { // Note it's called "Display Name". This is // provider-specific, and might not necessarily be the file name. val displayName: String = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) Log.i(TAG, "Display Name: $displayName") val sizeIndex: Int = it.getColumnIndex(OpenableColumns.SIZE) // If the size is unknown, the value stored is null. But because an // int can't be null, the behavior is implementation-specific, // and unpredictable. So as // a rule, check if it's null before assigning to an int. This will // happen often: The storage API allows for remote files, whose // size might not be locally known. val size: String = if (!it.isNull(sizeIndex)) { // Technically the column stores an int, but cursor.getString() // will do the conversion automatically. it.getString(sizeIndex) } else { "Unknown" } Log.i(TAG, "Size: $size") } } }
Java
public void dumpImageMetaData(Uri uri) { // The query, because it only applies to a single document, returns only // one row. There's no need to filter, sort, or select fields, // because we want all fields for one document. Cursor cursor = getActivity().getContentResolver() .query(uri, null, null, null, null, null); try { // moveToFirst() returns false if the cursor has 0 rows. Very handy for // "if there's anything to look at, look at it" conditionals. if (cursor != null && cursor.moveToFirst()) { // Note it's called "Display Name". This is // provider-specific, and might not necessarily be the file name. String displayName = cursor.getString( cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); Log.i(TAG, "Display Name: " + displayName); int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); // If the size is unknown, the value stored is null. But because an // int can't be null, the behavior is implementation-specific, // and unpredictable. So as // a rule, check if it's null before assigning to an int. This will // happen often: The storage API allows for remote files, whose // size might not be locally known. String size = null; if (!cursor.isNull(sizeIndex)) { // Technically the column stores an int, but cursor.getString() // will do the conversion automatically. size = cursor.getString(sizeIndex); } else { size = "Unknown"; } Log.i(TAG, "Size: " + size); } } finally { cursor.close(); } }
Ouvrir un document
Le fait de disposer d'une référence à l'URI d'un document vous permet de l'ouvrir en vue d'un traitement plus poussé. Cette section présente des exemples d'ouverture d'un bitmap et d'un flux d'entrée.
Bitmap
L'extrait de code suivant montre comment ouvrir un fichier Bitmap
en fonction de son URI :
Kotlin
val contentResolver = applicationContext.contentResolver @Throws(IOException::class) private fun getBitmapFromUri(uri: Uri): Bitmap { val parcelFileDescriptor: ParcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r") val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor val image: Bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor) parcelFileDescriptor.close() return image }
Java
private Bitmap getBitmapFromUri(Uri uri) throws IOException { ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri, "r"); FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor); parcelFileDescriptor.close(); return image; }
Après avoir ouvert le bitmap, vous pouvez l'afficher dans une ImageView
.
Flux d'entrée
L'extrait de code suivant montre comment ouvrir un objet InputStream en fonction de son URI. Dans cet extrait, les lignes du fichier sont lues dans une chaîne :
Kotlin
val contentResolver = applicationContext.contentResolver @Throws(IOException::class) private fun readTextFromUri(uri: Uri): String { val stringBuilder = StringBuilder() contentResolver.openInputStream(uri)?.use { inputStream -> BufferedReader(InputStreamReader(inputStream)).use { reader -> var line: String? = reader.readLine() while (line != null) { stringBuilder.append(line) line = reader.readLine() } } } return stringBuilder.toString() }
Java
private String readTextFromUri(Uri uri) throws IOException { StringBuilder stringBuilder = new StringBuilder(); try (InputStream inputStream = getContentResolver().openInputStream(uri); BufferedReader reader = new BufferedReader( new InputStreamReader(Objects.requireNonNull(inputStream)))) { String line; while ((line = reader.readLine()) != null) { stringBuilder.append(line); } } return stringBuilder.toString(); }
Modifier un document
Vous pouvez utiliser Storage Access Framework pour modifier un document texte à son emplacement.
L'extrait de code suivant écrase le contenu du document représenté par l'URI donné :
Kotlin
val contentResolver = applicationContext.contentResolver private fun alterDocument(uri: Uri) { try { contentResolver.openFileDescriptor(uri, "w")?.use { FileOutputStream(it.fileDescriptor).use { it.write( ("Overwritten at ${System.currentTimeMillis()}\n") .toByteArray() ) } } } catch (e: FileNotFoundException) { e.printStackTrace() } catch (e: IOException) { e.printStackTrace() } }
Java
private void alterDocument(Uri uri) { try { ParcelFileDescriptor pfd = getActivity().getContentResolver(). openFileDescriptor(uri, "w"); FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor()); fileOutputStream.write(("Overwritten at " + System.currentTimeMillis() + "\n").getBytes()); // Let the document provider know you're done by closing the stream. fileOutputStream.close(); pfd.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
Supprimer un document
Si vous disposez de l'URI d'un document et que la constante Document.COLUMN_FLAGS
du document contient SUPPORTS_DELETE
, vous pouvez supprimer le document. Exemple :
Kotlin
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)
Java
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);
Récupérer un URI multimédia équivalent
La méthode getMediaUri()
fournit un URI de magasin multimédia qui équivaut à l'URI du fournisseur de documents donné. Les deux URI font référence au même élément sous-jacent. L'URI du magasin multimédia vous permet d'accéder plus facilement aux fichiers multimédias d'un espace de stockage partagé.
La méthode getMediaUri()
accepte les URI ExternalStorageProvider
. Sur Android 12 (niveau d'API 31) ou version ultérieure, la méthode accepte également les URI MediaDocumentsProvider
.
Ouvrir un fichier virtuel
Sur Android 7.0 (niveau d'API 25) ou version ultérieure, votre application peut utiliser des fichiers virtuels mis à disposition par Storage Access Framework. Même si les fichiers virtuels n'ont pas de représentation binaire, votre application peut ouvrir leur contenu en forçant la conversion dans un autre type de fichier ou en les affichant à l'aide de l'action d'intent ACTION_VIEW
.
Pour ouvrir des fichiers virtuels, votre application cliente doit inclure une logique spéciale pour les gérer. Si vous souhaitez obtenir une représentation octale du fichier (pour le prévisualiser, par exemple), vous devez demander un autre type MIME au fournisseur de documents.
Une fois que l'utilisateur a effectué sa sélection, utilisez l'URI dans les données de résultats pour déterminer s'il s'agit d'un fichier virtuel, comme illustré dans l'extrait de code suivant :
Kotlin
private fun isVirtualFile(uri: Uri): Boolean { if (!DocumentsContract.isDocumentUri(this, uri)) { return false } val cursor: Cursor? = contentResolver.query( uri, arrayOf(DocumentsContract.Document.COLUMN_FLAGS), null, null, null ) val flags: Int = cursor?.use { if (cursor.moveToFirst()) { cursor.getInt(0) } else { 0 } } ?: 0 return flags and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0 }
Java
private boolean isVirtualFile(Uri uri) { if (!DocumentsContract.isDocumentUri(this, uri)) { return false; } Cursor cursor = getContentResolver().query( uri, new String[] { DocumentsContract.Document.COLUMN_FLAGS }, null, null, null); int flags = 0; if (cursor.moveToFirst()) { flags = cursor.getInt(0); } cursor.close(); return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0; }
Après avoir vérifié que le document était un fichier virtuel, vous pouvez forcer la conversion dans un autre type MIME tel que "image/png"
. L'extrait de code suivant montre comment vérifier si un fichier virtuel peut être représenté sous la forme d'une image et, le cas échéant, obtenir un flux d'entrée à partir du fichier virtuel :
Kotlin
@Throws(IOException::class) private fun getInputStreamForVirtualFile( uri: Uri, mimeTypeFilter: String): InputStream { val openableMimeTypes: Array<String>? = contentResolver.getStreamTypes(uri, mimeTypeFilter) return if (openableMimeTypes?.isNotEmpty() == true) { contentResolver .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null) .createInputStream() } else { throw FileNotFoundException() } }
Java
private InputStream getInputStreamForVirtualFile(Uri uri, String mimeTypeFilter) throws IOException { ContentResolver resolver = getContentResolver(); String[] openableMimeTypes = resolver.getStreamTypes(uri, mimeTypeFilter); if (openableMimeTypes == null || openableMimeTypes.length < 1) { throw new FileNotFoundException(); } return resolver .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null) .createInputStream(); }
Ressources supplémentaires
Pour en savoir plus sur le stockage de documents et d'autres fichiers, ainsi que sur leur accès, consultez les ressources ci-dessous.
Exemples
- ActionOpenDocument, disponible sur GitHub.
- ActionOpenDocumentTree, disponible sur GitHub.