Créer un fournisseur de documents personnalisé

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-classe DocumentsProvider, 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:

  1. Dans votre fichier de ressources bool.xml, sous res/values/, ajoutez la ligne suivante:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. Dans votre fichier de ressources bool.xml, sous res/values-v19/, ajoutez la ligne suivante:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 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:

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&lt;&gt;();

    // Iterate over the list of supported mime types to find a match.
    for (int i=0; i &lt; 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:

Pour en savoir plus, consultez les ressources suivantes: