Un proveedor de contenido administra el acceso a un repositorio central de datos. Un proveedor se implementa como una o más clases en una aplicación de Android, junto con elementos en el archivo de manifiesto. Una de tus clases implementa una subclase ContentProvider
, que es la interfaz entre tu proveedor y otras aplicaciones. Si bien los proveedores de contenido permiten que los datos estén disponibles para otras aplicaciones, puedes tener actividades en tu aplicación que le permitan al usuario consultar y modificar los datos administrados por tu proveedor.
El resto de este tema es una lista básica de pasos para crear un proveedor de contenido y una lista de API.
Antes de que empieces a crear
Antes de que empieces a crear un proveedor, haz lo siguiente:
-
Decide si necesitas un proveedor de contenido. Debes crear un proveedor de contenido si quieres proporcionar una o más de las siguientes funciones:
- Te recomendamos que ofrezcas datos o archivos complejos para otras aplicaciones.
- Te recomendamos que permitas que los usuarios copien datos complejos de tu aplicación en otras aplicaciones.
- Te recomendamos que proporciones sugerencias personalizadas usando el marco de trabajo de búsqueda.
- Quieres exponer los datos de tu aplicación a widgets.
- Quieres implementar la clase
AbstractThreadedSyncAdapter
,CursorAdapter
oCursorLoader
.
No necesitas un proveedor para utilizar bases de datos u otros tipos de almacenamiento persistente si el uso es totalmente dentro de tu propia aplicación y no necesitas ninguna de las funciones mencionadas anteriormente. En su lugar, puedes utilizar uno de los sistemas de almacenamiento descritos en la página Cómo guardar datos de apps.
- Si todavía no lo hiciste, lee el tema Conceptos básicos sobre los proveedores de contenido para obtener más información acerca de cómo funcionan.
A continuación, sigue estos pasos para crear un proveedor:
-
Diseña el almacenamiento sin formato para tus datos. Un proveedor de contenido ofrece datos de dos maneras:
- Datos de archivos
- Datos que normalmente se presentan en archivos, como fotos, audio o videos. Guarda los archivos en el espacio privado de tu aplicación. En respuesta a una solicitud de un archivo por parte de otra aplicación, tu proveedor puede ofrecer un controlador para el archivo.
- Datos "estructurados"
- Datos que normalmente se encuentran en una base de datos, una matriz o una estructura similar. Guarda los datos en un formato compatible con tablas de filas y columnas. Una fila representa una entidad, como una persona o un elemento en el inventario. Una columna representa datos para la entidad, como el nombre de la persona o el precio de un artículo. Una forma común de guardar este tipo de datos es en una base de datos SQLite, pero puedes usar cualquier tipo de almacenamiento persistente. Para obtener más información acerca de los tipos de almacenamiento disponibles en el sistema Android, consulta la sección Cómo diseñar almacenamiento de datos.
-
Define una implementación concreta de la clase
ContentProvider
y sus métodos necesarios. Esta clase es la interfaz entre tus datos y el resto del sistema Android. Para obtener más información acerca de esta clase, consulta la sección Cómo implementar la clase ContentProvider. - Define la string de autoridad del proveedor, sus URI de contenido y nombres de columnas. Si quieres que la aplicación del proveedor aborde intents, define acciones de intent, datos extra e indicadores. Define también los permisos que requerirás para las aplicaciones que quieran acceder a tus datos. Considera definir todos estos valores como constantes en una clase Contract independiente; posteriormente, podrás exponer esa clase a otros desarrolladores. Para obtener más información acerca de los URI de contenido, consulta la sección Cómo diseñar URI de contenido. Para obtener más información acerca de las intents, consulta la sección Intents y acceso a datos.
-
Agrega otras piezas opcionales, como datos de muestra o una implementación de
AbstractThreadedSyncAdapter
que pueda sincronizar datos entre el proveedor y datos basados en la nube.
Cómo diseñar almacenamiento de datos
Un proveedor de contenido es la interfaz para los datos guardados en un formato estructurado. Antes de crear la interfaz, debes decidir cómo guardar los datos. Puedes guardarlos en cualquier formato que quieras y luego diseñar la interfaz para leerlos y escribirlos, según sea necesario.
Estas son algunas de las tecnologías de almacenamiento de datos disponibles en Android:
- Si trabajas con datos estructurados, considera usar una base de datos relacional, como SQLite, o un almacén de datos de valores clave no relacional, como LevelDB. Si trabajas con datos no estructurados, como contenido de audio, imágenes o video, considera la posibilidad de almacenar los datos como archivos. Puedes mezclar y combinar varios tipos diferentes de almacenamiento y exponerlos utilizando un único proveedor de contenido si es necesario.
-
El sistema Android puede interactuar con la biblioteca de persistencia de Room, que proporciona acceso a la API de base de datos de SQLite que los propios proveedores de Android utilizan para almacenar datos orientados a tablas. Para crear una base de datos utilizando esta biblioteca, instala una subclase de
RoomDatabase
, como se describe en la guía de la biblioteca de persistencia de Room.Recuerda que no necesitas usar una base de datos para implementar tu repositorio. Un proveedor aparece externamente como un conjunto de tablas, similar a una base de datos relacional, pero esto no es un requisito para la implementación interna del proveedor.
- Para guardar datos de archivo, Android tiene una variedad de API orientadas a archivos. Para obtener más información acerca del almacenamiento de archivos, lee el tema Almacenamiento de datos. Si estás diseñando un proveedor que ofrece datos relacionados con contenido multimedia, como música o videos, puedes tener un proveedor que combine archivos y datos de tablas.
- En raras ocasiones, podrías beneficiarte de la implementación de más de un proveedor de contenido para una sola aplicación. Por ejemplo, es posible que quieras compartir algunos datos con un widget utilizando un proveedor de contenido y exponer un conjunto diferente de datos para compartirlos con otras aplicaciones.
-
Para trabajar con datos basados en la red, usa clases en
java.net
yandroid.net
. También puedes sincronizar datos basados en la red con un sitio de almacenamiento local, como una base de datos, y luego ofrecer los datos como tablas o archivos. La aplicación de muestra Ejemplo de adaptador de sincronización muestra este tipo de sincronización.
Nota: Si haces un cambio en tu repositorio que no es compatible con versiones anteriores, necesitas marcar el repositorio con un nuevo número de versión. También es necesario aumentar el número de versión de la aplicación que implementa el nuevo proveedor de contenido. Este cambio evita que las degradaciones provoquen que se bloquee el sistema cuando intente reinstalar una aplicación que tenga un proveedor de contenido incompatible.
Consideraciones del diseño de datos
Aquí te proporcionamos algunas sugerencias para diseñar la estructura de datos de tu proveedor:
-
Los datos de tabla siempre deben tener una columna de "clave primaria" que el proveedor mantiene como valor numérico único para cada fila. Puedes usar este valor para vincular la fila con filas relacionadas de otras tablas (usándola como "clave externa"). Si bien puedes usar cualquier nombre para esta columna, se recomienda utilizar
BaseColumns._ID
, ya que el enlace de los resultados de una consulta al proveedor con unaListView
requiere que una de las columnas recuperadas tenga el nombre_ID
. -
Si quieres proporcionar imágenes de mapas de bits u otro tipo de conjuntos grandes de datos orientados a archivos, guarda los datos en un archivo y luego proporciónalos indirectamente, en lugar de guardarlos directamente en una tabla. Si haces esto, deberás indicarles a los usuarios de tu proveedor que necesitan usar un método de archivo
ContentResolver
para acceder a los datos. -
Usa el tipo de datos Objeto binario grande (BLOB) para guardar datos que varíen en tamaño o tengan estructuras diferentes. Por ejemplo, puedes usar una columna BLOB para guardar un búfer de protocolo o una estructura JSON.
También puedes usar un BLOB para implementar una tabla independiente de esquemas. En este tipo de tabla, defines una columna de clave primaria, una columna de tipo de MIME y una o más columnas genéricas como BLOB. El significado de los datos en las columnas BLOB se indica mediante el valor de la columna de tipo de MIME. Esto te permite guardar diferentes tipos de filas en la misma tabla. La tabla "datos" del proveedor de contactos
ContactsContract.Data
es un ejemplo de una tabla independiente de esquemas.
Cómo diseñar URI de contenido
Un URI de contenido es un URI que identifica datos de un proveedor. Los URI de contenido incluyen el nombre simbólico de todo el proveedor (su autoridad) y un nombre que apunta a una tabla o un archivo (una ruta de acceso). La parte del ID opcional apunta a una fila individual de una tabla. Cada método de acceso a datos del ContentProvider
tiene un URI de contenido en forma de argumento; esto te permite determinar la tabla, la fila o el archivo al que se accederá.
Los conceptos básicos de los URI de contenido se describen en el tema Conceptos básicos de los proveedores de contenido.
Cómo diseñar una autoridad
Un proveedor generalmente tiene una sola autoridad, que sirve como su nombre interno en Android. Para evitar conflictos con otros proveedores, debes usar propiedad de dominios de Internet (en sentido inverso) como base de la autoridad de tu proveedor. Debido a que esta recomendación también se aplica a los nombres de paquete de Android, puedes definir la autoridad de tu proveedor como una extensión del nombre del paquete que contiene al proveedor. Por ejemplo, si el nombre de tu paquete de Android es com.example.<appname>
, debes proporcionarle a tu proveedor la autoridad com.example.<appname>.provider
.
Cómo diseñar la estructura de una ruta de acceso
Los desarrolladores generalmente crean URI de contenido a partir de la autoridad anexando rutas de acceso que apuntan a tablas individuales. Por ejemplo, si tienes dos tablas, table1 y table2, combinas la autoridad del ejemplo anterior para obtener URI de contenido com.example.<appname>.provider/table1
y com.example.<appname>.provider/table2
. Las rutas de acceso se limitan a un solo segmento, y no es necesario que haya una tabla para cada nivel de la ruta de acceso.
Cómo manipular los ID de URI de contenido
Por convención, los proveedores ofrecen acceso a una sola fila en una tabla aceptando un URI de contenido con un valor de ID para la fila al final del URI. También por convención, los proveedores comparan el valor del ID con la columna _ID
de la tabla y realizan el acceso solicitado en función de la fila que coincide.
Esta convención facilita un patrón de diseño común para aplicaciones que acceden a un proveedor. La aplicación realiza una consulta al proveedor y muestra el Cursor
resultante en una ListView
usando un CursorAdapter
. La definición de CursorAdapter
requiere que una de las columnas de Cursor
sea _ID
Luego, el usuario selecciona una de las filas exhibidas de la IU para observar o modificar los datos. La aplicación obtiene la fila correspondiente del Cursor
que respalda la ListView
, obtiene el valor _ID
para esta fila, lo anexa al URI de contenido y envía la solicitud de acceso al proveedor. Luego, el proveedor puede realizar la consulta o la modificación en la fila exacta que seleccionó el usuario.
Patrones de URI de contenido
Para ayudarte a seleccionar la acción que debes realizar para un URI de contenido entrante, la API del proveedor incluye la clase de conveniencia UriMatcher
, que asigna "patrones" de URI de contenido a valores enteros. Puedes usar los valores enteros en una instrucción switch
que seleccione la acción deseada para los URI de contenido que coincidan con un patrón específico.
Un patrón de URI de contenido compara URI de contenido usando caracteres comodín:
-
*
: Compara una string de caracteres válidos de cualquier longitud. -
#
: Compara una string de caracteres numéricos de cualquier longitud.
Como ejemplo de diseño y programación de la manipulación de URI de contenido, considera un proveedor con la autoridad com.example.app.provider
que reconozca los siguientes URI de contenido que apunten a tablas:
-
content://com.example.app.provider/table1
: Una tabla llamadatable1
. -
content://com.example.app.provider/table2/dataset1
: Una tabla llamadadataset1
. -
content://com.example.app.provider/table2/dataset2
: Una tabla llamadadataset2
. -
content://com.example.app.provider/table3
: Una tabla llamadatable3
.
El proveedor también reconoce esos URI de contenido si tienen un ID de fila anexado a ellos, como, por ejemplo, content://com.example.app.provider/table3/1
para la fila identificada por 1
en table3
.
Los siguientes patrones de URI de contenido serían posibles:
-
content://com.example.app.provider/*
- Compara cualquier URI de contenido en el proveedor.
-
content://com.example.app.provider/table2/*
: -
Compara un URI de contenido para las tablas
dataset1
ydataset2
, pero no compara URI de contenido paratable1
ni paratable3
. -
content://com.example.app.provider/table3/#
: Compara un URI de contenido para filas individuales entable3
, comocontent://com.example.app.provider/table3/6
para la fila identificada por6
.
El siguiente fragmento de código muestra cómo funcionan los métodos en UriMatcher
. Este código manipula URI para una tabla entera de forma diferente de los URI para una sola fila mediante el uso del patrón de URI de contenido content://<authority>/<path>
para tablas y content://<authority>/<path>/<id>
para filas individuales.
El método addURI()
asigna una autoridad y una ruta de acceso a un valor entero. El método match()
muestra el valor entero para un URI. Una instrucción switch
elige entre consultar toda la tabla o consultar un solo registro:
Kotlin
private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { /* * The calls to addURI() go here, for all of the content URI patterns that the provider * should recognize. For this snippet, only the calls for table 3 are shown. */ /* * Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used * in the path */ addURI("com.example.app.provider", "table3", 1) /* * Sets the code for a single row to 2. In this case, the "#" wildcard is * used. "content://com.example.app.provider/table3/3" matches, but * "content://com.example.app.provider/table3 doesn't. */ addURI("com.example.app.provider", "table3/#", 2) } ... class ExampleProvider : ContentProvider() { ... // Implements ContentProvider.query() override fun query( uri: Uri?, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String? ): Cursor? { var localSortOrder: String = sortOrder ?: "" var localSelection: String = selection ?: "" when (sUriMatcher.match(uri)) { 1 -> { // If the incoming URI was for all of table3 if (localSortOrder.isEmpty()) { localSortOrder = "_ID ASC" } } 2 -> { // If the incoming URI was for a single row /* * Because this URI was for a single row, the _ID value part is * present. Get the last path segment from the URI; this is the _ID value. * Then, append the value to the WHERE clause for the query */ localSelection += "_ID ${uri?.lastPathSegment}" } else -> { // If the URI is not recognized // You should do some error handling here. } } // call the code to actually do the query } }
Java
public class ExampleProvider extends ContentProvider { ... // Creates a UriMatcher object. private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { /* * The calls to addURI() go here, for all of the content URI patterns that the provider * should recognize. For this snippet, only the calls for table 3 are shown. */ /* * Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used * in the path */ uriMatcher.addURI("com.example.app.provider", "table3", 1); /* * Sets the code for a single row to 2. In this case, the "#" wildcard is * used. "content://com.example.app.provider/table3/3" matches, but * "content://com.example.app.provider/table3 doesn't. */ uriMatcher.addURI("com.example.app.provider", "table3/#", 2); } ... // Implements ContentProvider.query() public Cursor query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { ... /* * Choose the table to query and a sort order based on the code returned for the incoming * URI. Here, too, only the statements for table 3 are shown. */ switch (uriMatcher.match(uri)) { // If the incoming URI was for all of table3 case 1: if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC"; break; // If the incoming URI was for a single row case 2: /* * Because this URI was for a single row, the _ID value part is * present. Get the last path segment from the URI; this is the _ID value. * Then, append the value to the WHERE clause for the query */ selection = selection + "_ID = " + uri.getLastPathSegment(); break; default: ... // If the URI is not recognized, you should do some error handling here. } // call the code to actually do the query }
Otra clase, ContentUris
, proporciona métodos de conveniencia para trabajar con la parte id
de los URI de contenido. Las clases Uri
y Uri.Builder
incluyen métodos de conveniencia para analizar objetos Uri
y crear nuevos.
Implementación de la clase ContentProvider
La instancia ContentProvider
administra el acceso a un conjunto de datos estructurados mediante la manipulación de solicitudes de otras aplicaciones. Todas las formas de acceso eventualmente llaman a ContentResolver
, que luego llama a un método concreto de ContentProvider
para obtener acceso.
Métodos necesarios
La clase abstracta ContentProvider
define seis métodos abstractos que debes implementar como parte de tu propia subclase concreta. A todos estos métodos, excepto onCreate()
, los llama una aplicación cliente que intenta acceder a tu proveedor de contenido:
-
query()
-
Recupera datos de tu proveedor. Usa los argumentos para seleccionar la tabla que quieras consultar, así como las filas y columnas que quieras mostrar y el orden de los resultados. Muestra los datos en forma de objeto
Cursor
. -
insert()
- Inserta una nueva fila en tu proveedor. Usa los argumentos para seleccionar la tabla de destino y obtener los valores de columna que se usarán. Muestra un URI de contenido para la fila insertada recientemente.
-
update()
- Actualiza las filas existentes en tu proveedor. Usa los argumentos para seleccionar la tabla de destino y obtener los valores de la columna que se usarán. Muestra el número de filas actualizadas.
-
delete()
- Borra las filas de tu proveedor. Usa los argumentos para seleccionar la tabla y las filas que se borrarán. Muestra el número de filas borradas.
-
getType()
- Muestra el tipo de MIME correspondiente al URI de contenido. Este método se describe con mayor profundidad en la sección Cómo implementar tipos de MIME del proveedor de contenido.
-
onCreate()
-
Inicializa tu proveedor. El sistema Android llama a este método inmediatamente después de crear tu proveedor. Ten en cuenta que tu proveedor no se crea hasta que un objeto
ContentResolver
intenta acceder a él.
Ten en cuenta que estos métodos tienen la misma firma que los métodos ContentResolver
de igual nombre.
La implementación que realices de estos métodos debe considerar lo siguiente:
-
Todos estos métodos excepto
onCreate()
pueden ser llamados por varios subprocesos a la vez, de modo que deben ser seguros para estos. Para obtener más información acerca de varios subprocesos, consulta el tema Procesos y subprocesos. -
Evita realizar operaciones extensas en
onCreate()
. Aplaza tareas de inicialización hasta que sean necesarias. La sección Cómo implementar el método onCreate() aborda esto más detalladamente. -
Si bien debes implementar estos métodos, tu código no necesita hacer nada excepto mostrar el tipo de datos previsto. Por ejemplo, deberías evitar que otras aplicaciones inserten datos en algunas tablas. Para lograr esto, puedes ignorar la llamada a
insert()
y mostrar 0.
Cómo implementar el método query()
El método ContentProvider.query()
debe mostrar un objeto Cursor
o, si falla, arroja un Exception
. Si estás usando una base de datos SQLite como punto de almacenamiento de datos, puedes simplemente mostrar el Cursor
de uno de los métodos query()
de la clase SQLiteDatabase
. Si la consulta no coincide con ninguna fila, debes mostrar una instancia Cursor
cuyo método getCount()
muestre 0. Debes mostrar null
solo si se produce un error interno durante el proceso de consulta.
Si no estás usando una base de datos SQLite como punto de almacenamiento de datos, usa una de las subclases de Cursor
. Por ejemplo, la clase MatrixCursor
implementa un cursor en el que cada fila es una matriz de Object
. Con esta clase, usa addRow()
para agregar una nueva fila.
Recuerda que el sistema Android debe poder comunicar la Exception
en todos los límites del proceso. Android puede hacer esto para las siguientes excepciones que podrían ser útiles a fin de manipular errores en la consulta:
-
IllegalArgumentException
(Puedes optar por producir esto si tu proveedor recibe un URI de contenido no válido). -
NullPointerException
Cómo implementar el método insert()
El método insert()
agrega una nueva fila a la tabla correspondiente usando los valores del argumento ContentValues
. Si el nombre de una columna no está en el argumento ContentValues
, puedes proporcionar un valor predeterminado para él, ya sea en tu código de proveedor o en el esquema de tu base de datos.
Este método debe mostrar el URI de contenido para la nueva fila. Para construir esto, anexa el nuevo valor _ID
de la fila (u otra clave primaria) al URI de contenido de la tabla usando withAppendedId()
.
Cómo implementar el método delete()
El método delete()
no tiene que borrar filas físicamente de tu punto de almacenamiento de datos. Si usas un adaptador de sincronización con tu proveedor, debes considerar marcar una fila borrada con un indicador "borrar" en lugar de borrar la fila por completo. El adaptador de sincronización puede comprobar la existencia de filas borradas y quitarlas del servidor antes de borrarlas del proveedor.
Cómo implementar el método update()
El método update()
toma el mismo argumento ContentValues
que usa insert()
, y los mismos objetos selection
y selectionArgs
que usan delete()
y ContentProvider.query()
. Esto te puede permitir reutilizar código entre estos métodos.
Cómo implementar el método onCreate()
El sistema Android llama a onCreate()
cuando inicia el proveedor. En este método, solo debes realizar tareas de inicialización rápida y aplazar la creación de la base de datos y la carga de datos hasta que el proveedor reciba una solicitud de datos. Si realizas tareas más extensas en onCreate()
, harás que el inicio de tu proveedor sea más lento. A la vez, esto demorará la respuesta del proveedor a otras aplicaciones.
Los dos fragmentos de código siguientes demuestran la interacción entre ContentProvider.onCreate()
y Room.databaseBuilder()
. El fragmento muestra la implementación de ContentProvider.onCreate()
donde se crea el objeto de la base de datos y donde se crean las manipulaciones de los objetos de acceso a los datos:
Kotlin
// Defines the database name private const val DBNAME = "mydb" ... class ExampleProvider : ContentProvider() { // Defines a handle to the Room database private lateinit var appDatabase: AppDatabase // Defines a Data Access Object to perform the database operations private var userDao: UserDao? = null override fun onCreate(): Boolean { // Creates a new database object. appDatabase = Room.databaseBuilder(context, AppDatabase::class.java, DBNAME).build() // Gets a Data Access Object to perform the database operations userDao = appDatabase.userDao return true } ... // Implements the provider's insert method override fun insert(uri: Uri, values: ContentValues?): Uri? { // Insert code here to determine which DAO to use when inserting data, handle error conditions, etc. } }
Java
public class ExampleProvider extends ContentProvider // Defines a handle to the Room database private AppDatabase appDatabase; // Defines a Data Access Object to perform the database operations private UserDao userDao; // Defines the database name private static final String DBNAME = "mydb"; public boolean onCreate() { // Creates a new database object. appDatabase = Room.databaseBuilder(getContext(), AppDatabase.class, DBNAME).build(); // Gets a Data Access Object to perform the database operations userDao = appDatabase.getUserDao(); return true; } ... // Implements the provider's insert method public Cursor insert(Uri uri, ContentValues values) { // Insert code here to determine which DAO to use when inserting data, handle error conditions, etc. } }
Cómo implementar tipos de MIME ContentProvider
La clase ContentProvider
tiene dos métodos para mostrar tipos de MIME:
-
getType()
- Uno de los métodos obligatorios que debes implementar para cualquier proveedor.
-
getStreamTypes()
- Un método que se espera que implementes si tu proveedor ofrece archivos.
Tipos de MIME para tablas
El método getType()
muestra una String
en formato MIME que describe el tipo de datos que muestra el argumento del URI de contenido. El argumento Uri
puede ser un patrón en lugar de un URI específico; en ese caso, debes mostrar el tipo de datos asociado con los URI de contenido que coincidan con el patrón.
Para los tipos de datos comunes, como texto, HTML o JPEG, getType()
debe mostrar el tipo de MIME estándar para esos datos. Puedes encontrar una lista completa de esos tipos estándar en el sitio web IANA MIME Media Types.
Para los URI de contenido que apuntan a una o más filas de datos de una tabla, getType()
debe mostrar un tipo de MIME en formato MIME específico para proveedores de Android:
-
Parte de tipo:
vnd
-
Parte de subtipo:
-
Si el patrón del URI es para una sola fila:
android.cursor.item/
-
Si el patrón del URI es para más de una fila:
android.cursor.dir/
-
Si el patrón del URI es para una sola fila:
-
Parte específica para el proveedor:
vnd.<name>
.<type>
Debes proporcionar
<name>
y<type>
. El valor<name>
debe ser globalmente único y el valor<type>
debe ser único para el patrón de URI correspondiente. Una buena opción para<name>
es el nombre de tu empresa o alguna parte del nombre de paquete de Android de tu aplicación. Una buena opción para<type>
es una string que identifica la tabla asociada con el URI.
Por ejemplo, si la autoridad de un proveedor es com.example.app.provider
y expone una tabla llamada table1
, el tipo de MIME para múltiples filas en table1
es:
vnd.android.cursor.dir/vnd.com.example.provider.table1
Para una sola fila de table1
, el tipo de MIME es:
vnd.android.cursor.item/vnd.com.example.provider.table1
Tipos de MIME para archivos
Si tu proveedor ofrece archivos, implementa getStreamTypes()
. El método muestra una matriz String
de tipos de MIME para los archivos que tu proveedor puede mostrar para un URI de contenido determinado. Debes filtrar los tipos de MIME que ofreces por el argumento del filtro de tipos de MIME, de modo que muestres solo los tipos de MIME que el cliente quiere manipular.
Por ejemplo, piensa en un proveedor que ofrece fotos en forma de archivo con formato .jpg
, .png
y .gif
. Si una aplicación llama a ContentResolver.getStreamTypes()
con la string de filtros image/*
(algo que es una "imagen"), el método ContentProvider.getStreamTypes()
debe mostrar la matriz:
{ "image/jpeg", "image/png", "image/gif"}
Si a la aplicación solo le interesan los archivos .jpg
, puede llamar a ContentResolver.getStreamTypes()
con la string de filtros *\/jpeg
, y ContentProvider.getStreamTypes()
debe mostrar:
{"image/jpeg"}
Si tu proveedor no ofrece ninguno de los tipos de MIME solicitados en la string de filtros, getStreamTypes()
debe mostrar null
.
Cómo implementar una clase Contract
Una clase Contract es una clase public final
que contiene definiciones de constantes para URI, nombres de columnas, tipos de MIME y otros metadatos que le pertenecen al proveedor. La clase establece un contrato entre el proveedor y otras aplicaciones al asegurarse de que se pueda acceder correctamente al proveedor aunque se hayan producido cambios en los valores de los URI, los nombres de las columnas, etc.
Una clase Contract también ayuda a los desarrolladores porque generalmente tiene nombres mnemotécnicos para sus constantes, de modo que los desarrolladores tengan menos probabilidades de usar valores incorrectos para los nombres de las columnas o los URI. Como es una clase, puede contener documentación Javadoc. Los entornos de desarrollo integrados, como Android Studio, pueden autocompletar nombres de constantes desde la clase Contract y mostrar Javadoc para las constantes.
Los desarrolladores no pueden acceder al archivo de clase de la clase Contract desde tu aplicación, pero pueden compilar estáticamente en su aplicación desde un archivo .jar
que tú proporciones.
La clase ContactsContract
y sus clases anidadas son ejemplos de clases de contrato.
Cómo implementar permisos del proveedor de contenido
Los permisos y el acceso para todos los aspectos del sistema Android se describen en detalle en el tema Seguridad y permisos. El tema Almacenamiento de datos también describió la seguridad y los permisos vigentes para varios tipos de almacenamiento. En resumen, los puntos importantes son los siguientes:
- De forma predeterminada, los archivos de datos guardados en el punto de almacenamiento interno del dispositivo son privados de tu aplicación y proveedor.
-
Las bases de datos
SQLiteDatabase
que creas son privadas para tu aplicación y proveedor. - De forma predeterminada, los archivos de datos que guardas en tu punto de almacenamiento externo son públicos y pueden ser leídos por cualquier usuario. No puedes usar un proveedor de contenido para restringir el acceso a archivos en el punto de almacenamiento externo, ya que otras aplicaciones pueden usar otras llamadas de la API para leerlos y escribirlos.
- Las llamadas a métodos para abrir o crear archivos o bases de datos SQLite en el punto de almacenamiento interno del dispositivo pueden otorgar acceso de lectura y escritura a todas las demás aplicaciones. Si usas un archivo o base de datos interno como repositorio de tu proveedor, y le otorgas acceso de lectura y escritura abierto para cualquier usuario, los permisos que establezcas para tu proveedor en su manifiesto no protegerán tus datos. El acceso predeterminado para archivos y bases de datos en el punto de almacenamiento interno es "privado", y no debes cambiar esto para el repositorio de tu proveedor.
Si quieres usar permisos del proveedor de contenido para controlar el acceso a tus datos, debes guardar tus datos en archivos internos, en bases de datos SQLite o en la nube (por ejemplo, en un servidor remoto), y debes mantener los archivos y las bases de datos de forma privada para tu aplicación.
Cómo implementar permisos
Todas las aplicaciones pueden leer de tu proveedor o escribir en él, incluso si los datos subyacentes son privados, ya que, de forma predeterminada, tu proveedor no tiene permisos establecidos. Para cambiar esto, configura permisos para tu proveedor en tu archivo de manifiesto usando atributos o elementos secundarios del elemento
<provider>
. Puedes establecer permisos que se apliquen a todo el proveedor, o a ciertas tablas, o incluso a ciertos registros, o a los tres.
Defines permisos para tu proveedor con uno o más elementos
<permission>
en tu archivo de manifiesto. Para que el permiso sea exclusivo de tu proveedor, usa el ámbito de estilo Java para el atributo
android:name
. Por ejemplo, nombre y permiso de lectura com.example.app.provider.permission.READ_PROVIDER
.
La siguiente lista describe el ámbito de los permisos del proveedor comenzando con los que se aplican a todo el proveedor y luego se van refinando. Los permisos más refinados tienen prioridad sobre aquellos con un alcance más amplio:
- Permiso individual de lectura y escritura a nivel del proveedor
-
Permiso que controla el acceso de lectura y escritura a todo el proveedor, especificado con el atributo
android:permission
del elemento<provider>
. - Permisos independientes de lectura y escritura a nivel del proveedor
-
Un permiso de lectura y un permiso de escritura para todo el proveedor. Los especificas con los atributos
android:readPermission
yandroid:writePermission
del elemento<provider>
. Tienen prioridad sobre el permiso requerido porandroid:permission
. - Permiso a nivel de ruta de acceso
-
Permiso de lectura, escritura o lectura/escritura para un URI de contenido en tu proveedor. Especificas cada URI que quieres controlar con un elemento secundario
<path-permission>
del elemento<provider>
. Para cada URI de contenido que especificas, puedes especificar un permiso de lectura/escritura, un permiso de lectura, un permiso de escritura o los tres. Los permisos de lectura y escritura tienen prioridad sobre el permiso de lectura/escritura. Asimismo, el permiso a nivel de ruta de acceso tiene prioridad sobre los permisos a nivel del proveedor. - Permiso temporal
-
Un nivel de permiso que permite el acceso temporal a una aplicación, incluso cuando la aplicación no tiene los permisos que normalmente se requieren. La función de acceso temporal reduce la cantidad de permisos que una aplicación debe requerir en su manifiesto. Cuando activas los permisos temporales, las únicas aplicaciones que necesitan permisos "permanentes" para tu proveedor son las que acceden a todos tus datos de forma continua.
Considera los permisos que necesitas para implementar un proveedor y una aplicación de correo electrónico cuando quieras permitir que una aplicación para ver imágenes muestre archivos adjuntos de fotografía desde tu proveedor. Para otorgarle al visor de imágenes el acceso necesario sin requerir permisos, establece permisos temporales para los URI de contenido para fotos. Diseña tu aplicación de correo electrónico de modo que, cuando el usuario quiera mostrar una foto, la aplicación envíe una intent con el URI de contenido de la foto e indicadores de permiso al visor de imágenes. Luego, el visor de imágenes podrá consultar tu proveedor de correo electrónico para recuperar la foto, aunque no tenga el permiso de lectura normal.
Para activar los permisos temporales, establece el atributo
android:grantUriPermissions
del elemento<provider>
o agrega uno o más elementos secundarios<grant-uri-permission>
a tu elemento<provider>
. Si usas permisos temporales, deberás llamar aContext.revokeUriPermission()
cada vez que dejes de admitir un URI de contenido de tu proveedor, y el URI de contenido estará asociado a un permiso temporal.El valor del atributo determina la porción del proveedor a la que se puede acceder. Si el atributo está configurado en
true
, el sistema otorgará permiso temporal para todo tu proveedor y anulará cualquier otro permiso que requieran tus permisos a nivel de proveedor o de ruta de acceso.Si este indicador está configurado en
false
, debes agregar elementos secundarios<grant-uri-permission>
a tu elemento<provider>
. Cada elemento secundario especifica el URI o los URI de contenido para los que se concede el acceso temporal.Para delegar el acceso temporal a una aplicación, una intent debe contener el indicador
FLAG_GRANT_READ_URI_PERMISSION
oFLAG_GRANT_WRITE_URI_PERMISSION
, o ambos. Estos indicadores se establecen con el métodosetFlags()
.Si el atributo
android:grantUriPermissions
no está presente, se supone que esfalse
.
El elemento <provider>
Al igual que los componentes Activity
y Service
, se debe definir una subclase de ContentProvider
en el archivo de manifiesto de su aplicación usando el elemento
<provider>
. El sistema Android obtiene la siguiente información del elemento:
-
Autoridad (
android:authorities
) - Nombres simbólicos que identifican al proveedor completo en el sistema. Este atributo se describe de forma más detallada en la sección Diseño de URI de contenido.
-
Nombre de clase del proveedor (
android:name
) -
La clase que implementa
ContentProvider
. Esta clase se describe de forma más detallada en la sección Cómo implementar la clase ContentProvider. - Permisos
-
Atributos que especifican los permisos que otras aplicaciones deben tener para poder acceder a los datos del proveedor:
-
android:grantUriPermssions
: Indicador de permiso temporal. -
android:permission
: Permiso individual de lectura/escritura en todo el proveedor. -
android:readPermission
: Permiso de lectura en todo el proveedor. -
android:writePermission
: Permiso de escritura en todo el proveedor.
Los permisos y sus atributos correspondientes se describen de forma más detallada en la sección Cómo implementar permisos del proveedor de contenido.
-
- Atributos de inicio y control
-
Estos atributos determinan cómo y cuándo el sistema Android inicia el proveedor, las características del proceso del proveedor y otras configuraciones de tiempo de ejecución:
-
android:enabled
: Indicador que le permite al sistema iniciar el proveedor. -
android:exported
: Indicador que permite que otras aplicaciones usen este proveedor. -
android:initOrder
: El orden en el que se debe iniciar el proveedor en función de otros proveedores en el mismo proceso. -
android:multiProcess
: Indicador que le permite al sistema iniciar el proveedor en el mismo proceso que el cliente que realiza la llamada. -
android:process
: Nombre del proceso en el que se debe ejecutar el proveedor. -
android:syncable
: Indicador que señala que los datos del proveedor se sincronizarán con los datos de un servidor.
Los atributos están completamente documentados en el tema del elemento
<provider>
en la guía para desarrolladores. -
- Atributos informativos
-
Un ícono y una etiqueta opcionales del proveedor:
-
android:icon
: Un recurso de elemento de diseño que contiene un ícono para el proveedor. El ícono aparece junto a la etiqueta del proveedor en la lista de aplicaciones, en Configuración > Apps > Todas. -
android:label
: Una etiqueta informativa que describe el proveedor o sus datos, o ambos. La etiqueta aparece en la lista de aplicaciones, en Configuración > Apps > Todas.
Los atributos están completamente documentados en el tema del elemento
<provider>
en la guía para desarrolladores. -
Intents y acceso a datos
Las aplicaciones pueden acceder a un proveedor de contenido de forma indirecta con una Intent
. La aplicación no llama a ninguno de los métodos de ContentResolver
o ContentProvider
. En su lugar, envía una intent que inicia una actividad, que a menudo forma parte de la aplicación del proveedor. La actividad de destino está a cargo de recuperar y mostrar los datos en su IU. Según la acción en la intent, la actividad de destino también puede indicarle al usuario que modifique los datos del proveedor. Una intent también debe contener datos extra que la actividad de destino muestra en la IU; el usuario tiene la opción de cambiar estos datos antes de usarlos para modificar los datos en el proveedor.
Te recomendamos que uses acceso mediante intents para ayudar a garantizar la integridad de los datos. Es posible que tu proveedor esté sujeto a la inserción, actualización y eliminación de datos conforme a lógica de negocio bien definida. Si este es el caso, permitir que otras aplicaciones modifiquen directamente tus datos puede generar datos no válidos. Si quieres que los desarrolladores usen acceso mediante intents, asegúrate de documentarlo exhaustivamente. Explícales por qué el acceso mediante intents con la IU de tu aplicación es mejor que tratar de modificar datos con su código.
La manipulación de una intent entrante que quiere modificar los datos de tu proveedor no difiere de la manipulación de otras intents. Puedes obtener más información acerca de cómo usar intents leyendo el tema Intents y filtros de intents.
Para ver un ejemplo de código relacionado con esta página, consulta la aplicación de ejemplo de cuaderno.
Para obtener más información relacionada, consulta Proveedor de calendario.