Si vous développez une application qui fournit des services de stockage de fichiers (par exemple, un service d'enregistrement dans le cloud), vous pouvez rendre vos fichiers disponibles via Storage Access Framework (SAF) en écrivant un fournisseur de documents personnalisé. Cette page explique comment créer un fournisseur de documents personnalisés.
Pour en savoir plus sur son fonctionnement, consultez la présentation de Storage Access Framework.
Manifest
Pour implémenter un fournisseur de documents personnalisés, ajoutez le code suivant au fichier manifeste de votre application:
- Une cible de niveau d'API 19 ou supérieur
- Un élément
<provider>
qui déclare votre fournisseur de stockage personnalisé. -
L'attribut
android:name
défini sur le nom de votre sous-classeDocumentsProvider
, qui correspond au nom de sa classe, incluant le nom du package:com.example.android.storageprovider.MyCloudProvider
. -
L'attribut
android:authority
, qui correspond au nom de votre package (dans cet exemple,com.example.android.storageprovider
) et au type de fournisseur de contenu (documents
). - Attribut
android:exported
défini sur"true"
Vous devez exporter votre fournisseur pour que d'autres applications puissent le voir. - L'attribut
android:grantUriPermissions
défini sur"true"
Ce paramètre permet au système d'autoriser d'autres applications à accéder au contenu de votre fournisseur. Pour découvrir comment ces autres applications peuvent conserver leur accès au contenu de votre fournisseur, consultez la section Persistance des autorisations. - L'autorisation
MANAGE_DOCUMENTS
Par défaut, un fournisseur est accessible à tous. L'ajout de cette autorisation limite votre fournisseur au système. Cette restriction est importante pour la sécurité. - Un filtre d'intent qui inclut l'action
android.content.action.DOCUMENTS_PROVIDER
, de sorte que votre fournisseur apparaisse dans l'outil de sélection lorsque le système recherche des fournisseurs.
Voici des extraits d'un exemple de fichier manifeste incluant un fournisseur:
<manifest... > ... <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="19" /> .... <provider android:name="com.example.android.storageprovider.MyCloudProvider" android:authorities="com.example.android.storageprovider.documents" android:grantUriPermissions="true" android:exported="true" android:permission="android.permission.MANAGE_DOCUMENTS"> <intent-filter> <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> </intent-filter> </provider> </application> </manifest>
Compatibilité avec les appareils équipés d'Android 4.3 ou version antérieure
L'intent ACTION_OPEN_DOCUMENT
n'est disponible que sur les appareils équipés d'Android 4.4 ou version ultérieure.
Si vous souhaitez que votre application prenne en charge ACTION_GET_CONTENT
pour les appareils équipés d'Android 4.3 ou version antérieure, vous devez désactiver le filtre d'intent ACTION_GET_CONTENT
dans votre fichier manifeste pour les appareils équipés d'Android 4.4 ou version ultérieure. Un fournisseur de documents et ACTION_GET_CONTENT
doivent être considérés comme s'excluant mutuellement. Si vous prenez en charge les deux en même temps, votre application apparaît deux fois dans l'UI du sélecteur système, offrant deux façons différentes d'accéder aux données stockées. C'est déroutant pour les utilisateurs.
Voici la méthode recommandée pour désactiver le filtre d'intent ACTION_GET_CONTENT
pour les appareils équipés d'Android version 4.4 ou ultérieure:
- Dans votre fichier de ressources
bool.xml
, sousres/values/
, ajoutez la ligne suivante:<bool name="atMostJellyBeanMR2">true</bool>
- Dans votre fichier de ressources
bool.xml
, sousres/values-v19/
, ajoutez la ligne suivante:<bool name="atMostJellyBeanMR2">false</bool>
- Ajoutez un alias d'activité pour désactiver le filtre d'intent
ACTION_GET_CONTENT
pour les versions 4.4 (niveau d'API 19) et ultérieures. Exemple :<!-- This activity alias is added so that GET_CONTENT intent-filter can be disabled for builds on API level 19 and higher. --> <activity-alias android:name="com.android.example.app.MyPicker" android:targetActivity="com.android.example.app.MyActivity" ... android:enabled="@bool/atMostJellyBeanMR2"> <intent-filter> <action android:name="android.intent.action.GET_CONTENT" /> <category android:name="android.intent.category.OPENABLE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="image/*" /> <data android:mimeType="video/*" /> </intent-filter> </activity-alias>
Contrats
Généralement, lorsque vous écrivez un fournisseur de contenu personnalisé, l'une des tâches consiste à implémenter des classes de contrat, comme décrit dans le guide du développeur pour les
fournisseurs de contenu. Une classe de contrat est une classe public final
qui contient des définitions constantes des URI, des noms de colonne, des types MIME et d'autres métadonnées liées au fournisseur. La SAF fournit ces classes de contrat pour vous. Vous n'avez donc pas besoin d'écrire la vôtre:
Par exemple, voici les colonnes que vous pouvez renvoyer dans un curseur lorsque votre fournisseur de documents est interrogé pour obtenir des documents ou la racine:
Kotlin
private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf( DocumentsContract.Root.COLUMN_ROOT_ID, DocumentsContract.Root.COLUMN_MIME_TYPES, DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.COLUMN_ICON, DocumentsContract.Root.COLUMN_TITLE, DocumentsContract.Root.COLUMN_SUMMARY, DocumentsContract.Root.COLUMN_DOCUMENT_ID, DocumentsContract.Root.COLUMN_AVAILABLE_BYTES ) private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf( DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_FLAGS, DocumentsContract.Document.COLUMN_SIZE )
Java
private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,}; private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};
Le curseur de la racine doit inclure certaines colonnes obligatoires. Ces colonnes sont les suivantes:
Le curseur des documents doit inclure les colonnes obligatoires suivantes:
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
Créer une sous-classe de DocumentsProvider
L'étape suivante de l'écriture d'un fournisseur de documents personnalisés consiste à sous-classer la classe abstraite DocumentsProvider
. Vous devez au minimum implémenter les méthodes suivantes:
Ce sont les seules méthodes que vous êtes strictement tenu de mettre en œuvre, mais il en existe beaucoup d'autres que vous voudrez peut-être faire. Pour en savoir plus, consultez DocumentsProvider
.
Définir une racine
Votre implémentation de queryRoots()
doit renvoyer un Cursor
pointant vers tous les répertoires racine de votre fournisseur de documents, à l'aide des colonnes définies dans DocumentsContract.Root
.
Dans l'extrait de code suivant, le paramètre projection
représente les champs spécifiques que l'appelant souhaite récupérer. L'extrait crée un curseur et y ajoute une ligne : une racine, un répertoire de premier niveau tel que "Téléchargements" ou "Images". La plupart des fournisseurs n'ont qu'une seule racine. Vous pouvez avoir plusieurs comptes utilisateur, par exemple. Dans ce cas, ajoutez simplement une
deuxième ligne au curseur.
Kotlin
override fun queryRoots(projection: Array<out String>?): Cursor { // Use a MatrixCursor to build a cursor // with either the requested fields, or the default // projection if "projection" is null. val result = MatrixCursor(resolveRootProjection(projection)) // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) { return result } // It's possible to have multiple roots (e.g. for multiple accounts in the // same app) -- just add multiple cursor rows. result.newRow().apply { add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT) // You can provide an optional summary, which helps distinguish roots // with the same title. You can also use this field for displaying an // user account name. add(DocumentsContract.Root.COLUMN_SUMMARY, context.getString(R.string.root_summary)) // FLAG_SUPPORTS_CREATE means at least one directory under the root supports // creating documents. FLAG_SUPPORTS_RECENTS means your application's most // recently used documents will show up in the "Recents" category. // FLAG_SUPPORTS_SEARCH allows users to search all documents the application // shares. add( DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or DocumentsContract.Root.FLAG_SUPPORTS_SEARCH ) // COLUMN_TITLE is the root title (e.g. Gallery, Drive). add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.title)) // This document id cannot change after it's shared. add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir)) // The child MIME types are used to filter the roots and only present to the // user those roots that contain the desired type somewhere in their file hierarchy. add(DocumentsContract.Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir)) add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDir.freeSpace) add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher) } return result }
Java
@Override public Cursor queryRoots(String[] projection) throws FileNotFoundException { // Use a MatrixCursor to build a cursor // with either the requested fields, or the default // projection if "projection" is null. final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) { return result; } // It's possible to have multiple roots (e.g. for multiple accounts in the // same app) -- just add multiple cursor rows. final MatrixCursor.RowBuilder row = result.newRow(); row.add(Root.COLUMN_ROOT_ID, ROOT); // You can provide an optional summary, which helps distinguish roots // with the same title. You can also use this field for displaying an // user account name. row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary)); // FLAG_SUPPORTS_CREATE means at least one directory under the root supports // creating documents. FLAG_SUPPORTS_RECENTS means your application's most // recently used documents will show up in the "Recents" category. // FLAG_SUPPORTS_SEARCH allows users to search all documents the application // shares. row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH); // COLUMN_TITLE is the root title (e.g. Gallery, Drive). row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title)); // This document id cannot change after it's shared. row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir)); // The child MIME types are used to filter the roots and only present to the // user those roots that contain the desired type somewhere in their file hierarchy. row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir)); row.add(Root.COLUMN_AVAILABLE_BYTES, baseDir.getFreeSpace()); row.add(Root.COLUMN_ICON, R.drawable.ic_launcher); return result; }
Si votre fournisseur de documents se connecte à un ensemble dynamique de racines (par exemple, à un périphérique USB déconnecté ou à un compte dont l'utilisateur peut se déconnecter), vous pouvez mettre à jour l'UI du document pour qu'elle reste synchronisée avec ces modifications à l'aide de la méthode ContentResolver.notifyChange()
, comme indiqué dans l'extrait de code suivant.
Kotlin
val rootsUri: Uri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY) context.contentResolver.notifyChange(rootsUri, null)
Java
Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY); context.getContentResolver().notifyChange(rootsUri, null);
Lister les documents du fournisseur
Votre implémentation de queryChildDocuments()
doit renvoyer un Cursor
qui pointe vers tous les fichiers du répertoire spécifié, à l'aide des colonnes définies dans DocumentsContract.Document
.
Cette méthode est appelée lorsque l'utilisateur choisit votre racine dans l'interface utilisateur du sélecteur.
La méthode récupère les enfants de l'ID de document spécifié par COLUMN_DOCUMENT_ID
.
Le système appelle ensuite cette méthode chaque fois que l'utilisateur sélectionne un sous-répertoire au sein de votre fournisseur de documents.
Cet extrait crée un curseur avec les colonnes demandées, puis ajoute au curseur des informations sur chaque enfant immédiat du répertoire parent. Un enfant peut être une image, un autre répertoire ou n'importe quel fichier:
Kotlin
override fun queryChildDocuments( parentDocumentId: String?, projection: Array<out String>?, sortOrder: String? ): Cursor { return MatrixCursor(resolveDocumentProjection(projection)).apply { val parent: File = getFileForDocId(parentDocumentId) parent.listFiles() .forEach { file -> includeFile(this, null, file) } } }
Java
@Override public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); final File parent = getFileForDocId(parentDocumentId); for (File file : parent.listFiles()) { // Adds the file's display name, MIME type, size, and so on. includeFile(result, null, file); } return result; }
Obtenir des informations sur le document
Votre implémentation de queryDocument()
doit renvoyer un Cursor
qui pointe vers le fichier spécifié, à l'aide des colonnes définies dans DocumentsContract.Document
.
La méthode queryDocument()
renvoie les mêmes informations que celles transmises dans queryChildDocuments()
, mais pour un fichier spécifique:
Kotlin
override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor { // Create a cursor with the requested projection, or the default projection. return MatrixCursor(resolveDocumentProjection(projection)).apply { includeFile(this, documentId, null) } }
Java
@Override public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { // Create a cursor with the requested projection, or the default projection. final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); includeFile(result, documentId, null); return result; }
Votre fournisseur de documents peut également fournir des vignettes pour un document en remplaçant la méthode DocumentsProvider.openDocumentThumbnail()
et en ajoutant l'option FLAG_SUPPORTS_THUMBNAIL
aux fichiers compatibles.
L'extrait de code suivant fournit un exemple d'implémentation de DocumentsProvider.openDocumentThumbnail()
.
Kotlin
override fun openDocumentThumbnail( documentId: String?, sizeHint: Point?, signal: CancellationSignal? ): AssetFileDescriptor { val file = getThumbnailFileForDocId(documentId) val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) return AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH) }
Java
@Override public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { final File file = getThumbnailFileForDocId(documentId); final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); }
Attention : Un fournisseur de documents ne doit pas renvoyer des vignettes dont la taille dépasse le double de la taille spécifiée par le paramètre sizeHint
.
Ouvrir un document
Vous devez implémenter openDocument()
pour renvoyer un ParcelFileDescriptor
représentant le fichier spécifié. D'autres applications peuvent utiliser le ParcelFileDescriptor
renvoyé pour diffuser des données. Le système appelle cette méthode une fois que l'utilisateur a sélectionné un fichier, et l'application cliente demande l'accès au fichier en appelant openFileDescriptor()
.
Par exemple :
Kotlin
override fun openDocument( documentId: String, mode: String, signal: CancellationSignal ): ParcelFileDescriptor { Log.v(TAG, "openDocument, mode: $mode") // It's OK to do network operations in this method to download the document, // as long as you periodically check the CancellationSignal. If you have an // extremely large file to transfer from the network, a better solution may // be pipes or sockets (see ParcelFileDescriptor for helper methods). val file: File = getFileForDocId(documentId) val accessMode: Int = ParcelFileDescriptor.parseMode(mode) val isWrite: Boolean = mode.contains("w") return if (isWrite) { val handler = Handler(context.mainLooper) // Attach a close listener if the document is opened in write mode. try { ParcelFileDescriptor.open(file, accessMode, handler) { // Update the file with the cloud server. The client is done writing. Log.i(TAG, "A file with id $documentId has been closed! Time to update the server.") } } catch (e: IOException) { throw FileNotFoundException( "Failed to open document with id $documentId and mode $mode" ) } } else { ParcelFileDescriptor.open(file, accessMode) } }
Java
@Override public ParcelFileDescriptor openDocument(final String documentId, final String mode, CancellationSignal signal) throws FileNotFoundException { Log.v(TAG, "openDocument, mode: " + mode); // It's OK to do network operations in this method to download the document, // as long as you periodically check the CancellationSignal. If you have an // extremely large file to transfer from the network, a better solution may // be pipes or sockets (see ParcelFileDescriptor for helper methods). final File file = getFileForDocId(documentId); final int accessMode = ParcelFileDescriptor.parseMode(mode); final boolean isWrite = (mode.indexOf('w') != -1); if(isWrite) { // Attach a close listener if the document is opened in write mode. try { Handler handler = new Handler(getContext().getMainLooper()); return ParcelFileDescriptor.open(file, accessMode, handler, new ParcelFileDescriptor.OnCloseListener() { @Override public void onClose(IOException e) { // Update the file with the cloud server. The client is done // writing. Log.i(TAG, "A file with id " + documentId + " has been closed! Time to " + "update the server."); } }); } catch (IOException e) { throw new FileNotFoundException("Failed to open document with id" + documentId + " and mode " + mode); } } else { return ParcelFileDescriptor.open(file, accessMode); } }
Si votre fournisseur de documents diffuse des fichiers ou gère des structures de données complexes, envisagez d'implémenter les méthodes createReliablePipe()
ou createReliableSocketPair()
.
Ces méthodes vous permettent de créer une paire d'objets ParcelFileDescriptor
, dans lesquels vous pouvez en renvoyer un et envoyer l'autre via un ParcelFileDescriptor.AutoCloseOutputStream
ou un ParcelFileDescriptor.AutoCloseInputStream
.
Prendre en charge les recherches et les documents récents
Vous pouvez fournir une liste des documents récemment modifiés à la racine de votre fournisseur de documents en remplaçant la méthode queryRecentDocuments()
et en renvoyant FLAG_SUPPORTS_RECENTS
. L'extrait de code suivant montre comment implémenter les méthodes queryRecentDocuments()
.
Kotlin
override fun queryRecentDocuments(rootId: String?, projection: Array<out String>?): Cursor { // This example implementation walks a // local file structure to find the most recently // modified files. Other implementations might // include making a network call to query a // server. // Create a cursor with the requested projection, or the default projection. val result = MatrixCursor(resolveDocumentProjection(projection)) val parent: File = getFileForDocId(rootId) // Create a queue to store the most recent documents, // which orders by last modified. val lastModifiedFiles = PriorityQueue( 5, Comparator<File> { i, j -> Long.compare(i.lastModified(), j.lastModified()) } ) // Iterate through all files and directories // in the file structure under the root. If // the file is more recent than the least // recently modified, add it to the queue, // limiting the number of results. val pending : MutableList<File> = mutableListOf() // Start by adding the parent to the list of files to be processed pending.add(parent) // Do while we still have unexamined files while (pending.isNotEmpty()) { // Take a file from the list of unprocessed files val file: File = pending.removeAt(0) if (file.isDirectory) { // If it's a directory, add all its children to the unprocessed list pending += file.listFiles() } else { // If it's a file, add it to the ordered queue. lastModifiedFiles.add(file) } } // Add the most recent files to the cursor, // not exceeding the max number of results. for (i in 0 until Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size)) { val file: File = lastModifiedFiles.remove() includeFile(result, null, file) } return result }
Java
@Override public Cursor queryRecentDocuments(String rootId, String[] projection) throws FileNotFoundException { // This example implementation walks a // local file structure to find the most recently // modified files. Other implementations might // include making a network call to query a // server. // Create a cursor with the requested projection, or the default projection. final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); final File parent = getFileForDocId(rootId); // Create a queue to store the most recent documents, // which orders by last modified. PriorityQueue lastModifiedFiles = new PriorityQueue(5, new Comparator() { public int compare(File i, File j) { return Long.compare(i.lastModified(), j.lastModified()); } }); // Iterate through all files and directories // in the file structure under the root. If // the file is more recent than the least // recently modified, add it to the queue, // limiting the number of results. final LinkedList pending = new LinkedList(); // Start by adding the parent to the list of files to be processed pending.add(parent); // Do while we still have unexamined files while (!pending.isEmpty()) { // Take a file from the list of unprocessed files final File file = pending.removeFirst(); if (file.isDirectory()) { // If it's a directory, add all its children to the unprocessed list Collections.addAll(pending, file.listFiles()); } else { // If it's a file, add it to the ordered queue. lastModifiedFiles.add(file); } } // Add the most recent files to the cursor, // not exceeding the max number of results. for (int i = 0; i < Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size()); i++) { final File file = lastModifiedFiles.remove(); includeFile(result, null, file); } return result; }
Pour obtenir le code complet de l'extrait ci-dessus, téléchargez l'exemple de code StorageProvider.
Création de documents d'assistance
Vous pouvez autoriser les applications clientes à créer des fichiers dans votre fournisseur de documents.
Si une application cliente envoie un intent ACTION_CREATE_DOCUMENT
, votre fournisseur de documents peut l'autoriser à créer des documents au sein du fournisseur de documents.
Pour permettre la création de documents, votre racine doit disposer de l'option FLAG_SUPPORTS_CREATE
.
Les répertoires qui permettent de créer des fichiers doivent comporter l'option FLAG_DIR_SUPPORTS_CREATE
.
Votre fournisseur de documents doit également implémenter la méthode createDocument()
. Lorsqu'un utilisateur sélectionne un répertoire au sein de votre fournisseur de documents pour enregistrer un nouveau fichier, celui-ci reçoit un appel à createDocument()
. Dans l'implémentation de la méthode createDocument()
, vous renvoyez un nouveau COLUMN_DOCUMENT_ID
pour le fichier. L'application cliente peut ensuite utiliser cet ID pour obtenir un handle pour le fichier et, au final, appeler openDocument()
pour écrire dans le nouveau fichier.
L'extrait de code suivant montre comment créer un fichier dans un fournisseur de documents.
Kotlin
override fun createDocument(documentId: String?, mimeType: String?, displayName: String?): String { val parent: File = getFileForDocId(documentId) val file: File = try { File(parent.path, displayName).apply { createNewFile() setWritable(true) setReadable(true) } } catch (e: IOException) { throw FileNotFoundException( "Failed to create document with name $displayName and documentId $documentId" ) } return getDocIdForFile(file) }
Java
@Override public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException { File parent = getFileForDocId(documentId); File file = new File(parent.getPath(), displayName); try { file.createNewFile(); file.setWritable(true); file.setReadable(true); } catch (IOException e) { throw new FileNotFoundException("Failed to create document with name " + displayName +" and documentId " + documentId); } return getDocIdForFile(file); }
Pour obtenir le code complet de l'extrait ci-dessus, téléchargez l'exemple de code StorageProvider.
Compatibilité avec les fonctionnalités de gestion des documents
En plus d'ouvrir, de créer et d'afficher des fichiers, votre fournisseur de documents peut autoriser les applications clientes à renommer, copier, déplacer et supprimer des fichiers. Pour ajouter une fonctionnalité de gestion des documents à votre fournisseur de documents, ajoutez une option à la colonne COLUMN_FLAGS
du document pour indiquer la fonctionnalité compatible. Vous devez également implémenter la méthode correspondante de la classe DocumentsProvider
.
Le tableau suivant fournit l'option COLUMN_FLAGS
et la méthode DocumentsProvider
qu'un fournisseur de documents doit implémenter pour exposer des fonctionnalités spécifiques.
Fonctionnalité | Signaler | Méthode |
---|---|---|
Supprimer un fichier |
FLAG_SUPPORTS_DELETE
|
deleteDocument()
|
Renommer un fichier |
FLAG_SUPPORTS_RENAME
|
renameDocument()
|
Copier un fichier dans un nouveau répertoire parent du fournisseur de documents |
FLAG_SUPPORTS_COPY
|
copyDocument()
|
Déplacer un fichier d'un répertoire à un autre au sein du fournisseur de documents |
FLAG_SUPPORTS_MOVE
|
moveDocument()
|
Supprimer un fichier de son répertoire parent |
FLAG_SUPPORTS_REMOVE
|
removeDocument()
|
Assurer la compatibilité avec les fichiers virtuels et d'autres formats de fichiers
Les fichiers virtuels, une fonctionnalité introduite dans Android 7.0 (niveau d'API 24), permet aux fournisseurs de documents de fournir un accès en lecture aux fichiers qui n'ont pas de représentation bytecode directe. Pour permettre à d'autres applications d'afficher des fichiers virtuels, votre fournisseur de documents doit produire une autre représentation des fichiers virtuels pouvant être ouverts.
Par exemple, imaginez qu'un fournisseur de documents contienne un format de fichier que d'autres applications ne peuvent pas ouvrir directement, essentiellement un fichier virtuel.
Lorsqu'une application cliente envoie un intent ACTION_VIEW
sans catégorie CATEGORY_OPENABLE
, les utilisateurs peuvent sélectionner ces fichiers virtuels dans le fournisseur de documents pour les afficher. Le fournisseur du document renvoie ensuite le fichier virtuel dans un format de fichier différent, mais pouvant être ouvert, comme une image.
L'application cliente peut alors ouvrir le fichier virtuel que l'utilisateur peut consulter.
Pour déclarer qu'un document du fournisseur est virtuel, vous devez ajouter l'option FLAG_VIRTUAL_DOCUMENT
au fichier renvoyé par la méthode queryDocument()
. Cette option avertit les applications clientes que le fichier n'a pas de représentation bytecode directe et qu'il ne peut pas être ouvert directement.
Si vous déclarez qu'un fichier dans votre fournisseur de documents est virtuel, nous vous recommandons vivement de le rendre disponible dans un autre type MIME tel qu'une image ou un PDF. Le fournisseur de documents déclare les autres types MIME compatibles pour l'affichage d'un fichier virtuel en remplaçant la méthode getDocumentStreamTypes()
. Lorsque les applications clientes appellent la méthode getStreamTypes(android.net.Uri, java.lang.String)
, le système appelle la méthode getDocumentStreamTypes()
du fournisseur de documents. La méthode getDocumentStreamTypes()
renvoie ensuite un tableau d'autres types MIME pris en charge par le fournisseur de documents pour le fichier.
Une fois que le client a déterminé que le fournisseur de documents peut produire le document dans un format de fichier visible, l'application cliente appelle la méthode openTypedAssetFileDescriptor()
, qui appelle en interne la méthode openTypedDocument()
du fournisseur de documents. Le fournisseur de documents renvoie le fichier à l'application cliente dans le format de fichier demandé.
L'extrait de code suivant illustre une implémentation simple des méthodes getDocumentStreamTypes()
et openTypedDocument()
.
Kotlin
var SUPPORTED_MIME_TYPES : Array<String> = arrayOf("image/png", "image/jpg") override fun openTypedDocument( documentId: String?, mimeTypeFilter: String, opts: Bundle?, signal: CancellationSignal? ): AssetFileDescriptor? { return try { // Determine which supported MIME type the client app requested. when(mimeTypeFilter) { "image/jpg" -> openJpgDocument(documentId) "image/png", "image/*", "*/*" -> openPngDocument(documentId) else -> throw IllegalArgumentException("Invalid mimeTypeFilter $mimeTypeFilter") } } catch (ex: Exception) { Log.e(TAG, ex.message) null } } override fun getDocumentStreamTypes(documentId: String, mimeTypeFilter: String): Array<String> { return when (mimeTypeFilter) { "*/*", "image/*" -> { // Return all supported MIME types if the client app // passes in '*/*' or 'image/*'. SUPPORTED_MIME_TYPES } else -> { // Filter the list of supported mime types to find a match. SUPPORTED_MIME_TYPES.filter { it == mimeTypeFilter }.toTypedArray() } } }
Java
public static String[] SUPPORTED_MIME_TYPES = {"image/png", "image/jpg"}; @Override public AssetFileDescriptor openTypedDocument(String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal) { try { // Determine which supported MIME type the client app requested. if ("image/png".equals(mimeTypeFilter) || "image/*".equals(mimeTypeFilter) || "*/*".equals(mimeTypeFilter)) { // Return the file in the specified format. return openPngDocument(documentId); } else if ("image/jpg".equals(mimeTypeFilter)) { return openJpgDocument(documentId); } else { throw new IllegalArgumentException("Invalid mimeTypeFilter " + mimeTypeFilter); } } catch (Exception ex) { Log.e(TAG, ex.getMessage()); } finally { return null; } } @Override public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) { // Return all supported MIME tyupes if the client app // passes in '*/*' or 'image/*'. if ("*/*".equals(mimeTypeFilter) || "image/*".equals(mimeTypeFilter)) { return SUPPORTED_MIME_TYPES; } ArrayList requestedMimeTypes = new ArrayList<>(); // Iterate over the list of supported mime types to find a match. for (int i=0; i < SUPPORTED_MIME_TYPES.length; i++) { if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) { requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]); } } return (String[])requestedMimeTypes.toArray(); }
Sécurité
Supposons que votre fournisseur de documents soit un service de stockage cloud protégé par mot de passe et que vous souhaitiez vous assurer que les utilisateurs sont connectés avant de commencer à partager leurs fichiers.
Que doit faire votre application si l'utilisateur n'est pas connecté ? La solution consiste à renvoyer zéro racine dans votre implémentation de queryRoots()
. Autrement dit, un curseur racine vide:
Kotlin
override fun queryRoots(projection: Array<out String>): Cursor { ... // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) { return result }
Java
public Cursor queryRoots(String[] projection) throws FileNotFoundException { ... // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) { return result; }
L'autre étape consiste à appeler getContentResolver().notifyChange()
.
Vous vous souvenez du DocumentsContract
? Nous l'utilisons pour créer cet URI. L'extrait de code suivant indique au système d'interroger les racines du fournisseur de documents chaque fois que l'état de connexion de l'utilisateur change. Si l'utilisateur n'est pas connecté, un appel à queryRoots()
renvoie un curseur vide, comme indiqué ci-dessus. Cela garantit que les documents d'un fournisseur ne sont disponibles que si l'utilisateur est connecté à celui-ci.
Kotlin
private fun onLoginButtonClick() { loginOrLogout() getContentResolver().notifyChange( DocumentsContract.buildRootsUri(AUTHORITY), null ) }
Java
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null); }
Pour obtenir des exemples de code liés à cette page, consultez les ressources suivantes:
Pour les vidéos en rapport avec cette page, consultez les ressources suivantes:
- DevBytes: Android 4.4 Storage Access Framework: Provider
- Storage Access Framework: Créer un fournisseur de documents
- Fichiers virtuels dans le framework d'accès au stockage
Pour en savoir plus, consultez les ressources suivantes:
- Créer un fournisseur de documents
- Ouvrir des fichiers à l'aide de Storage Access Framework
- Principes de base des fournisseurs de contenu