Si estás desarrollando una app que ofrece servicios de almacenamiento para archivos (como un servicio de almacenamiento en la nube), puedes crear archivos disponibles a través del marco de trabajo 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 Descripción general del marco de trabajo de acceso al almacenamiento.
Manifiesto
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 subclaseDocumentsProvider
, 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
), 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
configurado como"true"
. Esta configuración permite que el sistema otorgue a otras aplicaciones acceso al contenido de tu proveedor. Si quieres ver un debate sobre cómo persistir en la concesión de un permiso para un documento en particular, 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 quieres que tu aplicación sea compatible con ACTION_GET_CONTENT
para 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 a ambos de forma simultánea, tu app aparece dos veces en la IU del selector del sistema, lo que ofrece 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 los dispositivos con Android 4.4 o versiones posteriores:
- En tu archivo de recursos
bool.xml
enres/values/
, agrega esta línea:<bool name="atMostJellyBeanMR2">true</bool>
- En el archivo de recursos
bool.xml
enres/values-v19/
, agrega esta línea:<bool name="atMostJellyBeanMR2">false</bool>
- Agrega un alias de actividad para inhabilitar el filtro de intents
ACTION_GET_CONTENT
para las versiones 4.4 (API nivel 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 implementar clases de contrato, como se describe en la guía para desarrolladores Proveedores de contenido. Una clase de contrato es una clase public final
que contiene definiciones constantes para los URI, los nombres de columna, los tipos de MIME y otros metadatos que pertenecen al proveedor. El SAF te proporciona estas clases de contrato, por lo que no necesitas escribir el tuyo propio:
Por ejemplo, estas son las columnas que puedes mostrar en un cursor cuando se consulta a tu proveedor de documentos por los 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:
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
Cómo crear una subclase de DocumentsProvider
El siguiente paso para escribir un proveedor de documentos personalizado es subclasificar la clase abstracta DocumentsProvider
. Como mínimo, debes implementar los siguientes métodos:
Estos son los únicos métodos que debes implementar estrictamente, pero es posible que desees implementar muchos más. Para obtener más información, consulta DocumentsProvider
.
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 cual 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 del 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 del documento especificado por COLUMN_DOCUMENT_ID
.
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 principal. 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 admitidos.
En el siguiente fragmento de código, se ofrece 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 aplicaciones pueden usar el ParcelFileDescriptor
mostrado 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 procesa estructuras de datos complicadas, considera implementar los métodos createReliablePipe()
o createReliableSocketPair()
.
Estos métodos te permiten crear un par de objetos ParcelFileDescriptor
, donde puedes devolver 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 recientemente modificados bajo 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 .
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 el ejemplo 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 le puede permitir crear 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 con el objetivo de guardar un archivo nuevo, ese proveedor recibe una llamada para 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, finalmente, 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 el ejemplo 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, transfieran y borren archivos. Para agregar la funcionalidad de administración de documentos a tu proveedor, agrega una marca a la columna COLUMN_FLAGS
del documento para indicar la funcionalidad admitida. 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 transferir un archivo de un directorio a otro dentro del proveedor de documentos |
FLAG_SUPPORTS_MOVE
|
moveDocument()
|
Cómo quitar un archivo de su directorio principal |
FLAG_SUPPORTS_REMOVE
|
removeDocument()
|
Cómo admitir archivos virtuales y alternar formatos de archivo
Archivos virtuales, una función presentada en Android 7.0 (API nivel 24), permite que los proveedores de documentos proporcionen el acceso de visualización a archivos que no tienen una representación directa de código de bytes. Para permitir que otras apps vean archivos virtuales, tu proveedor de documentos debe producir una representación alternativa de archivos que se pueda abrir para los archivos virtuales.
Por ejemplo, imagina que un proveedor de documentos contiene un formato de archivo que otras aplicaciones no pueden abrir de forma directa, 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 de que el archivo no tiene una representación directa de código de bytes y no se puede abrir directamente.
Si declaras que un archivo de tu proveedor de documentos es virtual, se recomienda que lo tengas disponible en otro tipo de MIME, como una imagen o un PDF. El proveedor del 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 arreglo de tipos de MIME alternativos que el proveedor de documentos admite para el archivo.
Cuando 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 devuelve 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<>(); // Iterate over the list of supported mime types to find a match. for (int i=0; i < SUPPORTED_MIME_TYPES.length; i++) { if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) { requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]); } } return (String[])requestedMimeTypes.toArray(); }
Seguridad
Supongamos que tu proveedor de documentos es un servicio de almacenamiento en la nube protegido por contraseña y deseas 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 estamos usando para crear este URI. En el siguiente fragmento, se le 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()
muestra un cursor vacío, como se indica 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:
- DevBytes: marco de trabajo de acceso al almacenamiento de Android 4.4: proveedor
- Marco de trabajo de acceso al almacenamiento: creación de un DocumentsProvider
- Archivos virtuales en el marco de trabajo de acceso al almacenamiento
Para obtener información adicional relacionada, consulta: