Si vous développez une application qui fournit des services de stockage pour des fichiers (comme un service d'enregistrement dans le cloud), vous pouvez rendre vos fichiers disponibles via le Storage Access Framework (SAF) en écrivant un fournisseur de documents personnalisé. Cette page explique comment créer un fournisseur de documents personnalisé.
Pour en savoir plus sur le fonctionnement de Storage Access Framework, consultez la Présentation de Storage Access Framework
Manifest
Pour implémenter un fournisseur de documents personnalisé, ajoutez les éléments suivants au fichier fichier manifeste:
- Cible de niveau d'API 19 ou supérieur.
- Un élément
<provider>
qui déclare votre espace de stockage personnalisé un fournisseur de services agréé. -
L'attribut
android:name
défini sur le nom de votreDocumentsProvider
, qui est son nom de classe, y compris le nom du package:com.example.android.storageprovider.MyCloudProvider
. -
L'attribut
android:authority
, qui est le nom de votre package (dans cet exemple,com.example.android.storageprovider
). ainsi que le type de fournisseur de contenu (documents
). - L'attribut
android:exported
a été défini sur"true"
. Vous devez exporter votre fournisseur pour que les autres applications puissent le voir. - L'attribut
android:grantUriPermissions
défini sur"true"
Ce paramètre permet au système d'accorder l'accès à d'autres applications au contenu de votre fournisseur. Pour voir comment ces autres applications peuvent conserver leur accès au contenu de votre fournisseur, consultez Persistance autorisations. - L'autorisation
MANAGE_DOCUMENTS
Par défaut, un fournisseur est disponible à tout le monde. 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 le
android.content.action.DOCUMENTS_PROVIDER
afin que votre fournisseur apparaît 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
La
L'intent ACTION_OPEN_DOCUMENT
n'est disponible
sur les appareils équipés d'Android 4.4 ou version ultérieure.
Si vous souhaitez que votre application soit compatible avec 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. A
fournisseur de documents et ACTION_GET_CONTENT
doivent être pris en compte
s'excluent mutuellement. Si vous les prenez en charge simultanément, votre application
apparaît deux fois dans l'interface utilisateur du sélecteur système, offrant deux façons différentes d'accéder
vos données stockées. Cela peut prêter à confusion pour les utilisateurs.
Voici la méthode recommandée pour désactiver
Filtre d'intent ACTION_GET_CONTENT
pour les appareils
équipés d'Android 4.4 ou version ultérieure:
- Dans votre fichier de ressources
bool.xml
, sousres/values/
, ajoutez cette ligne:<bool name="atMostJellyBeanMR2">true</bool>
- Dans votre fichier de ressources
bool.xml
, sousres/values-v19/
, ajoutez cette ligne:<bool name="atMostJellyBeanMR2">false</bool>
- Ajoutez un
activité
alias pour désactiver l'intent
ACTION_GET_CONTENT
filtrer pour les versions 4.4 (niveau d'API 19) ou ultérieures. Par 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
En général, lorsque vous écrivez un fournisseur
de contenu personnalisé, l'une des tâches
implémentant des classes de contrat, comme décrit dans
<ph type="x-smartling-placeholder"></ph>
guide du développeur pour les fournisseurs de contenu Une classe de contrat est une classe public final
.
contenant des définitions constantes pour les URI, les noms de colonne, les types MIME et
d'autres métadonnées
concernant le fournisseur. Le SAF
fournit ces classes de contrat pour vous, vous n'avez donc pas besoin d'écrire
possède:
Par exemple, voici les colonnes que vous pouvez renvoyer dans un curseur lorsque votre fournisseur de documents est interrogé pour 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 la création d'un fournisseur de documents personnalisé consiste à sous-classer le
classe abstraite DocumentsProvider
. Vous devez au moins
implémenter les méthodes suivantes:
Ce sont les seules méthodes que vous êtes strictement tenu d'implémenter, mais il existe
sont bien plus que
vous voudrez peut-être. Voir DocumentsProvider
pour en savoir plus.
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 de 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
voudra obtenir en réponse. L'extrait crée un nouveau curseur
et y ajoute une ligne : une racine, un répertoire de premier niveau, comme
Téléchargements ou images. La plupart des fournisseurs n'ont qu'une seule racine. Vous pourriez en avoir plusieurs,
par exemple, dans le cas
de plusieurs comptes utilisateur. Dans ce cas, ajoutez simplement
la 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 (un port USB, par exemple)
appareil qui pourrait être déconnecté ou à un compte dont l'utilisateur peut se déconnecter, vous
pouvez mettre à jour l'interface utilisateur du document pour qu'elle reste synchronisée avec ces modifications à l'aide de la
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);
Répertorier les documents dans le fournisseur
Votre implémentation de
queryChildDocuments()
doit renvoyer un Cursor
qui pointe vers tous les fichiers de
dans le répertoire spécifié, à l'aide des colonnes définies
DocumentsContract.Document
Cette méthode est appelée lorsque l'utilisateur choisit votre racine dans l'interface utilisateur du sélecteur.
Cette 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
dans votre fournisseur de documents.
Cet extrait crée un nouveau curseur avec les colonnes demandées, puis ajoute des informations sur chaque enfant immédiat dans le répertoire parent au curseur. 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 les informations du 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
.
queryDocument()
renvoie les mêmes informations que celles transmises
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
DocumentsProvider.openDocumentThumbnail()
et en ajoutant la
FLAG_SUPPORTS_THUMBNAIL
aux fichiers compatibles.
L'extrait de code suivant fournit un exemple de mise en œuvre de la méthode
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 de vignettes plus que le double
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é. Les 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 que l'application cliente
demande l'accès en appelant
openFileDescriptor()
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
structures de données, envisagez de mettre en œuvre
createReliablePipe()
ou
createReliableSocketPair()
.
Ces méthodes vous permettent
de créer une paire de
Des objets ParcelFileDescriptor
, où vous pouvez en renvoyer un
et envoyer l'autre via un
ParcelFileDescriptor.AutoCloseOutputStream
ou
ParcelFileDescriptor.AutoCloseInputStream
Prendre en charge les recherches et les documents récents
Vous pouvez fournir une liste des documents récemment modifiés sous la racine de votre
fournisseur de documents en remplaçant
queryRecentDocuments()
et en renvoyant
FLAG_SUPPORTS_RECENTS
,
L'extrait de code suivant montre comment mettre en œuvre la méthode
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; }
Vous pouvez obtenir le code complet de l'extrait ci-dessus en téléchargeant le Fournisseur de stockage exemple de code.
Prendre en charge la création de documents
Vous pouvez autoriser les applications clientes à créer des fichiers dans votre fournisseur de documents.
Si une application cliente envoie un ACTION_CREATE_DOCUMENT
intent, votre fournisseur de documents peut autoriser cette application cliente à créer
de nouveaux documents
dans le fournisseur de documents.
Pour permettre la création de documents, votre racine doit disposer du
FLAG_SUPPORTS_CREATE
.
Les répertoires permettant de créer des fichiers doivent être dotés du paramètre
FLAG_DIR_SUPPORTS_CREATE
.
Votre fournisseur de documents doit également implémenter la
createDocument()
. Lorsqu'un utilisateur sélectionne un répertoire dans votre
fournisseur de documents pour enregistrer un nouveau fichier, il reçoit un appel vers
createDocument()
Au cours de l'implémentation
createDocument()
, vous renvoyez une nouvelle
COLUMN_DOCUMENT_ID
pour
. L'application cliente peut ensuite utiliser cet ID pour obtenir un handle pour le fichier.
et, en fin de compte, 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); }
Vous pouvez obtenir le code complet de l'extrait ci-dessus en téléchargeant le Fournisseur de stockage exemple de code.
Prendre en charge les fonctionnalités de gestion des documents
En plus d'ouvrir, de créer et d'afficher des fichiers, votre fournisseur de documents
peut également permettre aux applications clientes de renommer, copier, déplacer et supprimer
. Pour ajouter la fonctionnalité de gestion des documents
votre fournisseur de document, ajoutez un indicateur à la liste
COLUMN_FLAGS
colonne
pour indiquer la fonctionnalité prise en charge. Vous devez également implémenter
la méthode correspondante de l'élément DocumentsProvider
.
.
Le tableau suivant fournit les
COLUMN_FLAGS
indicateur
et la méthode DocumentsProvider
qu'un document
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 au sein 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()
|
Prendre en charge les fichiers virtuels et d'autres formats de fichiers
Fichiers virtuels, une fonctionnalité introduite dans Android 7.0 (niveau d'API 24) qui permet aux fournisseurs de documents pour permettre l'accès en lecture aux fichiers n'ayant pas de représentation directe du bytecode. Pour permettre à d'autres applications d'afficher des fichiers virtuels, procédez comme suit : votre fournisseur de documents doit créer un autre fichier pouvant être ouvert pour les fichiers virtuels.
Par exemple, imaginez qu'un fournisseur de documents contient un fichier
que d'autres applications ne peuvent pas
ouvrir directement, c'est-à-dire un fichier virtuel.
Lorsqu'une application cliente envoie un intent ACTION_VIEW
sans la catégorie CATEGORY_OPENABLE
,
les utilisateurs peuvent alors
sélectionner ces fichiers virtuels dans le fournisseur de documents
pour consultation. Le fournisseur de documents renvoie ensuite le fichier virtuel
dans un format de fichier différent, mais
ouvert, comme une image.
L'application cliente peut alors ouvrir le fichier virtuel pour que l'utilisateur puisse le consulter.
Pour déclarer qu'un document du fournisseur est virtuel, vous devez ajouter la classe
FLAG_VIRTUAL_DOCUMENT
au fichier renvoyé par
queryDocument()
. Cet indicateur avertit les applications clientes que le fichier n'a pas d'accès direct
représentation bytecode et ne peut pas être ouverte directement.
Si vous déclarez qu'un fichier
dans votre fournisseur de documents est virtuel,
nous vous recommandons vivement de le rendre disponible
le type MIME, comme une image ou un PDF. Le fournisseur du document
déclare les autres types MIME qu'il
permet d'afficher un fichier virtuel en remplaçant
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
getDocumentStreamTypes()
renvoie ensuite un tableau d'autres types MIME que le
le fournisseur de documents
prend en charge pour le fichier.
Une fois que le client a déterminé
que le fournisseur puisse créer le document dans un fichier consultable
l'application cliente appelle
openTypedAssetFileDescriptor()
, qui appelle en interne la méthode du fournisseur de documents
openTypedDocument()
. 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 de la méthode
getDocumentStreamTypes()
et
openTypedDocument()
méthodes.
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 vous voulez 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()
. C'est-à-dire 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 de DocumentsContract
? Nous l'utilisons pour créer
cette URI. L'extrait de code suivant indique au système d'interroger les racines de votre
le 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 une
curseur vide, comme indiqué ci-dessus. Cela garantit que les documents d'un fournisseur ne sont
disponible si l'utilisateur est connecté au fournisseur.
Kotlin
private fun onLoginButtonClick() { loginOrLogout() getContentResolver().notifyChange( DocumentsContract.buildRootsUri(AUTHORITY), null ) }
Java
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null); }
Pour obtenir un exemple de code associé à cette page, consultez:
- <ph type="x-smartling-placeholder"></ph> Fournisseur de stockage
- <ph type="x-smartling-placeholder"></ph> StorageClient
Pour les vidéos en rapport avec cette page, consultez les ressources suivantes:
- <ph type="x-smartling-placeholder"></ph> DevBytes: Framework d'accès au stockage Android 4.4: fournisseur
- <ph type="x-smartling-placeholder"></ph> Framework d'accès au stockage: créer un DocumentProvider
- <ph type="x-smartling-placeholder"></ph> Fichiers virtuels dans le framework d'accès au stockage
Pour en savoir plus, consultez les ressources suivantes:
- <ph type="x-smartling-placeholder"></ph> Créer un DocumentProvider
- <ph type="x-smartling-placeholder"></ph> Ouvrir des fichiers à l'aide de Storage Access Framework
- <ph type="x-smartling-placeholder"></ph> Principes de base des fournisseurs de contenu