Conceptos básicos sobre proveedores de contenido

Un proveedor de contenido administra el acceso a un repositorio central de datos. Un proveedor forma parte de una aplicación para Android y a menudo proporciona su propia IU para trabajar con los datos. No obstante, los proveedores de contenido están principalmente orientados a que los usen otras aplicaciones que acceden al proveedor usando un objeto de cliente del proveedor. Juntos, los proveedores y clientes de proveedores ofrecen una interfaz estándar y uniforme para los datos que también manipula la comunicación dentro del proceso y el acceso seguro a los datos.

Por lo general, son dos las situaciones en las que se trabaja con proveedores de contenido: cuando quieres implementar código para acceder a un proveedor de contenido existente en otra aplicación o cuando decides crear un proveedor de contenido nuevo en tu aplicación a fin de compartir datos con otras aplicaciones. En este tema, se contemplan los aspectos básicos para trabajar con proveedores de contenido existentes. Para obtener más información sobre cómo implementar proveedores de contenido en tus propias aplicaciones, consulta Cómo crear un proveedor de contenido.

En este tema, se describe lo siguiente:

  • Cómo funcionan los proveedores de contenido.
  • La API que usas para recuperar datos de un proveedor de contenido.
  • La API que usas para insertar, actualizar o borrar datos en un proveedor de contenido.
  • Otras funciones de API que facilitan el trabajo con proveedores.

Descripción general

Un proveedor de contenido presenta datos a aplicaciones externas en forma de una o más tablas que son similares a las tablas de una base de datos relacional. Una fila representa una instancia de algún tipo de datos que recopila el proveedor, y cada columna de la fila representa un ítem individual de los datos recopilados para una instancia.

El proveedor de contenido organiza el acceso a la capa de almacenamiento de los datos en tu aplicación para una serie de API y componentes diferentes (como se detalla en la figura 1) e incluye lo siguiente:

  • Compartir con otras aplicaciones el acceso a los datos de tu aplicación
  • Enviar datos a un widget
  • Mostrar sugerencias personalizadas de búsqueda para tu aplicación mediante el marco de trabajo de búsqueda usando SearchRecentSuggestionsProvider
  • Sincronizar los datos de la aplicación con tu servidor mediante una implementación de AbstractThreadedSyncAdapter
  • Cargar datos en tu IU usando CursorLoader
Relación entre el proveedor de contenido y otros componentes

Figura 1: Relación entre el proveedor de contenido y otros componentes

Cómo acceder a un proveedor

Cuando quieres acceder a datos en un proveedor de contenido, usas el objeto ContentResolver en el Context de tu aplicación para comunicarte con el proveedor como cliente. El objeto ContentResolver se comunica con el objeto del proveedor, una instancia de una clase que implementa ContentProvider. El objeto del proveedor recibe solicitudes de datos de clientes, realiza la acción solicitada y muestra resultados. Este objeto tiene métodos que llaman a métodos con el mismo nombre en el objeto del proveedor, una instancia de una de las subclases concretas de ContentProvider. Los métodos ContentResolver proporcionan las funciones básicas "CRUD" (proveniente de los términos en inglés que equivalen a crear, recuperar, actualizar y borrar) del almacenamiento persistente.

Un patrón común para acceder a un ContentProvider desde tu IU usa un CursorLoader para ejecutar una consulta asíncrona en segundo plano. La Activity o el Fragment de tu IU llaman a un CursorLoader para la consulta, lo que permite obtener el ContentProvider mediante el ContentResolver. Así, la IU puede seguir estando disponible para el usuario mientras se ejecuta la consulta. Este patrón contempla la interacción de una serie de objetos diferentes y del mecanismo de almacenamiento subyacente, como se detalla en la figura 2.

Interacción entre ContentProvider, otras clases y el almacenamiento

Figura 2: Interacción entre ContentProvider, otras clases y el almacenamiento

Nota: Por lo general, para acceder a un proveedor, tu aplicación debe solicitar permisos específicos en su archivo de manifiesto. Este patrón de desarrollo se describe más detalladamente en la sección Permisos del proveedor de contenido.

Uno de los proveedores integrados en la plataforma de Android es el diccionario del usuario, que guarda la ortografía de palabras no estándar que el usuario quiere conservar. En la tabla 1 se detalla cómo se verían los datos en la tabla de este proveedor:

Tabla 1: Ejemplo de tabla de diccionario del usuario

palabra ID de la app frecuencia configuración regional _ID
mapreduce user1 100 es-419 1
precompiler user14 200 fr_FR 2
applet user2 225 fr_CA 3
const user1 255 pt_BR 4
int user5 100 en_UK 5

En la tabla 1 cada fila representa una instancia de una palabra que podría no encontrarse en un diccionario estándar. Cada columna representa algunos datos para esa palabra, como la configuración regional en la que se encontró por primera vez. Los encabezados de las columnas son nombres que se guardan en el proveedor. A fin de consultar la configuración regional de una fila, debes consultar su columna locale. Para este proveedor la columna _ID cumple la función de una columna de "clave primaria" que el proveedor mantiene automáticamente.

Si quieres obtener una lista de palabras y sus configuraciones regionales del proveedor de diccionario del usuario, debes llamar a ContentResolver.query(). El método query() llama al método ContentProvider.query() definido por el proveedor de diccionario del usuario. Las siguientes líneas de código muestran una llamada ContentResolver.query():

Kotlin

// Queries the user dictionary and returns results
cursor = contentResolver.query(
        UserDictionary.Words.CONTENT_URI,   // The content URI of the words table
        projection,                        // The columns to return for each row
        selectionClause,                   // Selection criteria
        selectionArgs.toTypedArray(),      // Selection criteria
        sortOrder                          // The sort order for the returned rows
)

Java

// Queries the user dictionary and returns results
cursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,   // The content URI of the words table
    projection,                        // The columns to return for each row
    selectionClause,                   // Selection criteria
    selectionArgs,                     // Selection criteria
    sortOrder);                        // The sort order for the returned rows

En la tabla 2 se muestra la forma en que los argumentos para query(Uri,projection,selection,selectionArgs,sortOrder) coinciden con una instrucción SQL SELECT:

Tabla 2: Comparación de una consulta "query()" y una consulta de SQL

Argumento de query() Palabra clave/parámetro SELECT Notas
Uri FROM table_name Uri se asigna a la tabla del proveedor llamada table_name.
projection col,col,col,... projection es una matriz de columnas que se debe incluir para cada fila recuperada.
selection WHERE col = value selection especifica los criterios para seleccionar filas.
selectionArgs (Sin equivalente exacto; los argumentos de selección reemplazan a los marcadores de posición ? en la cláusula de selección).
sortOrder ORDER BY col,col,... sortOrder especifica el orden en que aparece la fila en el Cursor que se muestra.

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 (una ruta de acceso). Cuando llamas al método del cliente para acceder a una tabla del proveedor, el URI de contenido de la tabla es uno de los argumentos.

En las líneas de código anteriores, la constante CONTENT_URI incluye el URI de contenido de la tabla "words" del diccionario del usuario. El objeto ContentResolver analiza la autoridad del URI y la usa para "resolver" el proveedor comparando la autoridad con una tabla del sistema de proveedores conocidos. Después, el ContentResolver puede enviar los argumentos de la consulta al proveedor correcto.

El ContentProvider usa la parte de la ruta de acceso del URI de contenido para seleccionar la tabla a la que quiere acceder. Un proveedor generalmente tiene una ruta de acceso por cada tabla que expone.

En las líneas de código anteriores, el URI completo de la tabla "words" es:

content://user_dictionary/words

donde la string user_dictionary es la autoridad del proveedor y la string words es la ruta de acceso de la tabla. La string content:// (el esquema) siempre está presente e identifica este elemento como un URI de contenido.

Muchos proveedores te permiten acceder a una sola fila de una tabla anexando un valor de ID al final del URI. Por ejemplo, para recuperar una fila cuyo _ID es 4 del diccionario del usuario, puedes usar este URI de contenido:

Kotlin

val singleUri: Uri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, 4)

Java

Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);

Con frecuencia usas valores de ID cuando recuperas un conjunto de filas y, luego, quieres actualizar o borrar una de ellas.

Nota: Las clases Uri y Uri.Builder contienen métodos prácticos para construir objetos de URI bien formados a partir de strings. La clase ContentUris contiene métodos convenientes para anexar valores de ID a un URI. El fragmento anterior usa withAppendedId() para anexar un ID al URI de contenido UserDictionary.

Cómo recuperar datos del proveedor

En esta sección se describe cómo recuperar datos de un proveedor usando el proveedor de diccionario del usuario como ejemplo.

Para ser claros, los fragmentos de código de esta sección llaman a ContentResolver.query() en el "subproceso de IU". Sin embargo, para los códigos reales, debes realizar consultas de forma asíncrona en un subproceso independiente. Una manera de hacerlo es usar la clase CursorLoader, que se describe más detalladamente en la guía Cargadores. Asimismo, las líneas de código solo corresponden a fragmentos del código en cuestión; no muestran una aplicación completa.

Para recuperar datos de un proveedor, sigue estos pasos básicos:

  1. Solicita el permiso de acceso de lectura para el proveedor.
  2. Define el código que envía una consulta al proveedor.

Cómo solicitar el permiso de acceso de lectura

Para recuperar datos de un proveedor, tu aplicación necesita el "permiso de acceso de lectura" correspondiente al proveedor. No puedes solicitar este permiso en tiempo de ejecución, sino que debes especificar que lo necesitas en tu manifiesto usando el elemento <uses-permission> y el nombre exacto del permiso definido por el proveedor. Cuando especificas este elemento en tu manifiesto, estás "solicitando" el permiso para tu aplicación. Cuando los usuarios instalan tu aplicación, conceden de forma implícita el permiso solicitado.

Para buscar el nombre exacto del permiso de acceso de lectura del proveedor que estás usando, así como los nombres de otros permisos de acceso que usa el proveedor, consulta la documentación del proveedor.

La función de los permisos al acceder a los proveedores se describe más detalladamente en la sección Permisos del proveedor de contenido.

El proveedor de diccionario del usuario define el permiso android.permission.READ_USER_DICTIONARY en su archivo de manifiesto, de modo que una aplicación que quiera leer desde el proveedor debe solicitar este permiso.

Cómo construir la consulta

El siguiente paso en la recuperación de datos de un proveedor es construir una consulta. Este primer fragmento define algunas variables para acceder al proveedor de diccionario del usuario:

Kotlin

// A "projection" defines the columns that will be returned for each row
private val mProjection: Array<String> = arrayOf(
        UserDictionary.Words._ID,    // Contract class constant for the _ID column name
        UserDictionary.Words.WORD,   // Contract class constant for the word column name
        UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
)

// Defines a string to contain the selection clause
private var selectionClause: String? = null

// Declares an array to contain selection arguments
private lateinit var selectionArgs: Array<String>

Java

// A "projection" defines the columns that will be returned for each row
String[] mProjection =
{
    UserDictionary.Words._ID,    // Contract class constant for the _ID column name
    UserDictionary.Words.WORD,   // Contract class constant for the word column name
    UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
};

// Defines a string to contain the selection clause
String selectionClause = null;

// Initializes an array to contain selection arguments
String[] selectionArgs = {""};

El siguiente fragmento de código muestra cómo usar ContentResolver.query() con el proveedor de diccionario del usuario como ejemplo. Una consulta de un cliente del proveedor es similar a una consulta de SQL y contiene un conjunto de columnas para mostrar, un conjunto de criterios de selección y un criterio de ordenamiento.

El conjunto de columnas que la consulta debe mostrar se denomina proyección (la variable mProjection).

La expresión que especifica las filas para recuperar se divide en una cláusula de selección y argumentos de selección. La cláusula de selección es una combinación de expresiones lógicas y booleanas, nombres de columna y valores (la variable mSelectionClause). Si especificas el parámetro reemplazable ? en lugar de un valor, el método de consulta recupera el valor de la matriz de argumentos de selección (la variable mSelectionArgs).

En el siguiente fragmento de código, si el usuario no ingresa una palabra, la cláusula de selección se establece en null y la consulta muestra todas las palabras del proveedor. Si el usuario ingresa una palabra, la cláusula de selección se establece en UserDictionary.Words.WORD + " = ?" y el primer elemento de la matriz de argumentos de selección se establece en la palabra que ingresa el usuario.

Kotlin

/*
 * This declares String array to contain the selection arguments.
 */
private lateinit var selectionArgs: Array<String>

// Gets a word from the UI
searchString = searchWord.text.toString()

// Remember to insert code here to check for invalid or malicious input.

// If the word is the empty string, gets everything
selectionArgs = searchString?.takeIf { it.isNotEmpty() }?.let {
    selectionClause = "${UserDictionary.Words.WORD} = ?"
    arrayOf(it)
} ?: run {
    selectionClause = null
    emptyArray<String>()
}

// Does a query against the table and returns a Cursor object
mCursor = contentResolver.query(
        UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
        projection,                       // The columns to return for each row
        selectionClause,                  // Either null, or the word the user entered
        selectionArgs,                    // Either empty, or the string the user entered
        sortOrder                         // The sort order for the returned rows
)

// Some providers return null if an error occurs, others throw an exception
when (mCursor?.count) {
    null -> {
        /*
         * Insert code here to handle the error. Be sure not to use the cursor!
         * You may want to call android.util.Log.e() to log this error.
         *
         */
    }
    0 -> {
        /*
         * Insert code here to notify the user that the search was unsuccessful. This isn't
         * necessarily an error. You may want to offer the user the option to insert a new
         * row, or re-type the search term.
         */
    }
    else -> {
        // Insert code here to do something with the results
    }
}

Java

/*
 * This defines a one-element String array to contain the selection argument.
 */
String[] selectionArgs = {""};

// Gets a word from the UI
searchString = searchWord.getText().toString();

// Remember to insert code here to check for invalid or malicious input.

// If the word is the empty string, gets everything
if (TextUtils.isEmpty(searchString)) {
    // Setting the selection clause to null will return all words
    selectionClause = null;
    selectionArgs[0] = "";

} else {
    // Constructs a selection clause that matches the word that the user entered.
    selectionClause = UserDictionary.Words.WORD + " = ?";

    // Moves the user's input string to the selection arguments.
    selectionArgs[0] = searchString;

}

// Does a query against the table and returns a Cursor object
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
    projection,                       // The columns to return for each row
    selectionClause,                  // Either null, or the word the user entered
    selectionArgs,                    // Either empty, or the string the user entered
    sortOrder);                       // The sort order for the returned rows

// Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
    /*
     * Insert code here to handle the error. Be sure not to use the cursor! You may want to
     * call android.util.Log.e() to log this error.
     *
     */
// If the Cursor is empty, the provider found no matches
} else if (mCursor.getCount() < 1) {

    /*
     * Insert code here to notify the user that the search was unsuccessful. This isn't necessarily
     * an error. You may want to offer the user the option to insert a new row, or re-type the
     * search term.
     */

} else {
    // Insert code here to do something with the results

}

Esta consulta es similar a la instrucción de SQL:

SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

En esta instrucción SQL, se usan los nombres de columnas reales en lugar de constantes de clase de contratos.

Cómo generar protección contra entradas malintencionadas

Si los datos que administra el proveedor de contenido se encuentran en una base de datos SQL, incluir datos externos que no son de confianza en instrucciones de SQL sin procesar puede conducir a la inyección de SQL.

Considera esta cláusula de selección:

Kotlin

