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 subclaseDocumentsProvider
, 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:
- En el 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
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:
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 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<>(); // 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 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:
- DevBytes: framework de acceso al almacenamiento de Android 4.4: proveedor
- Framework de acceso al almacenamiento: creación de un DocumentsProvider
- Archivos virtuales en el framework de acceso al almacenamiento
Para obtener información adicional relacionada, consulta:
- Cómo crear un DocumentsProvider
- Cómo abrir archivos con el framework de acceso al almacenamiento
- Conceptos básicos sobre el proveedor de contenido