Cómo crear un proveedor de documentos personalizado

Si estás desarrollando una app que proporciona servicios de almacenamiento para archivos (como un servicio de almacenamiento en la nube), puedes hacer que tus archivos estén disponibles a través del framework de acceso al almacenamiento (SAF) escribiendo un proveedor de documentos personalizado. En esta página, se describe cómo crear un proveedor de documentos personalizado.

Para obtener más información sobre cómo funciona el marco de trabajo de acceso al almacenamiento, consulta la Descripción general del marco de trabajo de acceso al almacenamiento.

Manifest

Para implementar un proveedor de documentos personalizado, agrega lo siguiente al manifiesto de tu aplicación:

  • Un objetivo de API nivel 19 o posterior.
  • Un elemento <provider> que declare tu proveedor de almacenamiento personalizado.
  • El atributo android:name configurado con el nombre de tu subclase DocumentsProvider, que es su nombre de clase, incluido el nombre del paquete:

    com.example.android.storageprovider.MyCloudProvider.

  • El atributo android:authority, que es el nombre de tu paquete (en este ejemplo, com.example.android.storageprovider), más el tipo de proveedor de contenido (documents).
  • El atributo android:exported configurado como "true". Debes exportar tu proveedor para que otras aplicaciones puedan verlo.
  • El atributo android:grantUriPermissions configurado como "true". Esta configuración permite que el sistema otorgue a otras apps acceso al contenido de tu proveedor. Para obtener un análisis sobre cómo estas otras apps pueden conservar su acceso al contenido desde tu proveedor, consulta Permisos persistentes.
  • El permiso MANAGE_DOCUMENTS De forma predeterminada, hay un proveedor disponible para todos. Agregar este permiso restringe tu proveedor al sistema. Esta restricción es importante para la seguridad.
  • Un filtro de intents que incluye la acción android.content.action.DOCUMENTS_PROVIDER, de modo que tu proveedor aparezca en el selector cuando el sistema busque proveedores.

Aquí hay extractos de un manifiesto de ejemplo que incluye un proveedor:

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

Dispositivos compatibles con Android 4.3 y versiones anteriores

El intent ACTION_OPEN_DOCUMENT solo está disponible en dispositivos con Android 4.4 y versiones posteriores. Si deseas que tu aplicación admita ACTION_GET_CONTENT para admitir dispositivos con Android 4.3 y versiones anteriores, debes inhabilitar el filtro de intents ACTION_GET_CONTENT en tu manifiesto para dispositivos con Android 4.4 o versiones posteriores. Un proveedor de documentos y ACTION_GET_CONTENT deben considerarse mutuamente excluyentes. Si admites ambos de forma simultánea, tu app aparecerá dos veces en la IU del selector del sistema y ofrecerá dos formas diferentes de acceder a tus datos almacenados. Esto es confuso para los usuarios.

Esta es la forma recomendada de inhabilitar el filtro de intents ACTION_GET_CONTENT para dispositivos que ejecutan Android 4.4 o versiones posteriores:

  1. En tu archivo de recursos bool.xml, en res/values/, agrega esta línea:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. En tu archivo de recursos bool.xml, en res/values-v19/, agrega esta línea:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Agrega un alias de actividad para inhabilitar el filtro de intents ACTION_GET_CONTENT para las versiones 4.4 (nivel de API 19) y versiones posteriores. Por ejemplo:
    <!-- 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>
    

Contratos

Por lo general, cuando escribes un proveedor de contenido personalizado, una de las tareas implementa clases de contrato, como se describe en la Guía para desarrolladores sobre proveedores de contenido. Una clase de contratos es una clase public final que contiene definiciones constantes para los URI, los nombres de columnas, los tipos de MIME y otros metadatos que pertenecen al proveedor. El SAF proporciona estas clases de contrato por ti, por lo que no necesitas escribir el tuyo:

Por ejemplo, estas son las columnas que puedes mostrar en un cursor cuando se consulta a tu proveedor de documentos por documentos o la raíz:

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,};

Tu cursor para la raíz debe incluir las siguientes columnas obligatorias:

El cursor para los documentos debe incluir las siguientes columnas obligatorias:

Cómo crear una subclase de DocumentsProvider

El siguiente paso en la escritura de un proveedor de documentos personalizado es crear una subclase de la clase abstracta DocumentsProvider. Como mínimo, debes implementar los siguientes métodos:

Estos son los únicos métodos que debes implementar estrictamente, pero hay muchos más que podrías considerar. Consulta DocumentsProvider para obtener más información.

Cómo definir una raíz

Tu implementación de queryRoots() debe mostrar un Cursor que apunte a todos los directorios raíz de tu proveedor de documentos usando las columnas definidas en DocumentsContract.Root.

En el siguiente fragmento, el parámetro projection representa los campos específicos que el emisor desea recuperar. El fragmento crea un nuevo cursor y le agrega una fila: una raíz, un directorio de nivel superior, como Descargas o Imágenes. La mayoría de los proveedores solo tienen una raíz. Es posible que tengas más de una, por ejemplo, en el caso de varias cuentas de usuario. En ese caso, solo agrega una segunda fila al cursor.

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 tu proveedor de documentos se conecta a un conjunto dinámico de raíces (por ejemplo, a un dispositivo USB que podría estar desconectado o a una cuenta desde la que el usuario puede salir), puedes actualizar la IU del documento para mantenerte sincronizado con esos cambios usando el método ContentResolver.notifyChange(), como se muestra en el siguiente fragmento de código.

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);

Cómo enumerar los documentos en el proveedor

Tu implementación de queryChildDocuments() debe mostrar un Cursor que apunte a todos los archivos en el directorio especificado usando las columnas definidas en DocumentsContract.Document.

Se llama a este método cuando el usuario elige tu raíz en la IU del selector. El método recupera los elementos secundarios del ID de documento especificado por COLUMN_DOCUMENT_ID. Luego, el sistema llama a este método cada vez que el usuario selecciona un subdirectorio dentro de tu proveedor de documentos.

Este fragmento crea un nuevo cursor con las columnas solicitadas y, luego, agrega al cursor información sobre cada elemento secundario inmediato del directorio superior. Un elemento secundario puede ser una imagen, otro directorio o cualquier archivo:

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;
}

Cómo obtener información del documento

Tu implementación de queryDocument() debe mostrar un Cursor que apunte al archivo especificado, usando las columnas definidas en DocumentsContract.Document.

El método queryDocument() muestra la misma información que se pasó en queryChildDocuments(), pero para un archivo específico:

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;
}

Tu proveedor de documentos también puede proporcionar miniaturas para un documento anulando el método DocumentsProvider.openDocumentThumbnail() y agregando la marca FLAG_SUPPORTS_THUMBNAIL a los archivos compatibles. En el siguiente fragmento de código, se proporciona un ejemplo de cómo implementar 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);
}

Precaución: Un proveedor de documentos no debe mostrar imágenes en miniatura que tengan más del doble del tamaño especificado por el parámetro sizeHint.

Cómo abrir un documento

Debes implementar openDocument() para mostrar un ParcelFileDescriptor que represente el archivo especificado. Otras apps pueden usar el ParcelFileDescriptor que se muestra para transmitir datos. El sistema llama a este método después de que el usuario selecciona un archivo, y la app cliente solicita acceso a él llamando a openFileDescriptor(). Por ejemplo:

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 tu proveedor de documentos transmite archivos o controla estructuras de datos complicadas, considera implementar los métodos createReliablePipe() o createReliableSocketPair(). Esos métodos te permiten crear un par de objetos ParcelFileDescriptor, donde puedes mostrar uno y enviar el otro a través de ParcelFileDescriptor.AutoCloseOutputStream o ParcelFileDescriptor.AutoCloseInputStream.

Cómo admitir documentos recientes y buscar

Puedes proporcionar una lista de documentos modificados recientemente en la raíz de tu proveedor de documentos anulando el método queryRecentDocuments() y mostrando FLAG_SUPPORTS_RECENTS. En el siguiente fragmento de código, se muestra un ejemplo de cómo implementar los métodos 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;
}

Puedes obtener el código completo del fragmento anterior si descargas la muestra de código de StorageProvider.

Cómo admitir la creación de documentos

Puedes permitir que las apps cliente creen archivos dentro de tu proveedor de documentos. Si una app cliente envía un intent ACTION_CREATE_DOCUMENT, tu proveedor de documentos puede permitir que esa app cliente cree documentos nuevos dentro del proveedor de documentos.

Para admitir la creación de documentos, tu raíz debe tener la marca FLAG_SUPPORTS_CREATE. Los directorios que permiten crear archivos nuevos dentro de ellos deben tener la marca FLAG_DIR_SUPPORTS_CREATE.

Tu proveedor de documentos también debe implementar el método createDocument(). Cuando un usuario selecciona un directorio dentro de tu proveedor de documentos para guardar un archivo nuevo, este recibe una llamada a createDocument(). Dentro de la implementación del método createDocument(), muestras un COLUMN_DOCUMENT_ID nuevo para el archivo. La app cliente puede usar ese ID para obtener un controlador para el archivo y, en última instancia, llamar a openDocument() para escribir en el archivo nuevo.

En el siguiente fragmento de código, se muestra cómo crear un archivo nuevo dentro de un proveedor de documentos.

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);
}

Puedes obtener el código completo del fragmento anterior si descargas la muestra de código de StorageProvider.

Cómo admitir las funciones de administración de documentos

Además de abrir, crear y ver archivos, tu proveedor de documentos también puede permitir que las apps cliente cambien el nombre, copien, muevan y borren archivos. Para agregar la funcionalidad de administración de documentos a tu proveedor de documentos, agrega una marca a la columna COLUMN_FLAGS del documento para indicar la funcionalidad compatible. También debes implementar el método correspondiente de la clase DocumentsProvider.

En la siguiente tabla, se proporciona la marca COLUMN_FLAGS y el método DocumentsProvider que un proveedor de documentos debe implementar para exponer funciones específicas.

Función Marca Método
Cómo borrar un archivo FLAG_SUPPORTS_DELETE deleteDocument()
Cómo cambiar el nombre de un archivo FLAG_SUPPORTS_RENAME renameDocument()
Cómo copiar un archivo en un nuevo directorio principal dentro del proveedor de documentos FLAG_SUPPORTS_COPY copyDocument()
Cómo mover un archivo de un directorio a otro dentro del proveedor de documentos FLAG_SUPPORTS_MOVE moveDocument()
Quita un archivo de su directorio superior FLAG_SUPPORTS_REMOVE removeDocument()

Cómo admitir archivos virtuales y alternar formatos de archivo

Los archivos virtuales, una función que se introdujo en Android 7.0 (API nivel 24), permite a los proveedores de documentos proporcionar acceso de lectura a los archivos que no tienen una representación directa en código de bytes. Para permitir que otras apps vean archivos virtuales, tu proveedor de documentos debe producir una representación alternativa de archivo que se pueda abrir para los archivos virtuales.

Por ejemplo, imagina que un proveedor de documentos contiene un formato de archivo que otras apps no pueden abrir directamente; básicamente, un archivo virtual. Cuando una app cliente envía un intent ACTION_VIEW sin la categoría CATEGORY_OPENABLE, los usuarios pueden seleccionar estos archivos virtuales dentro del proveedor de documentos para verlos. Luego, el proveedor de documentos muestra el archivo virtual en un formato de archivo diferente, pero que se puede abrir, como una imagen. La app cliente puede abrir el archivo virtual para que el usuario lo vea.

Para declarar que un documento del proveedor es virtual, debes agregar la marca FLAG_VIRTUAL_DOCUMENT al archivo que muestra el método queryDocument(). Esta marca alerta a las apps cliente que el archivo no tiene una representación directa en código de bytes y no se puede abrir directamente.

Si declaras que un archivo de tu proveedor de documentos es virtual, te recomendamos que lo tengas disponible en otro tipo de MIME, como una imagen o un PDF. El proveedor de documentos declara los tipos de MIME alternativos que admite para ver un archivo virtual anulando el método getDocumentStreamTypes(). Cuando las apps cliente llaman al método getStreamTypes(android.net.Uri, java.lang.String), el sistema llama al método getDocumentStreamTypes() del proveedor de documentos. Luego, el método getDocumentStreamTypes() muestra un array de tipos de MIME alternativos que el proveedor de documentos admite para el archivo.

Después de que el cliente determina que el proveedor de documentos puede mostrar el documento en un formato de archivo visible, la app cliente llama al método openTypedAssetFileDescriptor(), que internamente llama al método openTypedDocument() del proveedor de documentos. El proveedor de documentos le muestra el archivo a la app cliente en el formato de archivo solicitado.

En el siguiente fragmento de código, se muestra una implementación simple de los métodos getDocumentStreamTypes() y 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();
}

Seguridad

Supongamos que tu proveedor de documentos es un servicio de almacenamiento en la nube protegido con contraseña y quieres asegurarte de que los usuarios accedan antes de comenzar a compartir sus archivos. ¿Qué debe hacer tu app si el usuario no ha accedido? La solución es no mostrar raíces en tu implementación de queryRoots(). Es decir, un cursor raíz vacío:

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;
}

El otro paso es llamar a getContentResolver().notifyChange(). ¿Recuerdas el DocumentsContract? Lo usamos para crear este URI. En el siguiente fragmento, se indica al sistema que consulte las raíces de tu proveedor de documentos cada vez que cambie el estado de acceso del usuario. Si el usuario no accedió, una llamada a queryRoots() mostrará un cursor vacío, como se indica más arriba. Esto garantiza que los documentos de un proveedor solo estén disponibles si el usuario accedió al proveedor.

Kotlin

private fun onLoginButtonClick() {
    loginOrLogout()
    getContentResolver().notifyChange(
        DocumentsContract.buildRootsUri(AUTHORITY),
        null
    )
}

Java

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}

Para ver el código de ejemplo relacionado con esta página, consulta:

Para ver videos relacionados con esta página, consulta:

Para obtener información adicional relacionada, consulta: