Document Provider en Android: Introducción
Siro Ramírez Losada Noticias 26/04/2019
Este artículo ha sido escrito por Abel Garcia y publicado originalmente en el blog de Solid GEAR.
Antes de la introducción del Document Provider, si necesitábamos crear un gestor de documentos, ya fuera para archivos locales, dispositivos de almacenamiento o almacenamiento en Cloud, teníamos que crear una interfaz completa con sus vistas, adaptadores, los menús de opciones, la multiselección etc. Al fin y al cabo, cada gestor de documentos tenía su propia interfaz y el usuario tenía que aprender a utilizar todas y cada una de ellas. El Battle Royale de los gestores de documentos.
Pero todo cambió con la llegada de Android KitKat (API 19) y la introducción del Storage Access Framework y con ello, el Document Provider. Esto permitió a los usuarios tener una sola interfaz para los ficheros, sin importar si son locales o de un almacenamiento en la nube. Una interfaz para unirlas a todas.
Pero, ¿qué es en realidad un Document Provider?
La clave de todo es que la interfaz la genera el sistema en vez de nuestra aplicación. Con esto, solo nos tendremos que preocupar de proveer de la información necesaria al Document Provider para que muestre en la interfaz los directorios o ficheros de nuestra aplicación. Todo esto lo facilita la API del DocumentsProvider. Sin más dilación, vamos al turrón.
Lo añadimos al Manifest
Lo primero que tendremos que hacer, será registrarlo en nuestro manifest y tendrá un aspecto tal que así:
- <provider
- android:name="com.example.MyCloudProvider"
- android:authorities="com.example.mycloudprovider"
- android:exported="true"
- android:grantUriPermissions="true"
- android:permission="android.permission.MANAGE_DOCUMENTS"
- android:enabled="@bool/isAtLeastKitKat">
Tenemos toda la información relativa a los campos que necesitamos aquí, pero vamos a comentar algunos de ellos:
- Authorities: String único que será el prefijo de todas las Uris que tengamos en nuestro document provider. Podríamos crear un String y recuperarlo desde allí, o incluso añadirlo al gradle.build de la aplicación para acceder de forma sencilla a este valor, porque vamos a utilizarlo posteriormente.
- Enabled: Si soportamos versiones previas a Kitkat, tendríamos que añadir un boolean para determinar si estamos en una versión anterior a KitKat o no, tal y cómo se ve en el código superior. Si soportamos solo versiones superiores, podríamos suprimir este campo, porque estará habilitado por defecto.
Tarea previa: Los Document Contracts
Antes de comenzar con la funcionalidad básica, vamos a recordar dos DocumentsContract que nos permitirán construir la estructura que va a tener nuestro Document Provider.
En primer lugar el Root que nos permitirá establecer los campos que tendrá nuestra raíz de documentos. Podemos añadir más columnas, que aportarán información adicional, pero lo mínimo necesario para comenzar sería lo siguiente:
- COLUMN_ROOT_ID
- COLUMN_ICON
- COLUMN_TITLE
- COLUMN_FLAGS
- COLUMN_DOCUMENT_ID
Cada raíz comienza con un document_id que será el identificador único y superior de un árbol de directorios, que puede ser una cuenta, o un tipo de almacenamiento etc y que podrá contener archivos o más directorios que se tratarán con el siguiente Contract, el Document.
Con esto, estableceremos la información para cada archivo. Lo mínimo necesario para comenzar:
- COLUMN_DOCUMENT_ID
- COLUMN_DISPLAY_NAME
- COLUMN_MIME_TYPE
- COLUMN_FLAGS
- COLUMN_SIZE
- COLUMN_LAST_MODIFIED
Los Flags de ambos contracts nos indicarán las operaciones disponibles para ese root o ese documento. Por ejemplo, si queremos permitir operaciones de creación, renombrado o borrado de documentos, tendriamos que añadir los flags correspondientes.
Comenzamos por la raíz: queryRoots
En esta función tendremos que devolver un cursor con la información relativa que queramos añadir al Root. Podemos añadir una nueva fila por cuenta, o incluso dividir las cuentas por tipo de documento que queramos mostrar: Imágenes, pdf, videos etc con los flags que consideremos oportunos para cada uno de ellos.
De esta forma poblaríamos nuestro Root, y podríamos conseguir que muestre algo similar a esto:
Mostrar los documentos en el provider: queryChildDocuments
Ahora que hemos conseguido mostrar nuestra raíz en el Provider, vamos a listar los documentos que hay en ella. Para ello usaremos la función queryChildDocuments. Se llamará a este método cada vez que seleccionemos un directorio en nuestro provider.
Esta función devolverá un MatrixCursor que tendremos que poblar con los ficheros que se encuentren en el directorio cuyo id recibimos por parámetro. Añadiremos una fila por cada fichero siguiendo el Document Contract comentado anteriormente.
Si todo ha ido bien, podremos ver nuestra lista de archivos cuando seleccionemos nuestra raíz:
Apuntando a un documento: queryDocument
Ahora que tenemos nuestra lista de ficheros y directorios, nos interesa consultar cada uno de ellos. Para ello utilizaremos queryDocument.
Esta función es similar a la anterior, pero solo para el documento específico. Es decir, devolveremos un Cursor pero solo con los metadatos del documento que queremos consultar. Debería ser lo más rápida posible (Sin peticiones de red ni nada por el estilo).
Abriendo un Documento: openDocument
Para abrir un documento, tendremos que devolver un ParcelFileDescriptor que represente al fichero cuyo Id recibimos por parámetro. Además del id, obtendremos también el modo por parámetro, ya sea lectura o escritura. Para hacerlo lo más sencillo posible, vamos a establecer solo modo lectura.
Con esto ya quedarían resueltos todos las funciones mínimas y necesarias para tener nuestro Document Provider funcional. Pero hay algunas cosas que comentar todavía.
Notificar al Document Provider de un cambio
Podemos notificar al Document Provider de un cambio añadiendo al cursor un listener. De esta forma cuando actualicemos un archivo, o creamos un documento, notificamos al listener y actualizará la vista.
Esto puede ahorrarnos costes innecesarios de sincronización tras cada operación que realicemos y se realiza mediante Uris. Estas Uris se generarán con la authority mencionada en el apartado del manifest. Podríamos crear una funcion como esta para generar estas Uris:
- private fun toNotifyUri(uri: Uri): Uri = DocumentsContract.buildDocumentUri(DOCUMENTS_PROVIDER_AUTHORITY, uri.toString())
Vamos a aplicarlo. Podríamos colocar un listener en nuestro queryChildDocuments, que se ocupa de actualizar la vista de los archivos si hay algún cambio, por ejemplo al renombrar o borrar un archivo. Antes de retornar el cursor, establecemos una uri de notificación tal que así:
- resultCursor.setNotificationUri(context.contentResolver, toNotifyUri(Uri.parse(folderId)))
De tal forma que cuando actualizamos un archivo, por ejemplo al renombrarlo, llamamos al content resolver y notificamos el cambio con la misma Uri que establecimos en el listener:
- context.contentResolver.notifyChange(toNotifyUri(Uri.parse(folderId)), null)
Si todo ha salido bien, el cambio se verá reflejado al momento y el coste de esta actualización será mínimo.
Notificación de errores
La notificación de errores se realiza mediante excepciones, lo cuál no es muy agradable de tratar, pero es lo que hay. Uno de los puntos que tiene que mejorar el Document Provider.
Cuando al realizar una operación, obtengamos un resultado no esperado, lanzamos una excepción y el Provider ya se encargará de avisar al usuario de que la operación no se completó correctamente mediante un SnackBar.
Que más funcionalidades nos ofrece el Document Provider
Además de todo lo comentado anteriormente, el document provider tiene más que ofrecer. Y es que sólo hemos comentado lo más básico, pero todavía podríamos implementar las siguientes funcionalidades:
- createDocument: Permite la creación de ficheros o directorios dentro de nuestro Document Provider
- queryRecentDocuments: Permite mostrar los últimos archivos modificados en nuestro Document Provider
- openDocumentThumbnail: Permite mostrar miniaturas de nuestros documentos
- querySearchDocuments: Permite la búsqueda de archivos en nuestra aplicación
- deleteDocument: Borrado de archivos
- renameDocument: Renombrado de archivos
- copyDocument: Copiar archivos dentro de nuestro document provider
- moveDocument: Mover archivos dentro de nuestro document provider
Pero esto lo dejaremos para la próxima.