Cómo crear un proveedor de contenido

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 para Android, junto con elementos en el archivo de manifiesto. Una de tus clases implementa una subclase de ContentProvider, que es la interfaz entre tu proveedor y otras aplicaciones.

Si bien los proveedores de contenido hacen 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 que administra tu proveedor.

En esta página, se incluye el proceso básico para crear un proveedor de contenido y una lista de las APIs que se deben usar.

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 compilar 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.
    • Quieres permitir que los usuarios copien datos complejos de tu app en otras apps.
    • 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 las clases AbstractThreadedSyncAdapter, CursorAdapter o CursorLoader.

    No necesitas un proveedor para usar bases de datos ni otros tipos de almacenamiento persistente si el uso ocurre completamente dentro de tu aplicación y no necesitas ninguna de las funciones anteriores mencionadas. En su lugar, puedes usar uno de los sistemas de almacenamiento que se describen en Descripción general del almacenamiento de datos y archivos.

  • Si aún no lo has hecho, lee Conceptos básicos sobre los proveedores de contenido para obtener más información sobre sus proveedores y su funcionamiento.

A continuación, sigue estos pasos para crear un proveedor:

  1. 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. Almacena 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, un array o una estructura similar. Almacena los datos en un formato que sea compatible con tablas de filas y columnas. Una fila representa una entidad, como una persona o un elemento en el inventario. Una columna representa algunos datos de la entidad, como el nombre de la persona o el precio de un elemento. Una forma común de almacenar 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 sobre los tipos de almacenamiento disponibles en el sistema Android, consulta la sección Cómo diseñar el almacenamiento de datos.
  2. Define una implementación concreta de la clase ContentProvider y sus métodos obligatorios. Esta clase es la interfaz entre tus datos y el resto del sistema Android. Para obtener más información sobre esta clase, consulta la sección Cómo implementar la clase ContentProvider.
  3. Define la string de autoridad del proveedor, los URI de contenido y los nombres de columnas. Si quieres que la aplicación del proveedor controle intents, define también acciones de intent, datos adicionales y marcas. Define también los permisos que requieres para las aplicaciones que quieran acceder a tus datos. Procura definir todos estos valores como constantes en una clase Contract independiente. Más adelante, puedes exponer esta clase a otros desarrolladores. Para obtener más información sobre los URI de contenido, consulta la sección Cómo diseñar URI de contenido. Para obtener más información sobre los intents, consulta la sección Intents y acceso a datos.
  4. Agrega otras piezas opcionales, como datos de muestra o una implementación de AbstractThreadedSyncAdapter que pueda sincronizar datos entre el proveedor y los datos basados en la nube.

Cómo diseñar el almacenamiento de datos

Un proveedor de contenido es la interfaz para los datos guardados en un formato estructurado. Antes de crear la interfaz, decide cómo almacenar los datos. Puedes almacenar los datos en cualquier formato que desees 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 clave-valor no relacional, como LevelDB. Si trabajas con datos no estructurados, como contenido multimedia de audio, imagen o video, considera almacenar los datos como archivos. Puedes combinar y combinar varios tipos diferentes de almacenamiento y exponerlos con un solo proveedor de contenido si es necesario.
  • El sistema Android puede interactuar con la biblioteca de persistencias Room, que proporciona acceso a la API de base de datos SQLite que usan los proveedores de Android para almacenar datos orientados a tablas. Para crear una base de datos con esta biblioteca, crea una instancia de una subclase de RoomDatabase, como se describe en Cómo guardar contenido en una base de datos local con Room.

    No es necesario que uses 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 sobre el almacenamiento de archivos, consulta la Descripción general del almacenamiento de datos y archivos. 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 desees compartir algunos datos con un widget mediante 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 las clases en java.net y android.net. También puedes sincronizar datos basados en la red con un almacén de datos local, como una base de datos, y, luego, ofrecer los datos como tablas o archivos.