// Constructs a selection clause by concatenating the user's input to the column name
var selectionClause = "var = $mUserInput"

Java

// Constructs a selection clause by concatenating the user's input to the column name
String selectionClause = "var = " + userInput;

Si haces esto, le permites al usuario concatenar código SQL malintencionado en tu instrucción de SQL. Por ejemplo, el usuario podría ingresar "nothing; DROP TABLE *;" para mUserInput, que daría como resultado la cláusula de selección var = nothing; DROP TABLE *;. Como la cláusula de selección se aborda como una instrucción de SQL, esto podría provocar que el proveedor borre todas las tablas de la base de datos SQLite subyacente (a menos que el proveedor esté configurado para detectar intentos de inyección de SQL).

Para evitar este problema, usa una cláusula de selección que tenga ? como parámetro reemplazable y una matriz de argumentos de selección independiente. Cuando haces esto, los datos que ingresa el usuario se vinculan directamente con la consulta en lugar de interpretarse como parte de una instrucción de SQL. Dado que no se abordan como código SQL, los datos ingresados por el usuario no pueden inyectar SQL malintencionado. En lugar de recurrir a la concatenación para incluir los datos que ingresa el usuario, usa esta cláusula de selección:

Kotlin

// Constructs a selection clause with a replaceable parameter
var selectionClause = "var = ?"

Java

// Constructs a selection clause with a replaceable parameter
String selectionClause =  "var = ?";

Configura la matriz de argumentos de selección de esta manera:

Kotlin

// Defines a mutable list to contain the selection arguments
var selectionArgs: MutableList<String> = mutableListOf()

Java

// Defines an array to contain the selection arguments
String[] selectionArgs = {""};

Coloca un valor en la matriz de argumentos de selección de esta manera:

Kotlin

// Adds the user's input to the selection argument
selectionArgs += userInput

Java

// Sets the selection argument to the user's input
selectionArgs[0] = userInput;

Una cláusula de selección que use ? como parámetro reemplazable y una matriz de argumentos de selección son la forma preferida para especificar una selección, incluso cuando el proveedor no use una base de datos SQL.

Cómo visualizar resultados de una consulta

El método del cliente ContentResolver.query() siempre muestra un Cursor que contiene las columnas especificadas por la proyección de la consulta para las filas que coinciden con los criterios de selección de la consulta. Un objeto Cursor proporciona acceso de lectura aleatorio a las filas y columnas que contiene. Al usar métodos Cursor, puedes iterar por las filas en los resultados, determinar el tipo de datos de cada columna, obtener datos de una columna y examinar otras propiedades de los resultados. Algunas implementaciones Cursor actualizan automáticamente el objeto cuando cambian los datos del proveedor, o desencadenan métodos en un objeto observador cuando cambia Cursor, o ambos.

Nota: Un proveedor puede restringir el acceso a las columnas en función del origen del objeto que realiza la consulta. Por ejemplo, el proveedor de contactos restringe el acceso de los adaptadores de sincronización a algunas columnas, de modo que no las devolverá a una actividad o servicio.

Si no hay filas que coincidan con los criterios de selección, el proveedor muestra un objeto Cursor cuyo Cursor.getCount() es 0 (un cursor vacío).

Si ocurre un error interno, los resultados de la consulta dependen del proveedor específico. Puede decidir mostrar null o puede producir una Exception.

Como Cursor es una "lista" de filas, una buena manera de mostrar el contenido de un Cursor es vincularlo a una ListView a través de un SimpleCursorAdapter.

El siguiente fragmento continúa el código del fragmento anterior. Crea un objeto SimpleCursorAdapter que contenga el cursor Cursor recuperado por la consulta y establece este objeto como el adaptador para una ListView:

Kotlin

// Defines a list of columns to retrieve from the Cursor and load into an output row
val wordListColumns : Array<String> = arrayOf(
        UserDictionary.Words.WORD,      // Contract class constant containing the word column name
        UserDictionary.Words.LOCALE     // Contract class constant containing the locale column name
)

// Defines a list of View IDs that will receive the Cursor columns for each row
val wordListItems = intArrayOf(R.id.dictWord, R.id.locale)

// Creates a new SimpleCursorAdapter
cursorAdapter = SimpleCursorAdapter(
        applicationContext,             // The application's Context object
        R.layout.wordlistrow,           // A layout in XML for one row in the ListView
        mCursor,                        // The result from the query
        wordListColumns,               // A string array of column names in the cursor
        wordListItems,                 // An integer array of view IDs in the row layout
        0                               // Flags (usually none are needed)
)

// Sets the adapter for the ListView
wordList.setAdapter(cursorAdapter)

Java

// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] wordListColumns =
{
    UserDictionary.Words.WORD,   // Contract class constant containing the word column name
    UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name
};

// Defines a list of View IDs that will receive the Cursor columns for each row
int[] wordListItems = { R.id.dictWord, R.id.locale};

// Creates a new SimpleCursorAdapter
cursorAdapter = new SimpleCursorAdapter(
    getApplicationContext(),               // The application's Context object
    R.layout.wordlistrow,                  // A layout in XML for one row in the ListView
    mCursor,                               // The result from the query
    wordListColumns,                      // A string array of column names in the cursor
    wordListItems,                        // An integer array of view IDs in the row layout
    0);                                    // Flags (usually none are needed)

