Основные сведения о поставщике контента

Поставщик контента управляет доступом к центральному хранилищу данных. Поставщик является частью приложения для Android, которое часто предоставляет собственный пользовательский интерфейс для работы с данными. Однако поставщики контента в основном используются другими приложениями, которые получают доступ к поставщику с помощью клиентского объекта поставщика. Вместе поставщики и клиенты поставщиков предлагают согласованный стандартный интерфейс для данных, который также обрабатывает межпроцессное взаимодействие и безопасный доступ к данным.

Обычно поставщики контента используются в двух сценариях: для реализации кода, который позволяет получить доступ к существующему поставщику контента в другом приложении, или для создания нового поставщика контента в вашем приложении, чтобы обмениваться данными с другими приложениями.

На этой странице описаны основы работы с существующими поставщиками контента. Чтобы узнать, как реализовать поставщиков контента в собственных приложениях, ознакомьтесь со статьей Создание поставщика контента.

В этой статье рассматриваются следующие темы:

  • Как работают поставщики контента.
  • API, который вы используете для получения данных от поставщика контента.
  • API, который вы используете для вставки, обновления или удаления данных в поставщике контента.
  • Другие функции API, упрощающие работу с поставщиками.

Обзор

Поставщик контента представляет данные внешним приложениям в виде одной или нескольких таблиц, похожих на таблицы в реляционной базе данных. Строка представляет собой экземпляр определенного типа данных, которые собирает поставщик, а каждый столбец в строке – отдельный фрагмент данных, собранных для экземпляра.

Поставщик контента координирует доступ к уровню хранения данных в приложении для ряда различных API и компонентов. Как показано на рисунке 1, к ним относятся:

  • Предоставление доступа к данным вашего приложения другим приложениям.
  • Отправка данных в виджет
  • Возврат собственных подсказок при поиске для приложения через фреймворк поиска с помощью SearchRecentSuggestionsProvider.
  • Синхронизация данных приложения с сервером с помощью реализации AbstractThreadedSyncAdapter.
  • Загрузка данных в интерфейс с помощью CursorLoader
Связь между поставщиком контента и другими компонентами.

Рисунок 1. Отношения между поставщиком контента и другими компонентами.

Как получить доступ к поставщику

Чтобы получить доступ к данным в поставщике контента, используйте объект ContentResolver в Context приложения для связи с поставщиком в качестве клиента. Объект ContentResolver взаимодействует с объектом поставщика – экземпляром класса, реализующего ContentProvider.

Объект поставщика получает запросы данных от клиентов, выполняет запрошенное действие и возвращает результаты. Этот объект содержит методы, которые вызывают методы с такими же названиями в объекте поставщика, экземпляре одного из конкретных подклассов ContentProvider. Методы ContentResolver обеспечивают базовые функции CRUD (создание, чтение, обновление и удаление) для постоянного хранилища.

Обычно для доступа к ContentProvider из интерфейса используется CursorLoader, который выполняет асинхронный запрос в фоновом режиме. Activity или Fragment в интерфейсе вызывает CursorLoader для запроса, который, в свою очередь, получает ContentProvider с помощью ContentResolver.

Это позволяет пользователю продолжать работу с интерфейсом во время выполнения запроса. Этот шаблон включает взаимодействие нескольких различных объектов, а также базовый механизм хранения, как показано на рисунке 2.

Взаимодействие между ContentProvider, другими классами и хранилищем.

Рисунок 2. Взаимодействие между ContentProvider, другими классами и хранилищем.

Примечание. Чтобы получить доступ к поставщику, приложению обычно требуется запросить определенные разрешения в файле манифеста. Этот шаблон разработки более подробно описан в разделе Разрешения поставщика контента.

Одним из встроенных поставщиков на платформе Android является поставщик пользовательского словаря, который хранит нестандартные слова, которые пользователь хочет сохранить. В таблице 1 показано, как могут выглядеть данные в таблице этого поставщика.

Таблица 1. Пример таблицы пользовательского словаря.

слово идентификатор приложения частота региональные настройки _ID
mapreduce user1 100 ru_RU 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

В таблице 1 каждая строка представляет собой экземпляр слова, не найденного в стандартном словаре. Каждый столбец представляет собой фрагмент данных для этого слова, например, региональные настройки, в которых оно было впервые обнаружено. Заголовки столбцов – это названия столбцов, которые хранятся у поставщика. Например, чтобы сослаться на региональные настройки строки, нужно сослаться на ее столбец locale. Для этого поставщика столбец _ID служит столбцом первичного ключа, который поставщик поддерживает автоматически.

Чтобы получить список слов и их языковых стандартов от поставщика пользовательского словаря, вызовите ContentResolver.query(). Метод query() вызывает метод ContentProvider.query(), определенный поставщиком пользовательского словаря. Ниже приведен пример вызова ContentResolver.query():

Kotlin

// Queries the UserDictionary 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 UserDictionary 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

В таблице 2 показано, как аргументы query(Uri,projection,selection,selectionArgs,sortOrder) соответствуют оператору SQL SELECT.

Таблица 2. Сравнение query() и SQL-запроса.

query() аргумент Ключевое слово/параметр SELECT Примечания
Uri FROM table_name Uri сопоставляется с таблицей table_name в поставщике.
projection col,col,col,... projection – массив столбцов, включенных в каждую полученную строку.
selection WHERE col = value selection задает критерии выбора строк.
selectionArgs Точного эквивалента нет. Аргументы выбора заменяют плейсхолдеры ? в условии выбора.
sortOrder ORDER BY col,col,... sortOrder определяет порядок, в котором строки появляются в возвращенном Cursor.

URI контента

URI контента – это URI, который идентифицирует данные в поставщике. URI контента включают символическое имя всего поставщика (его источник) и имя, указывающее на таблицу (путь). Когда вы вызываете метод клиента для доступа к таблице в поставщике, URI контента для таблицы является одним из аргументов.

В приведенных выше строках кода константа CONTENT_URI содержит URI контента таблицы Words поставщика пользовательского словаря. Объект ContentResolver анализирует URI и использует его для определения поставщика, сравнивая URI с системной таблицей известных поставщиков. Затем ContentResolver может передать аргументы запроса нужному поставщику.

ContentProvider использует часть пути URI контента, чтобы выбрать таблицу для доступа. Обычно у поставщика есть путь для каждой таблицы, которую он предоставляет.

В предыдущих строках кода полный URI для таблицы Words выглядит следующим образом:

content://user_dictionary/words
  • Строка content:// – это схема, которая всегда присутствует и идентифицирует URI контента.
  • Строка user_dictionary – это полномочия поставщика.
  • Строка words – это путь к таблице.

Многие поставщики позволяют получить доступ к одной строке в таблице, добавив значение идентификатора в конец URI. Например, чтобы получить строку, у которой _ID равен 4, из поставщика пользовательского словаря, можно использовать следующий URI контента:

Kotlin

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

Java

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

Идентификаторы часто используются, когда вы извлекаете набор строк, а затем хотите обновить или удалить одну из них.

Примечание. Классы Uri и Uri.Builder содержат удобные методы для создания правильно сформированных объектов URI из строк. Класс ContentUris содержит удобные методы для добавления значений идентификаторов в URI. В предыдущем фрагменте используется withAppendedId(), чтобы добавить идентификатор к URI контента поставщика пользовательского словаря.

Получение данных от поставщика

В этом разделе описывается, как получить данные от поставщика, на примере поставщика пользовательского словаря.

Для простоты во фрагментах кода в этом разделе метод ContentResolver.query() вызывается в потоке UI. Однако в реальном коде запросы выполняются асинхронно в отдельном потоке. Вы можете использовать класс CursorLoader, который подробно описан в руководстве Загрузчики. Кроме того, строки кода представляют собой лишь фрагменты. Они не дают полного представления о приложении.

Чтобы получить данные от поставщика, выполните следующие действия:

  1. Запросите у поставщика разрешение на доступ для чтения.
  2. Определите код, который отправляет запрос поставщику.

Запросить разрешение на доступ для чтения

Чтобы получить данные от поставщика, вашему приложению требуется разрешение на чтение. Вы не можете запрашивать это разрешение во время выполнения. Вместо этого вам нужно указать в манифесте, что вам требуется это разрешение, используя элемент <uses-permission> и точное название разрешения, определенное поставщиком.

Указывая этот элемент в манифесте, вы запрашиваете разрешение для приложения. Когда пользователи устанавливают ваше приложение, они неявно предоставляют это разрешение.

Чтобы узнать точное название разрешения на чтение для используемого поставщика, а также названия других разрешений на доступ, используемых поставщиком, обратитесь к документации поставщика.

Роль разрешений в доступе к поставщикам более подробно описана в разделе Разрешения поставщика контента.

Поставщик пользовательского словаря определяет разрешение android.permission.READ_USER_DICTIONARY в своем файле манифеста, поэтому приложение, которое хочет считывать данные из поставщика, должно запросить это разрешение.

Создание запроса

Следующий шаг в получении данных от поставщика – создание запроса. В следующем фрагменте кода определены некоторые переменные для доступа к поставщику пользовательского словаря:

Kotlin

// A "projection" defines the columns that are 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 are 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 = {""};

В следующем фрагменте показано, как использовать ContentResolver.query() на примере поставщика пользовательского словаря. Запрос клиента поставщика похож на SQL-запрос и содержит набор столбцов, которые нужно вернуть, набор критериев выбора и порядок сортировки.

Набор столбцов, возвращаемых запросом, называется проекцией, а переменная – mProjection.

Выражение, указывающее, какие строки нужно получить, разделено на условие выбора и аргументы выбора. Предложение выбора – это комбинация логических и булевых выражений, названий столбцов и значений. Переменная – mSelectionClause. Если вместо значения указать заменяемый параметр ?, метод запроса получит значение из массива аргументов выбора, который является переменной mSelectionArgs.

В следующем фрагменте кода, если пользователь не вводит слово, для условия выбора задается значение null и запрос возвращает все слова в поставщике. Если пользователь вводит слово, для условия выбора задается значение UserDictionary.Words.WORD + " = ?", а для первого элемента массива аргументов выбора – введенное пользователем слово.

Kotlin

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

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

// 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 might want to call android.util.Log.e() to log this error.
         */
    }
    0 -> {
        /*
         * Insert code here to notify the user that the search is unsuccessful. This isn't
         * necessarily an error. You might 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 returns 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 can
     * 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 is unsuccessful. This isn't necessarily
     * an error. You can 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

}

Этот запрос аналогичен следующей инструкции SQL:

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

В этой инструкции SQL используются фактические названия столбцов, а не константы класса контракта.

Защита от вредоносных входных данных

Если данные, которыми управляет поставщик контента, находятся в базе данных SQL, включение внешних ненадежных данных в необработанные инструкции SQL может привести к внедрению SQL-кода.

Рассмотрим следующий пункт выбора:

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;

В противном случае пользователь может добавить вредоносный SQL-код в вашу инструкцию SQL. Например, пользователь может ввести "nothing; DROP TABLE *;" для mUserInput, что приведет к созданию условия выбора var = nothing; DROP TABLE *;.

Поскольку предложение выбора обрабатывается как инструкция SQL, это может привести к тому, что поставщик удалит все таблицы в базовой базе данных SQLite, если он не настроен на перехват попыток внедрения SQL-кода.

Чтобы избежать этой проблемы, используйте предложение выбора, в котором в качестве заменяемого параметра выступает ?, а также указан отдельный массив аргументов выбора. Таким образом, пользовательский ввод привязывается непосредственно к запросу, а не интерпретируется как часть инструкции SQL. Поскольку введенные пользователем данные не обрабатываются как SQL-код, они не могут содержать вредоносный SQL-код. Вместо объединения для включения пользовательского ввода используйте следующее условие выбора:

Kotlin

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

Java

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

Настройте массив аргументов выбора следующим образом:

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 = {""};

Добавьте значение в массив аргументов выбора, как показано ниже.

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;

Предложение выбора, в котором в качестве заменяемого параметра выступает ?, а также указан массив аргументов выбора, – это предпочтительный способ задать выбор, даже если поставщик не основан на базе данных SQL.

Показать результаты запроса

Клиентский метод ContentResolver.query() всегда возвращает объект Cursor, содержащий столбцы, которые указаны в проекции запроса для строк, соответствующих критериям выбора запроса. Объект Cursor обеспечивает произвольный доступ для чтения к содержащимся в нем строкам и столбцам.

С помощью методов Cursor можно перебирать строки в результатах, определять тип данных каждого столбца, извлекать данные из столбца и проверять другие свойства результатов.

Некоторые реализации Cursor автоматически обновляют объект при изменении данных поставщика, запускают методы в объекте наблюдателя при изменении Cursor или делают и то, и другое.

Примечание. Поставщик может ограничить доступ к столбцам в зависимости от характера объекта, выполняющего запрос. Например, поставщик контактов ограничивает доступ к некоторым столбцам для адаптеров синхронизации, поэтому он не возвращает их в действие или сервис.

Если ни одна строка не соответствует критериям выбора, поставщик возвращает объект Cursor, для которого Cursor.getCount() равен 0, то есть пустой курсор.

Если возникает внутренняя ошибка, результаты запроса зависят от конкретного поставщика. Он может возвращать null или вызывать Exception.

Поскольку Cursor представляет собой список строк, его Cursor содержимое удобно отображать, связав его с ListView с помощью SimpleCursorAdapter.

Приведенный ниже фрагмент продолжает код из предыдущего фрагмента. Он создает объект SimpleCursorAdapter, содержащий Cursor, полученный в результате запроса, и устанавливает этот объект в качестве адаптера для 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 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 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);

Примечание. Чтобы использовать ListView с Cursor, курсор должен содержать столбец с названием _ID. Поэтому приведенный выше запрос извлекает столбец _ID для таблицы Words, хотя в ListView он не отображается. Это ограничение также объясняет, почему у большинства поставщиков есть столбец _ID для каждой из их таблиц.

Как получить данные из результатов запроса

Результаты запросов можно не только просматривать, но и использовать для других задач. Например, вы можете получить варианты написания от поставщика пользовательского словаря, а затем найти их у других поставщиков. Для этого нужно перебрать строки в Cursor, как показано в примере ниже.

Kotlin

/*
* Only executes if the cursor is valid. The User Dictionary Provider returns null if
* an internal error occurs. Other providers might 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 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 might 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 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
}

Реализации Cursor содержат несколько методов get для получения разных типов данных из объекта. Например, в предыдущем фрагменте используется getString(). У них также есть метод getType(), который возвращает значение, указывающее на тип данных столбца.

Освобождение ресурсов результатов запроса

Объекты Cursor, которые больше не нужны, необходимо закрывать, чтобы связанные с ними ресурсы освобождались быстрее. Это можно сделать, вызвав close(), с помощью оператора try-with-resources на языке программирования Java или функции use() на языке программирования Kotlin.

Разрешения поставщика контента

Приложение поставщика может указывать разрешения, которые должны иметь другие приложения для доступа к данным поставщика. Эти разрешения позволяют пользователю узнать, к каким данным приложение пытается получить доступ. Другие приложения запрашивают разрешения, необходимые для доступа к поставщику, в соответствии с его требованиями. Конечные пользователи видят запрашиваемые разрешения при установке приложения.

Если в приложении поставщика не указаны разрешения, то у других приложений не будет доступа к его данным, если только поставщик не экспортирован. Кроме того, компоненты в приложении поставщика всегда имеют полный доступ для чтения и записи, независимо от указанных разрешений.

Для получения данных из поставщика пользовательского словаря требуется разрешение android.permission.READ_USER_DICTIONARY. У поставщика есть отдельное разрешение android.permission.WRITE_USER_DICTIONARY на вставку, обновление и удаление данных.

Чтобы получить разрешения, необходимые для доступа к поставщику, приложение запрашивает их с помощью элемента <uses-permission> в файле манифеста. При установке приложения с помощью Android Package Manager пользователь должен одобрить все разрешения, запрашиваемые приложением. Если пользователь дает согласие, Package Manager продолжает установку. Если пользователь не предоставит разрешения, Package Manager остановит установку.

В следующем примере элемент <uses-permission> запрашивает доступ на чтение к поставщику пользовательского словаря:

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

Подробнее о том, как разрешения влияют на доступ к поставщику, рассказывается в советах по безопасности.

Вставка, обновление и удаление данных

Изменение данных происходит так же, как и их получение, – за счет взаимодействия между клиентом поставщика и ContentProvider поставщика. Вы вызываете метод ContentResolver с аргументами, которые передаются в соответствующий метод ContentProvider. Поставщик и клиент поставщика автоматически обеспечивают безопасность и межпроцессное взаимодействие.

Вставьте данные.

Чтобы вставить данные в поставщик, вызовите метод ContentResolver.insert(). Этот метод вставляет новую строку в поставщик и возвращает URI контента для этой строки. В приведенном ниже фрагменте кода показано, как вставить новое слово в словарь пользователя.

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 UserDictionary 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 UserDictionary content URI
    newValues                           // The values to insert
);

Данные для новой строки помещаются в один объект ContentValues, который по форме похож на однострочный курсор. Столбцы в этом объекте не обязательно должны иметь один и тот же тип данных. Если вы не хотите указывать значение, то можете задать для столбца значение null с помощью ContentValues.putNull().

В предыдущем фрагменте кода не добавляется столбец _ID, поскольку он поддерживается автоматически. Поставщик присваивает уникальное значение _ID каждой добавленной строке. Поставщики обычно используют это значение в качестве первичного ключа таблицы.

URI контента, возвращаемый в newUri, идентифицирует добавленную строку в следующем формате:

content://user_dictionary/words/<id_value>

<id_value> – содержимое _ID для новой строки. Большинство поставщиков могут автоматически распознавать такую форму URI контента и выполнять запрошенную операцию с определенной строкой.

Чтобы получить значение _ID из возвращенного Uri, вызовите ContentUris.parseId().

Обновление данных

Чтобы обновить строку, используйте объект ContentValues с обновленными значениями, как при вставке, и критерии выбора, как при запросе. Используется метод клиента ContentResolver.update(). Вам нужно добавить значения в объект ContentValues только для тех столбцов, которые вы обновляете. Если вы хотите очистить содержимое столбца, задайте значение null.

В следующем фрагменте кода во всех строках, где указан язык "en", он заменяется на null. Возвращаемое значение – это количество обновленных строк.

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 UserDictionary 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 UserDictionary content URI
    updateValues,                      // The columns to update
    selectionClause,                   // The column to select on
    selectionArgs                      // The value to compare to
);

Очищайте пользовательский ввод при вызове ContentResolver.update(). Подробнее об этом рассказывается в разделе Защита от вредоносных входных данных.

Удалить данные

Удаление строк выполняется аналогично получению данных строк. Вы указываете критерии выбора строк, которые нужно удалить, а метод клиента возвращает количество удаленных строк. Следующий фрагмент удаляет строки, идентификатор приложения в которых совпадает с "user". Метод возвращает количество удаленных строк.

Kotlin

// Defines selection criteria for the rows you want to delete
val selectionClause = "${UserDictionary.Words.APP_ID} 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 UserDictionary 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 UserDictionary content URI
    selectionClause,                   // The column to select on
    selectionArgs                      // The value to compare to
);

Очищайте пользовательский ввод при вызове ContentResolver.delete(). Подробнее об этом рассказывается в разделе Защита от вредоносных входных данных.

Типы данных поставщика

Поставщики контента могут предлагать множество разных типов данных. Поставщик пользовательского словаря предлагает только текст, но поставщики также могут предлагать следующие форматы:

  • Целое число
  • длинное целое число (long);
  • число с плавающей запятой
  • длинное число с плавающей запятой (double)

Другой тип данных, который часто используют поставщики, – это большой двоичный объект (BLOB), реализованный в виде массива байтов размером 64 КБ. Доступные типы данных можно посмотреть в методах get класса Cursor.

Тип данных для каждого столбца поставщика обычно указывается в его документации. Типы данных для User Dictionary Provider перечислены в справочной документации для его класса контракта UserDictionary.Words. Классы контрактов описаны в разделе Классы контрактов. Вы также можете определить тип данных, вызвав Cursor.getType().

Поставщики также хранят информацию о MIME-типе данных для каждого определенного ими URI контента. Вы можете использовать информацию о MIME-типе, чтобы узнать, может ли ваше приложение обрабатывать данные, которые предлагает поставщик, или выбрать тип обработки на основе MIME-типа. Обычно MIME-тип требуется, если вы работаете с поставщиком, который содержит сложные структуры данных или файлы.

Например, таблица ContactsContract.Data в поставщике контактов использует MIME-типы, чтобы помечать тип данных контакта, хранящихся в каждой строке. Чтобы получить MIME-тип, соответствующий URI контента, вызовите ContentResolver.getType().

В разделе Справочник по MIME-типам описан синтаксис стандартных и специальных MIME-типов.

Альтернативные формы доступа поставщика

При разработке приложений важны три альтернативные формы доступа к поставщику:

  • Пакетный доступ. Вы можете создать пакет вызовов доступа с помощью методов класса ContentProviderOperation, а затем применить их с помощью ContentResolver.applyBatch().
  • Асинхронные запросы. Выполняйте запросы в отдельном потоке. Вы можете использовать объект CursorLoader. Примеры в руководстве по загрузчикам показывают, как это сделать.
  • Доступ к данным с помощью намерений. Хотя вы не можете отправить намерение непосредственно поставщику, вы можете отправить его в приложение поставщика, которое обычно лучше всего подходит для изменения данных поставщика.

Пакетный доступ и изменение с помощью намерений описаны в следующих разделах.

Пакетный доступ

Пакетный доступ к поставщику полезен для вставки большого количества строк, для вставки строк в несколько таблиц в одном вызове метода и в целом для выполнения набора операций через границы процесса в виде транзакции, называемой атомарной операцией.

Чтобы получить доступ к поставщику в пакетном режиме, создайте массив объектов ContentProviderOperation и отправьте их поставщику контента с помощью ContentResolver.applyBatch(). В этот метод передается авторитет поставщика контента, а не конкретный URI контента.

Это позволяет каждому объекту ContentProviderOperation в массиве работать с отдельной таблицей. Вызов ContentResolver.applyBatch() возвращает массив результатов.

В описании класса контракта ContactsContract.RawContacts есть фрагмент кода, демонстрирующий пакетную вставку.

Доступ к данным с помощью намерений

Намерения могут предоставлять косвенный доступ к поставщику контента. Вы можете предоставить пользователю доступ к данным поставщика, даже если у вашего приложения нет разрешений на доступ. Для этого нужно получить намерение результата от приложения, у которого есть разрешения, или активировать приложение с разрешениями и позволить пользователю работать в нем.

Как получить доступ с помощью временных разрешений

Вы можете получить доступ к данным в поставщике контента, даже если у вас нет необходимых разрешений на доступ. Для этого отправьте намерение в приложение, у которого есть разрешения, и получите обратно намерение результата, содержащее разрешения URI. Это разрешения для определенного URI контента, которые действуют до завершения действия, которое их получает. Приложение, у которого есть постоянные разрешения, предоставляет временные разрешения, устанавливая флаг в намерении результата:

Примечание. Эти флаги не предоставляют общий доступ на чтение или запись поставщику, чьи полномочия содержатся в URI контента. Доступ предоставляется только к самому URI.

При отправке URI контента в другое приложение включите хотя бы один из этих флагов. Флаги предоставляют следующие возможности любому приложению, которое получает намерение и предназначено для Android 11 (уровень API 30) или более поздней версии:

  • Чтение или запись данных, которые представляет URI контента, в зависимости от флага, включенного в намерение.
  • Получите видимость пакета в приложении, содержащем поставщика контента, который соответствует центру URI. Приложение, которое отправляет намерение, и приложение, которое содержит поставщика контента, могут быть разными.

Поставщик определяет разрешения URI для URI контента в своем манифесте, используя атрибут android:grantUriPermissions элемента <provider>, а также дочерний элемент <grant-uri-permission> элемента <provider>. Подробнее о механизме разрешений URI рассказывается в руководстве Разрешения в Android.

Например, вы можете получить данные контакта из поставщика контактов, даже если у вас нет разрешения READ_CONTACTS. Это может быть полезно, например, в приложении, которое отправляет контактам электронные поздравления с днем рождения. Вместо того чтобы запрашивать разрешение READ_CONTACTS, которое дает доступ ко всем контактам пользователя и всей информации о них, позвольте пользователю самому выбрать, какие контакты будут доступны вашему приложению. Для этого выполните следующие действия:

  1. В приложении отправьте намерение, содержащее действие ACTION_PICK и MIME-тип контактов CONTENT_ITEM_TYPE, используя метод startActivityForResult().
  2. Поскольку этот интент соответствует фильтру интентов для действия "выбор" в приложении "Контакты", это действие выходит на передний план.
  3. В действии выбора пользователь выбирает контакт для обновления. В этом случае действие выбора вызывает setResult(resultcode, intent), чтобы настроить намерение, которое будет возвращено вашему приложению. Намерение содержит URI контента выбранного пользователем контакта и дополнительные флаги FLAG_GRANT_READ_URI_PERMISSION. Эти флаги предоставляют приложению разрешение URI на чтение данных контакта, на который указывает URI контента. Затем действие выбора вызывает finish(), чтобы вернуть управление вашему приложению.
  4. Объект activity возвращается на передний план, и система вызывает его метод onActivityResult(). Этот метод получает намерение результата, созданное действием выбора в приложении "Контакты".
  5. С помощью URI контента из намерения результата вы можете прочитать данные контакта от поставщика контактов, даже если вы не запрашивали разрешение на постоянный доступ для чтения к поставщику в манифесте. Затем вы можете получить информацию о дне рождения или адрес электронной почты контакта и отправить электронное поздравление.

Использовать другое приложение

Ещё один способ позволить пользователю изменить данные, к которым у вас нет доступа, – активировать приложение, у которого есть разрешения, и позволить пользователю выполнить работу в нем.

Например, приложение "Календарь" принимает намерение ACTION_INSERT, которое позволяет активировать интерфейс вставки приложения. В этом интенте можно передавать дополнительные данные, которые приложение использует для автоматического заполнения пользовательского интерфейса. Поскольку повторяющиеся мероприятия имеют сложный синтаксис, предпочтительным способом вставки мероприятий в поставщик календаря является активация приложения "Календарь" с помощью ACTION_INSERT, чтобы пользователь мог вставить мероприятие в нем.

Показ данных с помощью вспомогательного приложения

Если у вашего приложения есть разрешения на доступ, вы все равно можете использовать намерение для показа данных в другом приложении. Например, приложение "Календарь" принимает намерение ACTION_VIEW, которое показывает определенную дату или событие. Это позволяет отображать информацию календаря без необходимости создавать собственный пользовательский интерфейс. Подробнее об этой функции рассказывается в обзоре поставщика календаря.

Приложение, которому вы отправляете намерение, не обязательно должно быть связано с поставщиком. Например, вы можете получить контакт от поставщика контактов, а затем отправить намерение ACTION_VIEW, содержащее URI контента для изображения контакта, в средство просмотра изображений.

Классы контрактов

Класс контракта определяет константы, которые помогают приложениям работать с URI контента, названиями столбцов, действиями intent и другими функциями поставщика контента. Классы контрактов не включаются в поставщик автоматически. Разработчик поставщика должен определить их, а затем сделать доступными для других разработчиков. Многие поставщики, включенные в платформу Android, имеют соответствующие классы контрактов в пакете android.provider.

Например, у поставщика пользовательского словаря есть класс контракта UserDictionary, содержащий константы URI контента и названия столбцов. URI контента для таблицы Words определяется в константе UserDictionary.Words.CONTENT_URI. Класс UserDictionary.Words также содержит константы названий столбцов, которые используются в примерах кода в этом руководстве. Например, проекция запроса может быть определена следующим образом:

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
};

Ещё один класс контракта – ContactsContract для поставщика контактов. Справочная документация по этому классу содержит примеры фрагментов кода. Один из его подклассов, ContactsContract.Intents.Insert, – это класс контракта, содержащий константы для намерений и их данных.

Справочник по MIME-типам

Поставщики контента могут возвращать стандартные MIME-типы, строки специальных MIME-типов или и то, и другое.

MIME-типы имеют следующий формат:

type/subtype

Например, у известного MIME-типа text/html есть тип text и подтип html. Если поставщик возвращает этот тип для URI, это означает, что запрос с использованием этого URI возвращает текст, содержащий HTML-теги.

Строки специальных MIME-типов, также называемых зависящими от поставщика, имеют более сложные значения type и subtype. Для нескольких строк значение типа всегда следующее:

vnd.android.cursor.dir

Для одной строки значение типа всегда следующее:

vnd.android.cursor.item

Значение subtype зависит от поставщика. Встроенные поставщики Android обычно имеют простой подтип. Например, когда приложение для управления контактами создает строку для номера телефона, оно задает для нее следующий MIME-тип:

vnd.android.cursor.item/phone_v2

Значение подтипа – phone_v2.

Разработчики других поставщиков могут создавать собственные шаблоны подтипов на основе названия поставщика и таблиц. Например, у вас есть поставщик, который содержит расписание поездов. Авторитет поставщика – com.example.trains, и он содержит таблицы Line1, Line2 и Line3. В ответ на следующий URI контента для таблицы Line1:

content://com.example.trains/Line1

Поставщик возвращает следующий MIME-тип:

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

В ответ на следующий URI контента для строки 5 в таблице Line2:

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

Поставщик возвращает следующий MIME-тип:

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

Большинство поставщиков контента определяют константы класса контракта для используемых ими MIME-типов. Например, класс контракта поставщика контактов ContactsContract.RawContacts определяет константу CONTENT_ITEM_TYPE для MIME-типа одной строки необработанного контакта.

URI контента для отдельных строк описаны в разделе URI контента.