Nota: Si realizas un cambio en el repositorio que no es retrocompatible, debes marcar el repositorio con un nuevo número de versión. También debes aumentar el número de versión de la app que implementa el nuevo proveedor de contenido. Este cambio evita que los cambios a una versión inferior provoquen fallas cuando se intente reinstalar una app que tenga un proveedor de contenido incompatible.

Consideraciones del diseño de datos

Estos son algunos consejos 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 un valor numérico único para cada fila. Puedes usar este valor para vincular la fila con filas relacionadas en otras tablas (usándola como "clave externa"). Si bien puedes usar cualquier nombre para esta columna, el uso de BaseColumns._ID es la mejor opción, ya que vincular los resultados de una consulta del proveedor a una ListView requiere que una de las columnas recuperadas tenga el nombre _ID.
  • Si quieres proporcionar imágenes de mapas de bits u otros datos muy grandes orientados a archivos, almacena los datos en un archivo y, luego, proporciónalos indirectamente, en lugar de guardarlos directamente en una tabla. Si lo haces, debes indicarles a los usuarios de tu proveedor que deben usar un método de archivo ContentResolver para acceder a los datos.
  • Usa el tipo de datos de objeto binario grande (BLOB) para almacenar datos que varíen en tamaño o tengan estructuras diferentes. Por ejemplo, puedes usar una columna BLOB para almacenar un búfer de protocolo o una estructura JSON.

    También puedes usar un BLOB para implementar una tabla independiente del esquema. 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 almacenar diferentes tipos de filas en la misma tabla. La tabla "datos" ContactsContract.Data del Proveedor de contactos 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 los datos de ContentProvider tiene un URI de contenido como argumento. Esto te permite determinar la tabla, la fila o el archivo al que se accederá.

Para obtener información sobre los URI de contenido, consulta Conceptos básicos sobre los proveedores de contenido.

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, usa la propiedad del dominio de Internet (en sentido inverso) como base de la autoridad de tu proveedor. Dado que esta recomendación también se aplica a los nombres de paquetes 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>, otórgale a tu proveedor la autoridad com.example.<appname>.provider.

Diseña una estructura de ruta

Por lo general, los desarrolladores 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, puedes combinarlas con la autoridad del ejemplo anterior para obtener los 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.

Cómo controlar los IDs de URI de contenido

Por convención, los proveedores ofrecen acceso a una sola fila en una tabla si aceptan un URI de contenido con un valor de ID para la fila al final del URI. También por convención, los proveedores hacen coincidir el valor de ID con la columna _ID de la tabla y realizan el acceso solicitado en la fila que coincide.

Esta convención facilita un patrón de diseño común para aplicaciones que acceden a un proveedor. La app realiza una consulta al proveedor y muestra el Cursor resultante en una ListView mediante 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 que se muestran de la IU para observar o modificar los datos. La app obtiene la fila correspondiente del Cursor que respalda el ListView, obtiene el valor _ID para esta fila, lo agrega 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 elegir qué acción 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 de números enteros en una declaración switch que elijan 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:

  • * coincide con una string de cualquier carácter válido de cualquier longitud.
  • # coincide con una string de caracteres numéricos de cualquier longitud.

Como ejemplo de diseño y programación de manejo de URI de contenido, considera un proveedor con la autoridad com.example.app.provider que reconozca los siguientes URI de contenido que apuntan a tablas:

  • content://com.example.app.provider/table1: Una tabla llamada table1.
  • content://com.example.app.provider/table2/dataset1: Una tabla llamada dataset1.
  • content://com.example.app.provider/table2/dataset2: Una tabla llamada dataset2.
  • content://com.example.app.provider/table3: Una tabla llamada table3.

El proveedor también reconoce estos URI de contenido si tienen un ID de fila anexado, como content://com.example.app.provider/table3/1 para la fila identificada por 1 en table3.

Los siguientes patrones de URI de contenido son posibles:

content://com.example.app.provider/*
Coincide con cualquier URI de contenido en el proveedor.
content://com.example.app.provider/table2/*
Compara un URI de contenido para las tablas dataset1 y dataset2, pero no coincide con los URI de contenido de table1 ni table3.
content://com.example.app.provider/table3/#
Coincide con un URI de contenido para filas individuales en table3, como content://com.example.app.provider/table3/6 para la fila identificada por 6.

En el siguiente fragmento de código, se muestra cómo funcionan los métodos en UriMatcher. Este código controla los URI de una tabla completa de manera diferente a los URI de una sola fila mediante el 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 de número entero. El método match() muestra el valor de número 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 the content URI patterns that the provider
     * recognizes. 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 isn't recognized,
                // 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 the content URI patterns that the provider
         * recognizes. For this snippet, only the calls for table 3 are shown.
         */

        /*
         * Sets the integer value for multiple rows in table 3 to one. 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 isn't recognized, 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 existentes y compilar objetos nuevos.

Implementa la clase ContentProvider

La instancia ContentProvider administra el acceso a un conjunto estructurado de datos mediante la administració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 obligatorios

La clase abstracta ContentProvider define seis métodos abstractos que debes implementar como parte de la 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 deseas consultar, las filas y columnas que se mostrarán, y el orden de clasificación del resultado. Muestra los datos como un objeto Cursor.
insert()
Inserta una fila nueva 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 recién insertada.
update()
Actualiza las filas existentes en tu proveedor. Usa los argumentos para seleccionar la tabla y las filas que se actualizarán y obtener los valores de columna actualizados. Muestra el número de filas actualizadas.
delete()
Borra filas de tu proveedor. Usa los argumentos para seleccionar la tabla y las filas que deseas borrar. Muestra el número de filas borradas.
getType()
Muestra el tipo de MIME correspondiente a un URI de contenido. Este método se describe con más detalle 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. No se creará tu proveedor hasta que un objeto ContentResolver intente acceder a él.

Estos métodos tienen la misma firma que los métodos ContentResolver con el mismo nombre.

La implementación de estos métodos debe tener en cuenta lo siguiente:

  • Varios subprocesos a la vez pueden llamar a todos estos métodos, excepto onCreate(), por lo que deben ser seguros para los subprocesos. Para obtener más información acerca de varios subprocesos, consulta la Descripción general de procesos y subprocesos.
  • Evita realizar operaciones extensas en onCreate(). Aplaza tareas de inicialización hasta que sean necesarias. En la sección sobre la implementación del método onCreate(), se aborda esto en más detalle.
  • Si bien debes implementar estos métodos, tu código no tiene que hacer nada excepto mostrar el tipo de datos esperado. Por ejemplo, para evitar que otras aplicaciones inserten datos en algunas tablas, puedes ignorar la llamada a insert() y mostrar 0.

Implementa el método query()

El método ContentProvider.query() debe mostrar un objeto Cursor o, si falla, arrojar una Exception. Si usas una base de datos SQLite como almacenamiento de datos, puedes mostrar el Cursor que muestra uno de los métodos query() de la clase SQLiteDatabase.

Si la consulta no coincide con ninguna fila, muestra una instancia de Cursor cuyo método getCount() muestre 0. Muestra null solo si se produjo un error interno durante el proceso de consulta.

Si no usas una base de datos SQLite como almacenamiento de datos, usa una de las subclases de Cursor. Por ejemplo, la clase MatrixCursor implementa un cursor en el que cada fila es un array de instancias de Object. Con esta clase, usa addRow() para agregar una fila nueva.

El sistema Android debe poder comunicar el Exception en todos los límites del proceso. Android puede hacer esto para las siguientes excepciones que son útiles a la hora de manejar errores de consulta:

Implementa el método insert()

El método insert() agrega una fila nueva a la tabla correspondiente mediante los valores del argumento ContentValues. Si el nombre de una columna no está en el argumento ContentValues, te recomendamos que proporciones un valor predeterminado en el código de tu proveedor o en el esquema de tu base de datos.

Este método muestra el URI de contenido para la nueva fila. Para construirlo, agrega la clave primaria de la fila nueva (por lo general, el valor _ID) al URI de contenido de la tabla mediante withAppendedId().

Implementa el método delete()

El método delete() no tiene que borrar filas de tu almacenamiento de datos. Si usas un adaptador de sincronización con tu proveedor, considera marcar una fila borrada con una marca "borrar" en lugar de quitar la fila por completo. El adaptador de sincronización puede buscar 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 argumentos selection y selectionArgs que usan delete() y ContentProvider.query(). Esto podría permitirte reutilizar el código entre estos métodos.

Implementa el método onCreate()

El sistema Android llama a onCreate() cuando inicia el proveedor. En este método, solo realiza tareas de inicialización de ejecución rápida y aplaza 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 largas en onCreate(), harás que el inicio de tu proveedor sea más lento. A su vez, esto ralentiza la respuesta del proveedor a otras aplicaciones.

Los siguientes dos fragmentos demuestran la interacción entre ContentProvider.onCreate() y Room.databaseBuilder(). En el primer fragmento, se muestra la implementación de ContentProvider.onCreate(), donde se compila el objeto de base de datos y se encarga de crear los objetos de acceso a 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.
    }
}

Implementa tipos de MIME ContentProvider

La clase ContentProvider tiene dos métodos para mostrar tipos de MIME:

getType()
Es uno de los métodos obligatorios que implementas para cualquier proveedor.
getStreamTypes()
Es un método que se espera que implementes si tu proveedor ofrece archivos.

Tipos de MIME para tablas

El método getType() muestra un 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 este caso, muestra el tipo de datos asociados con los URI de contenido que coinciden con el patrón.

Para los tipos de datos comunes, como texto, HTML o JPEG, getType() muestra el tipo de MIME estándar para esos datos. Puedes encontrar una lista completa de estos tipos estándar en el sitio web IANA MIME Media Types.

Para los URI de contenido que apuntan a una fila o filas de datos de una tabla, getType() muestra un tipo de MIME en formato MIME específico del proveedor de Android:

  • Parte de tipo: vnd
  • Parte de subtipo:
    • Si el patrón de URI es para una sola fila: android.cursor.item/
    • Si el patrón de URI es para más de una fila: android.cursor.dir/
  • Parte específica del proveedor: vnd.<name>.<type>

    Debes proporcionar <name> y <type>. El valor <name> es único a nivel global y el valor <type> es ú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 del 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 de varias filas en table1 es el siguiente:

vnd.android.cursor.dir/vnd.com.example.provider.table1

Para una sola fila de table1, el tipo de MIME es el siguiente:

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 un arreglo String de tipos de MIME para los archivos que tu proveedor puede mostrar para un URI de contenido determinado. Filtra los tipos de MIME que ofreces por el argumento del filtro de tipos de MIME, de modo que se muestren solo los tipos de MIME que el cliente quiere manejar.

Por ejemplo, considera un proveedor que ofrece fotos como archivos en formato JPG, PNG y GIF. Si una aplicación llama a ContentResolver.getStreamTypes() con la string de filtro image/*, para algo que es una "imagen", el método ContentProvider.getStreamTypes() muestra el array:

{ "image/jpeg", "image/png", "image/gif"}

Si a la app solo le interesan los archivos JPG, puede llamar a ContentResolver.getStreamTypes() con la string de filtro *\/jpeg, y getStreamTypes() muestra lo siguiente:

{"image/jpeg"}

Si tu proveedor no ofrece ninguno de los tipos de MIME solicitados en la string de filtro, getStreamTypes() muestra null.

Cómo implementar una clase Contract

Una clase de contratos es una clase public final que contiene definiciones constantes para los URI, los nombres de columnas, los tipos de MIME y otros metadatos que pertenecen al proveedor. La clase establece un contrato entre el proveedor y otras aplicaciones, ya que garantiza que se pueda acceder correctamente al proveedor incluso si hay cambios en los valores reales de los URI, los nombres de las columnas, etcétera.

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 URIs. 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 proporciones.

La clase ContactsContract y sus clases anidadas son ejemplos de clases de contrato.

Implementa permisos del proveedor de contenido

Los permisos y el acceso para todos los aspectos del sistema Android se describen en detalle en las Sugerencias de seguridad. En la descripción general del almacenamiento de datos y archivos, también se describe 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 almacenamiento interno del dispositivo son privados para tu aplicación y proveedor.
  • Las bases de datos de SQLiteDatabase que creas son privadas para tu aplicación y proveedor.
  • De manera predeterminada, los archivos de datos que guardas en el almacenamiento externo son públicos y legibles para todo el mundo. No puedes usar un proveedor de contenido para restringir el acceso a archivos en el almacenamiento externo, ya que otras aplicaciones pueden usar otras llamadas a la API para leerlos y escribirlos.
  • Las llamadas de método para abrir o crear archivos o bases de datos SQLite en el almacenamiento interno de tu dispositivo pueden otorgar acceso de lectura y escritura a todas las demás apps. Si usas un archivo o una base de datos internos como repositorio de tu proveedor y le otorgas acceso de lectura y escritura en todo el mundo, los permisos que configures para tu proveedor en su manifiesto no protegerán tus datos. El acceso predeterminado para los archivos y las bases de datos en el almacenamiento interno es “privado”. No lo cambies para el repositorio de tu proveedor.

Si deseas usar permisos del proveedor de contenido para controlar el acceso a tus datos, almacénalos en archivos internos, bases de datos SQLite o en la nube, como en un servidor remoto, y mantén los archivos y las bases de datos en privado para tu aplicación.

Implementa permisos

De forma predeterminada, todas las aplicaciones pueden leer de tu proveedor o escribir en él, incluso si los datos subyacentes son privados, ya que, de forma predeterminada, el proveedor no tiene permisos establecidos. Para cambiar esto, establece 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, a ciertas tablas, 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 alcance de estilo Java para el atributo android:name. Por ejemplo, asigna un nombre al permiso de lectura com.example.app.provider.permission.READ_PROVIDER.

En la siguiente lista, se describe el alcance de los permisos del proveedor comenzando con los permisos que se aplican a todo el proveedor y luego se van refinando. Los permisos más detallados tienen prioridad sobre los que tienen un alcance más amplio.

Permiso individual de lectura y escritura a nivel del proveedor
Un 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 uno de escritura para todo el proveedor. Se especifican con los atributos android:readPermission y android:writePermission del elemento <provider>. Tienen prioridad sobre el permiso requerido por android:permission.
Permiso a nivel de ruta de acceso
Permiso de lectura, escritura o lectura y escritura para un URI de contenido en tu proveedor. Especifica cada URI que quieras controlar con un elemento secundario <path-permission> del elemento <provider>. Para cada URI de contenido que especifiques, puedes especificar un permiso de lectura y 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. Además, el permiso a nivel de ruta de acceso tiene prioridad sobre los permisos a nivel del proveedor.
Permiso temporal
Corresponde a un nivel de permiso que otorga acceso temporal a una aplicación, incluso si esta no tiene los permisos que normalmente se requieren. La función de acceso temporal reduce la cantidad de permisos que una aplicación debe solicitar en su manifiesto. Cuando activas los permisos temporales, las únicas aplicaciones que necesitan permisos permanentes para tu proveedor son las que acceden continuamente a todos tus datos.

Por ejemplo, ten en cuenta los permisos que necesitas si implementas un proveedor de correo electrónico y una app, y quieres permitir que una aplicación de visualización de imágenes externa muestre archivos adjuntos de fotos de tu proveedor. Para otorgarle al visor de imágenes el acceso necesario sin requerir permisos, puedes configurar permisos temporales para los URI de contenido de fotos.

Diseña tu app de correo electrónico para que, cuando el usuario quiera mostrar una foto, la app envíe un intent con el URI de contenido de la foto y las marcas 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 el visor no tenga el permiso de lectura normal para el proveedor.

Para activar los permisos temporales, configura el atributo android:grantUriPermissions del elemento <provider> o agrega uno o más elementos secundarios <grant-uri-permission> a tu elemento <provider>. Llama a Context.revokeUriPermission() cada vez que quites la compatibilidad con un URI de contenido asociado con un permiso temporal de tu proveedor.

El valor del atributo determina a qué parte de tu proveedor se puede acceder. Si el atributo está configurado como "true", el sistema otorgará permiso temporal a todo el proveedor y anulará cualquier otro permiso que requieran tus permisos a nivel de proveedor o ruta de acceso.

Si la marca se establece en "false", agrega 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, un intent debe contener las marcas FLAG_GRANT_READ_URI_PERMISSION o FLAG_GRANT_WRITE_URI_PERMISSION, o ambas. Estas se establecen con el método setFlags().

Si el atributo android:grantUriPermissions no está presente, se supone que es "false".

El elemento <provider>

Al igual que los componentes Activity y Service, una subclase de ContentProvider se define en el archivo de manifiesto de su aplicación, utilizando el elemento <provider>. El sistema Android obtiene la siguiente información del elemento:

Autoridad (android:authorities)
Son nombres simbólicos que identifican a todo el proveedor en el sistema. Este atributo se describe con más detalle en la sección Cómo diseñar URI de contenido.
Nombre de clase del proveedor (android:name)
Es la clase que implementa ContentProvider. Esta clase se describe con más detalle 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:

Los permisos y sus atributos correspondientes se describen con más detalle 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 otros parámetros de configuración del tiempo de ejecución:
  • android:enabled: Es una marca que permite que el sistema inicie el proveedor.
  • android:exported: marca que permite que otras aplicaciones usen este proveedor.
  • android:initOrder: Es el orden en el que se inicia este proveedor, en relación con otros proveedores en el mismo proceso.
  • android:multiProcess: Es una marca que permite que el sistema inicie el proveedor en el mismo proceso que el cliente que realiza la llamada.
  • android:process: Es el nombre del proceso en el que se ejecuta el proveedor.
  • android:syncable: Es una marca que indica que los datos del proveedor se deben sincronizar con los datos de un servidor.

Estos atributos están completamente documentados en la guía del elemento <provider>.

Atributos informativos
Un ícono y una etiqueta opcionales para el proveedor:
  • android:icon: Es 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 apps en Configuración > Apps > Todas.
  • android:label: Es una etiqueta informativa que describe el proveedor, sus datos o ambos. La etiqueta aparece en la lista de apps en Configuración > Apps > Todas.

Estos atributos están completamente documentados en la guía del elemento <provider>.

Intents y acceso a datos

Las aplicaciones pueden acceder a un proveedor de contenido de forma indirecta con un Intent. La aplicación no llama a ninguno de los métodos de ContentResolver o ContentProvider. En cambio, envía un 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 del intent, la actividad de destino también puede pedirle al usuario que modifique los datos del proveedor. Un intent también puede contener datos "adicionales" que la actividad de destino muestra en la IU. Luego, el usuario tiene la opción de cambiar estos datos antes de usarlos para modificar los datos en el proveedor.

Puedes usar el acceso mediante intents para ayudar a la integridad de los datos. Es posible que tu proveedor dependa de que se inserten, actualicen y borren datos de acuerdo con la lógica empresarial definida de forma estricta. 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. Explica por qué el acceso mediante intents con la IU de tu aplicación es mejor que intentar modificar los datos con su código.

La manipulación de un intent entrante que desea modificar los datos de tu proveedor no difiere de la manipulación de otros intents. Para obtener más información acerca de cómo usar intents, consulta Intents y filtros de intents.

Para obtener más información relacionada, consulta la descripción general del proveedor de calendario.