// Sets the adapter for the ListView
wordList.setAdapter(cursorAdapter);

Nota: Para respaldar una ListView con un Cursor, el cursor debe contener una columna llamada _ID. Por este motivo, la consulta que se mostró antes recupera la columna _ID para la tabla "words", aunque la ListView no la muestre. Esta restricción también explica por qué la mayoría de los proveedores tienen una columna _ID para cada una de sus tablas.

Cómo obtener datos de los resultados de una consulta

En lugar de simplemente mostrar resultados de una consulta, puedes usarlos para otras tareas. Por ejemplo, puedes recuperar palabras del diccionario del usuario y, luego, buscarlas en otros proveedores. Para hacerlo, debes iterar por las filas del Cursor:

Kotlin

/*
* Only executes if the cursor is valid. The User Dictionary Provider returns null if
* an internal error occurs. Other providers may throw an Exception instead of returning null.
*/
mCursor?.apply {
    // Determine the column index of the column named "word"
    val index: Int = getColumnIndex(UserDictionary.Words.WORD)

    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you will get an
     * exception.
     */
    while (moveToNext()) {
        // Gets the value from the column.
        newWord = getString(index)

        // Insert code here to process the retrieved word.

        ...

        // end of while loop
    }
}

Java

// Determine the column index of the column named "word"
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);

/*
 * Only executes if the cursor is valid. The User Dictionary Provider returns null if
 * an internal error occurs. Other providers may throw an Exception instead of returning null.
 */

if (mCursor != null) {
    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you will get an
     * exception.
     */
    while (mCursor.moveToNext()) {

        // Gets the value from the column.
        newWord = mCursor.getString(index);

        // Insert code here to process the retrieved word.

        ...

        // end of while loop
    }
} else {

    // Insert code here to report an error if the cursor is null or the provider threw an exception.
}

Las implementaciones de Cursor contienen varios métodos GET para recuperar diferentes tipos de datos del objeto. Por ejemplo, el fragmento anterior usa getString(). También tienen un método getType(), el cual muestra un valor que indica el tipo de datos de la columna.

Permisos del proveedor de contenido

La aplicación de un proveedor puede especificar permisos que otras aplicaciones deben tener para poder acceder a los datos del proveedor. Estos permisos garantizan que el usuario sepa a qué datos intentará acceder una aplicación. En función de los requisitos del proveedor, otras aplicaciones solicitan los permisos que necesitan para acceder al proveedor. Los usuarios finales ven los permisos solicitados cuando instalan la aplicación.

Si la aplicación de un proveedor no especifica ningún permiso, otras aplicaciones no podrán acceder a los datos del proveedor. No obstante, los componentes de la aplicación del proveedor siempre tienen acceso pleno de lectura y escritura, independientemente de los permisos especificados.

Como se indicó antes, el proveedor de diccionario del usuario requiere el permiso android.permission.READ_USER_DICTIONARY para recuperar datos de él. El proveedor tiene el permiso android.permission.WRITE_USER_DICTIONARY independiente para insertar, actualizar o borrar datos.

A fin de obtener los permisos necesarios para acceder a un proveedor, una aplicación los solicita con un elemento <uses-permission> en su archivo de manifiesto. Cuando el Administrador de paquetes de Android instala la aplicación, el usuario debe aprobar todos los permisos que la aplicación solicita. Si el usuario aprueba todos, el Administrador de paquetes continúa con la instalación; si el usuario no los aprueba, se anula la instalación.

El siguiente elemento <uses-permission> solicita acceso de lectura al proveedor de diccionario del usuario:

<uses-permission android:name="android.permission.READ_USER_DICTIONARY">

El impacto de los permisos en el acceso al proveedor se explica más detalladamente en la guía Seguridad y permisos.

Cómo insertar, actualizar y borrar datos

De la misma manera que recuperas datos de un proveedor, puedes usar la interacción entre un cliente del proveedor y el ContentProvider del proveedor para modificar datos. Debes llamar a un método de ContentResolver con argumentos que se pasan al método correspondiente de ContentProvider. El proveedor y el cliente del proveedor administran automáticamente la seguridad y la comunicación dentro del proceso.

Cómo insertar datos

Para insertar datos en un proveedor, debes llamar al método ContentResolver.insert(). Este método inserta una nueva fila en el proveedor y devuelve un URI de contenido para esa fila. Este fragmento de código muestra cómo insertar una nueva palabra en el proveedor de diccionario del usuario:

Kotlin

// Defines a new Uri object that receives the result of the insertion
lateinit var newUri: Uri

...

// Defines an object to contain the new values to insert
val newValues = ContentValues().apply {
    /*
     * Sets the values of each column and inserts the word. The arguments to the "put"
     * method are "column name" and "value"
     */
    put(UserDictionary.Words.APP_ID, "example.user")
    put(UserDictionary.Words.LOCALE, "en_US")
    put(UserDictionary.Words.WORD, "insert")
    put(UserDictionary.Words.FREQUENCY, "100")

}

newUri = contentResolver.insert(
        UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
        newValues                          // the values to insert
)

Java

// Defines a new Uri object that receives the result of the insertion
Uri newUri;

...

// Defines an object to contain the new values to insert
ContentValues newValues = new ContentValues();

/*
 * Sets the values of each column and inserts the word. The arguments to the "put"
 * method are "column name" and "value"
 */
newValues.put(UserDictionary.Words.APP_ID, "example.user");
newValues.put(UserDictionary.Words.LOCALE, "en_US");
newValues.put(UserDictionary.Words.WORD, "insert");
newValues.put(UserDictionary.Words.FREQUENCY, "100");

newUri = getContentResolver().insert(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    newValues                          // the values to insert
);

Los datos para la nueva fila están incluidos en un solo objeto ContentValues, de forma similar a la de un cursor de una fila. No es necesario que las columnas de este objeto tengan el mismo tipo de datos y, si no quieres especificar un valor, puedes establecer una columna en null usando ContentValues.putNull().

El fragmento no agrega la columna _ID, ya que esta columna se mantiene automáticamente. El proveedor asigna un valor único de _ID a cada fila que se agrega. Por lo general, los proveedores usan este valor como la clave primaria de la tabla.

El URI de contenido que se muestra en newUri identifica la fila nueva con el siguiente formato:

content://user_dictionary/words/<id_value>

<id_value> es el contenido de _ID para la nueva fila. La mayoría de los proveedores detectan esta forma de URI de contenido automáticamente y, luego, realizan la operación solicitada en esa fila específica.

Para obtener el valor de _ID del Uri que se muestra, llama a ContentUris.parseId().

Cómo actualizar datos

Para actualizar una fila, debes usar un objeto ContentValues con los valores actualizados, tal como lo haces con una inserción, y con criterios de selección como lo haces con una consulta. El método de cliente que usas es ContentResolver.update(). Solo debes agregar valores al objeto ContentValues para las columnas que estés actualizando. Si quieres borrar el contenido de una columna, establece el valor en null.

El siguiente fragmento de código cambia todas las filas cuya configuración regional tenga el idioma "en" por una configuración regional null. El valor devuelto es el número de filas que se actualizaron:

Kotlin

// Defines an object to contain the updated values
val updateValues = ContentValues().apply {
    /*
     * Sets the updated value and updates the selected words.
     */
    putNull(UserDictionary.Words.LOCALE)
}

// Defines selection criteria for the rows you want to update
val selectionClause: String = UserDictionary.Words.LOCALE + "LIKE ?"
val selectionArgs: Array<String> = arrayOf("en_%")

// Defines a variable to contain the number of updated rows
var rowsUpdated: Int = 0

...

rowsUpdated = contentResolver.update(
        UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
        updateValues,                      // the columns to update
        selectionClause,                   // the column to select on
        selectionArgs                      // the value to compare to
)

Java

// Defines an object to contain the updated values
ContentValues updateValues = new ContentValues();

// Defines selection criteria for the rows you want to update
String selectionClause = UserDictionary.Words.LOCALE +  " LIKE ?";
String[] selectionArgs = {"en_%"};

// Defines a variable to contain the number of updated rows
int rowsUpdated = 0;

...

/*
 * Sets the updated value and updates the selected words.
 */
updateValues.putNull(UserDictionary.Words.LOCALE);

rowsUpdated = getContentResolver().update(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    updateValues,                      // the columns to update
    selectionClause,                   // the column to select on
    selectionArgs                      // the value to compare to
);

Cuando llamas a ContentResolver.update(), también debes depurar las entradas del usuario. Para obtener más información, consulta la sección Protección contra entradas malintencionadas.

Cómo borrar datos

Borrar filas es similar a recuperar datos de una fila: debes especificar criterios de selección para las filas que quieras borrar y el método de cliente muestra el número de filas borradas. El siguiente fragmento borra las filas cuyo ID de aplicación coincide con "user". El método muestra el número de filas borradas.

Kotlin

// Defines selection criteria for the rows you want to delete
val selectionClause = "${UserDictionary.Words.LOCALE} LIKE ?"
val selectionArgs: Array<String> = arrayOf("user")

// Defines a variable to contain the number of rows deleted
var rowsDeleted: Int = 0

...

// Deletes the words that match the selection criteria
rowsDeleted = contentResolver.delete(
        UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
        selectionClause,                   // the column to select on
        selectionArgs                      // the value to compare to
)

Java

// Defines selection criteria for the rows you want to delete
String selectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] selectionArgs = {"user"};

// Defines a variable to contain the number of rows deleted
int rowsDeleted = 0;

...

// Deletes the words that match the selection criteria
rowsDeleted = getContentResolver().delete(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    selectionClause,                   // the column to select on
    selectionArgs                      // the value to compare to
);

Cuando llamas a ContentResolver.delete(), también debes depurar las entradas del usuario. Para obtener más información, consulta la sección Protección contra entradas malintencionadas.

Tipos de datos del proveedor

Los proveedores de contenido pueden ofrecer muchos tipos de datos diferentes. El proveedor de diccionario del usuario solo ofrece texto, pero en general los proveedores también pueden ofrecer los siguientes formatos:

  • número entero
  • entero largo (largo)
  • punto flotante
  • punto flotante largo (doble)

Otro tipo de datos que los proveedores usan con frecuencia es el objeto binario grande (BLOB) implementado como una matriz de 64 KB. Si quieres ver los tipos de datos disponibles, puedes ver los métodos GET de la clase Cursor.

El tipo de datos para cada columna de un proveedor suele indicarse en su documentación. Los tipos de datos para el proveedor de diccionario del usuario se indican en la documentación de referencia para su clase de contratos UserDictionary.Words (las clases de contratos se describen en la sección Clases de contratos). También puedes determinar el tipo de datos llamando a Cursor.getType().

Los proveedores también mantienen información del tipo de datos MIME para cada URI de contenido que definen. Usa la información del tipo de MIME para averiguar si tu aplicación puede administrar datos que ofrece el proveedor o seleccionar un tipo de administración en función del tipo de MIME. Generalmente necesitas el tipo de MIME cuando trabajas con un proveedor que contiene estructuras de datos o archivos complejos. Por ejemplo, la tabla ContactsContract.Data del proveedor de contactos usa tipos de MIME para etiquetar el tipo de datos de contacto guardado en cada fila. Para obtener el tipo de MIME correspondiente a un URI de contenido, llama a ContentResolver.getType().

La sección Referencia a tipos de MIME describe la sintaxis de los tipos de MIME estándar y personalizado.

Formas alternativas de acceso al proveedor

En el desarrollo de la aplicación, hay tres formas alternativas de acceso al proveedor que son importantes.

El acceso por lotes y las modificaciones mediante intents se describen en las siguientes secciones.

Acceso por lotes

El acceso por lotes a un proveedor es útil para insertar una gran cantidad de filas o para insertar filas en diferentes tablas en la misma llamada de método, o bien, en general, para realizar un conjunto de operaciones entre límites del proceso como una transacción (una operación atómica).

Para acceder a un proveedor en "modo por lotes", puedes crear una matriz de objetos ContentProviderOperation y, luego, enviarlos a un proveedor de contenido con ContentResolver.applyBatch(). Debes pasar la autoridad del proveedor de contenido a este método en lugar de hacerlo a un URI de contenido específico. Esto permite que cada objeto ContentProviderOperation de la matriz funcione de acuerdo con una tabla diferente. Una llamada a ContentResolver.applyBatch() muestra una matriz de resultados.

La descripción de la clase de contratos ContactsContract.RawContacts incluye un fragmento de código que demuestra la inserción por lotes. La aplicación de muestra Administrador de contactos contiene un ejemplo de acceso por lotes en su archivo fuente ContactAdder.java.

Acceso a datos mediante intents

Las intents pueden proporcionar acceso indirecto a un proveedor de contenido. Puedes permitir que el usuario acceda a datos en el proveedor aunque tu aplicación no tenga permisos de acceso, ya sea mediante la obtención de una intent de resultado desde una aplicación que tenga permisos o mediante la activación de una aplicación que tenga permisos y la habilitación para que el usuario pueda realizar tareas en ella.

Cómo obtener acceso con permisos temporales

Puedes acceder a datos en un proveedor de contenido aunque no tengas los permisos de acceso adecuados, ya sea mediante el envío de una intent a una aplicación que tenga permisos o mediante la obtención de una intent de resultado que contenga permisos de "URI". Se trata de permisos para un URI de contenido específico que duran hasta que finaliza la actividad que los recibe. La aplicación que tiene permisos permanentes concede permisos temporales estableciendo un marcador en la intent de resultado:

Nota: Estos indicadores no proporcionan acceso general de escritura ni de lectura al proveedor cuya autoridad está incluida en el URI de contenido. El acceso es solo para el URI.

Un proveedor define permisos de URI para URI de contenido en su manifiesto usando el atributo android:grantUriPermission del elemento <provider>, así como el elemento secundario <grant-uri-permission> del elemento <provider>. El mecanismo de permisos del URI se explica más detalladamente en la guía Descripción general de los permisos.

Por ejemplo, puedes recuperar datos para un contacto en el proveedor de contactos aunque no tengas el permiso READ_CONTACTS. Podrías querer hacer esto en una aplicación que envíe saludos electrónicos a un contacto en su cumpleaños. En lugar de solicitar READ_CONTACTS, que te permite acceder a todos los contactos del usuario y a toda su información, prefieres permitirle al usuario controlar los contactos que usa tu aplicación. Para hacerlo, usa el siguiente proceso:

  1. Tu aplicación envía una intent que contiene la acción ACTION_PICK y el tipo de MIME "contacts" CONTENT_ITEM_TYPE mediante el método startActivityForResult().
  2. Como esta intent coincide con el filtro de intents para la actividad "selection" de la aplicación People, la actividad pasará a primer plano.
  3. En la actividad de selección, el usuario elige un contacto para actualizar. Cuando esto ocurre, la actividad de selección llama a setResult(resultcode, intent) para configurar una intent que regrese a tu aplicación. La intent tiene el URI de contenido del contacto que seleccionó el usuario y las marcas "extras" FLAG_GRANT_READ_URI_PERMISSION. Estas marcas otorgan permiso de URI para que tu aplicación pueda leer datos del contacto al que hace referencia el URI de contenido. Después, la actividad de selección llama a finish() para devolverle el control a tu aplicación.
  4. Tu actividad vuelve a segundo plano y el sistema llama al método onActivityResult() de tu actividad. Este método recibe la intent de resultado que creó la actividad de selección en la aplicación People.
  5. Con el URI de contenido de la intent de resultado, puedes leer los datos del contacto desde el proveedor de contactos, aunque no hayas solicitado un permiso de acceso de lectura permanente al proveedor en tu manifiesto. Después puedes obtener la información de cumpleaños del contacto o su dirección de correo electrónico y enviarle un saludo electrónico.

Cómo usar otra aplicación

Una forma sencilla de permitir que el usuario modifique datos para los que no tienes permisos de acceso es activar una aplicación que tenga permisos y habilitar al usuario para que realice operaciones allí.

Por ejemplo, la aplicación de Calendario admite una intent ACTION_INSERT que te permite activar la IU de inserción de la aplicación. Puedes pasar datos "extras" en esa intent, que se usan para autocompletar la IU. Como los eventos periódicos tienen una sintaxis compleja, el método preferido para insertar eventos en un proveedor de calendario es activar la aplicación Calendario con una ACTION_INSERT y luego permitirle al usuario que inserte el evento allí.

Cómo usar una app de ayuda para visualizar datos

Si tu aplicación tiene permisos de acceso, quizá quieras usar una intent para mostrar datos en otra aplicación. Por ejemplo, la aplicación de Calendario admite una intent ACTION_VIEW que muestra una fecha o un evento específicos. Esto te permite mostrar información del calendario sin tener que crear tu propia IU. Para obtener más información acerca de esta función, consulta la guía Proveedor de calendario.

No es necesario que la aplicación a la que envías la intent esté asociada con el proveedor. Por ejemplo, puedes recuperar un contacto del proveedor de contactos y, luego, enviar una intent ACTION_VIEW con el URI de contenido de la imagen del contacto a un visor de imágenes.

Clases de contratos

Una clase de contratos define constantes que ayudan a las apps a trabajar con URI de contenido, nombres de columnas, acciones de intents y otras funciones de un proveedor de contenido. Las clases de contratos no están incluidas automáticamente en un proveedor; el desarrollador del proveedor debe definirlas y, después, permitir que estén disponibles para otros desarrolladores. Muchos de los proveedores incluidos en la plataforma de Android tienen clases de contratos correspondientes en el paquete android.provider.

Por ejemplo, el proveedor de diccionario del usuario dispone de una clase de contratos UserDictionary que tiene constantes de URI de contenido y nombre de columna. El URI de contenido de la tabla "words" se define en la constante UserDictionary.Words.CONTENT_URI. La clase UserDictionary.Words también tiene constantes de nombre de columnas, que se usan en los fragmentos de ejemplo de esta guía. Por ejemplo, una proyección de consulta se puede definir de la siguiente manera:

Kotlin

val projection : Array<String> = arrayOf(
        UserDictionary.Words._ID,
        UserDictionary.Words.WORD,
        UserDictionary.Words.LOCALE
)

Java

String[] projection =
{
    UserDictionary.Words._ID,
    UserDictionary.Words.WORD,
    UserDictionary.Words.LOCALE
};

Otra clase de contratos es ContactsContract para el proveedor de contactos. La documentación de referencia para esta clase incluye fragmentos de código de ejemplo. Una de sus subclases, ContactsContract.Intents.Insert, es una clase de contratos que contiene constantes para intents y datos de intents.

Referencia a tipos de MIME

Los proveedores de contenido pueden devolver tipos de medios MIME estándar o strings de tipos de MIME personalizadas, o ambos.

Los tipos de MIME tienen este formato:

type/subtype

Por ejemplo, el tipo de MIME conocido text/html tiene el tipo text y el subtipo html. Si el proveedor muestra este tipo para un URI, significa que una consulta que use ese URI mostrará texto que contenga etiquetas HTML.

Las strings de tipo de MIME personalizadas, también llamadas tipos de MIME "específicos del proveedor", tienen valores de tipo y subtipo más complejos. El valor de tipo es siempre

vnd.android.cursor.dir

para múltiples filas, o

vnd.android.cursor.item

para una sola fila.

El subtipo es específico del proveedor. Los proveedores integrados a Android suelen tener un subtipo simple. Por ejemplo, cuando la aplicación de Contactos crea una fila para un número de teléfono, establece el siguiente tipo de MIME en la fila:

vnd.android.cursor.item/phone_v2

Ten en cuenta que el valor del subtipo es simplemente phone_v2.

Otros desarrolladores de proveedores pueden crear sus propios patrones de subtipos en función de la autoridad y los nombres de tablas del proveedor. Por ejemplo, piensa en un proveedor que contiene horarios de trenes. La autoridad del proveedor es com.example.trains y contiene las tablas Line1, Line2 y Line3. En respuesta al URI de contenido

content://com.example.trains/Line1

para la tabla Line1, el proveedor devuelve el tipo de MIME

vnd.android.cursor.dir/vnd.example.line1

En respuesta al URI de contenido

content://com.example.trains/Line2/5

para la fila 5 de la tabla Line2, el proveedor devuelve el tipo de MIME

vnd.android.cursor.item/vnd.example.line2

La mayoría de los proveedores de contenido definen constantes de clase de contratos para los tipos de MIME que usan. Por ejemplo, la clase de contratos ContactsContract.RawContacts del proveedor de contactos define la constante CONTENT_ITEM_TYPE para el tipo de MIME de una sola fila de contacto sin procesar.

Los URI de contenido para filas individuales se describen en la sección URI de contenido.