Créer un fournisseur de documents personnalisé

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

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

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&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 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:

Pour les vidéos en rapport avec cette page, consultez les ressources suivantes:

Pour en savoir plus, consultez les ressources suivantes: