Cómo crear un proveedor de documentos personalizado

Si estás desarrollando una app que ofrece 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 framework de acceso al almacenamiento, consulta la Descripción general del framework de acceso al almacenamiento.

Manifest

Para implementar un proveedor de documentos personalizado, agrega lo siguiente al directorio manifiesto:

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

    com.example.android.storageprovider.MyCloudProvider.

  • El atributo android:authority que es el nombre del paquete (en este ejemplo, com.example.android.storageprovider) además del 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 establecido en "true" Este parámetro de configuración permite que el sistema otorgue acceso a otras apps al contenido de tu proveedor. Para analizar cómo estas otras apps pueden mantener su acceso al contenido de tu proveedor, consulta Conservación permisos.
  • 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 incluya las android.content.action.DOCUMENTS_PROVIDER, para que tu proveedor aparece en el selector cuando el sistema busca 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 El intent ACTION_OPEN_DOCUMENT solo está disponible en dispositivos con Android 4.4 y versiones posteriores. Si quieres que tu aplicación sea compatible con ACTION_GET_CONTENT para admitir dispositivos con Android 4.3 y versiones anteriores, deberías inhabilita el filtro de intents ACTION_GET_CONTENT en tu manifiesto para dispositivos con Android 4.4 o versiones posteriores. R como proveedor de documentos y ACTION_GET_CONTENT mutuamente excluyentes. Si admites ambos a la vez, tu app aparece dos veces en la IU del selector del sistema, lo que ofrece dos formas diferentes de acceder tus datos almacenados. Esto es confuso para los usuarios.

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

  1. En el archivo de recursos bool.xml en res/values/, agrega esta línea:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. En el archivo de recursos bool.xml en res/values-v19/, agrega esta línea:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Agrega un actividad alias para inhabilitar el intent ACTION_GET_CONTENT filtro para las versiones 4.4 (nivel de API 19) y 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 es la implementación de clases Contract, como se describe en el Proveedores de contenido para desarrolladores. Una clase de contratos es una clase public final. que contiene definiciones de constantes para URI, nombres de columnas, tipos de MIME y otros metadatos que pertenecen al proveedor. El SAF te brinda estas clases de contrato, por lo que no necesitas escribir tu propio:

Por ejemplo, estas son las columnas que podrías devolver en un cursor cuando se consulta al proveedor de 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 para escribir un proveedor de documentos personalizado es crear una subclase de la clase abstracta DocumentsProvider. Como mínimo, debes implementa los siguientes métodos:

Estos son los únicos métodos que debes implementar estrictamente, pero son muchos más que te recomendamos. Ver DocumentsProvider para conocer los detalles.

Cómo definir una raíz

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

En el siguiente fragmento, el parámetro projection representa la campos específicos que el llamador 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 uno, por ejemplo, en el caso de varias cuentas de usuario. En ese caso, simplemente agrega 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 puerto USB dispositivo que podría estar desconectado o una cuenta desde la que el usuario pueda salir, puede actualizar la IU del documento para que se mantenga sincronizado con esos cambios usando el 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 de el directorio especificado, con 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, luego agrega información sobre cada elemento secundario inmediato en el directorio principal al cursor. 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 con columnas definidas en DocumentsContract.Document.

El 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 mediante anulando el DocumentsProvider.openDocumentThumbnail() y agregar el FLAG_SUPPORTS_THUMBNAIL marca a los archivos admitidos. El siguiente fragmento de código proporciona un ejemplo de cómo implementar la 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 devolver imágenes en miniatura con un tamaño superior al doble el tamaño especificado por el parámetro sizeHint

Cómo abrir un documento

Debes implementar openDocument() para que se muestre un ParcelFileDescriptor que represente el archivo especificado. Otras apps pueden usar la ParcelFileDescriptor que se devuelve 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 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 las estructuras de datos, considera implementar el createReliablePipe() o createReliableSocketPair(). Esos métodos te permiten crear un par de ParcelFileDescriptor, en los que puedes mostrar uno y envía al otro a través de un ParcelFileDescriptor.AutoCloseOutputStream o ParcelFileDescriptor.AutoCloseInputStream

Cómo admitir documentos recientes y buscar

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

Puede obtener el código completo del fragmento anterior si descarga el StorageProvider. muestra de código fuente.

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 ACTION_CREATE_DOCUMENT intent, 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 FLAG_SUPPORTS_CREATE. Los directorios que permiten crear nuevos archivos en ellos deben tener la etiqueta FLAG_DIR_SUPPORTS_CREATE marca.

Tu proveedor de documentos también debe implementar el createDocument(). Cuando un usuario selecciona un directorio dentro de para guardar un archivo nuevo, el proveedor de documentos recibe una llamada para createDocument() Dentro de la implementación createDocument(), se devuelve un nuevo método COLUMN_DOCUMENT_ID por el período . La app cliente puede usar ese ID para obtener un handle para el archivo y, en última instancia, llamar openDocument() para escribir en el archivo nuevo.

El siguiente fragmento de código demuestra cómo crear un nuevo archivo dentro 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);
}

Puede obtener el código completo del fragmento anterior si descarga el StorageProvider. muestra de código fuente.

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

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

En la siguiente tabla, se proporciona la marca COLUMN_FLAGS y DocumentsProvider que se documenta el proveedor de servicios 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()
Copia un archivo en un nuevo directorio superior 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()
Cómo quitar un archivo de su directorio superior FLAG_SUPPORTS_REMOVE removeDocument()

Cómo admitir archivos virtuales y alternar formatos de archivo

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

Por ejemplo, imagina que un proveedor de documentos contiene un 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 para visualizar. Luego, el proveedor de documentos devuelve 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 el FLAG_VIRTUAL_DOCUMENT marca en el archivo que devuelve el queryDocument() . Esta marca alerta a las aplicaciones cliente que el archivo no tiene una dirección de 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 habilites en otro Es un tipo de MIME, como una imagen o un PDF. El proveedor del documento declara los tipos de MIME alternativos que admite la visualización de un archivo virtual anulando el getDocumentStreamTypes() . Cuando las apps cliente llaman al getStreamTypes(android.net.Uri, java.lang.String) método, el sistema llama a la getDocumentStreamTypes() método del proveedor de documentos. El getDocumentStreamTypes() muestra un array de tipos de MIME alternativos que la que el proveedor de documentos admite el archivo.

Después de que el cliente determina que el proveedor de documentos pueda mostrar el documento en un archivo visible de registro, la aplicación cliente llama al openTypedAssetFileDescriptor() que llama internamente al estado del proveedor openTypedDocument() . El proveedor de documentos le devuelve el archivo a la app cliente en el formato de archivo solicitado.

En el siguiente fragmento de código, se demuestra una implementación simple de la 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 a sus cuentas antes de compartir sus archivos. ¿Qué debe hacer tu app si el usuario no ha accedido? La solución es devolver cero 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? La utilizamos para que este URI. El siguiente fragmento le indica al sistema que consulte las raíces de tu proveedor de documentos cada vez que cambia el estado de acceso del usuario. Si el usuario no es accediste, una llamada a queryRoots() devuelve un cursor vacío, como se muestra más arriba. Esto garantiza que los documentos de un proveedor solo disponible 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: