Основы контент-провайдера

Поставщик контента управляет доступом к центральному хранилищу данных. Поставщик — это часть приложения 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. Пример таблицы пользовательского словаря.

слово идентификатор приложения частота локаль _ИДЕНТИФИКАТОР
mapreduce пользователь1 100 ru_US 1
precompiler пользователь14 200 fr_FR 2
applet пользователь2 225 fr_CA 3
const пользователь1 255 pt_BR 4
int пользователь5 100 ru_UK 5

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

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

Котлин

// 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
)

Ява

// 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() ВЫБРАТЬ ключевое слово/параметр Примечания
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 контента включают символическое имя всего провайдера (его полномочия ) и имя, указывающее на таблицу ( path ). Когда вы вызываете клиентский метод для доступа к таблице в поставщике, URI содержимого таблицы является одним из аргументов.

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

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

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

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

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

Котлин

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

Ява

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

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

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

Получить данные от провайдера

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

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

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

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

Запросить разрешение на чтение

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

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

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

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

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

Создайте запрос

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

Котлин

// 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>

Ява

// 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 + " = ?" и первый элемент массива аргументов выбора устанавливается на слово, которое вводит пользователь.

Котлин

/*
 * 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
    }
}

Ява

/*
 * 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.

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

Котлин

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

Ява

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

Если вы сделаете это, вы позволите пользователю потенциально объединить вредоносный SQL с вашим оператором SQL. Например, пользователь может ввести «ничего; DROP TABLE *;» для mUserInput , что приводит к предложению выбора var = nothing; DROP TABLE *; .

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

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

Котлин

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

Ява

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

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

Котлин

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

Ява

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

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

Котлин

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

Ява

// 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 .

Котлин

// 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)

Ява

// 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 , как показано в следующем примере:

Котлин

/*
* 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
    }
}

Ява

// 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 устанавливает приложение, пользователь должен утвердить все разрешения, запрашиваемые приложением. Если пользователь одобряет их, диспетчер пакетов продолжает установку. Если пользователь не одобряет их, диспетчер пакетов останавливает установку.

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

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

Влияние разрешений на доступ провайдера более подробно объясняется в разделе Советы по безопасности .

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

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

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

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

Котлин

// 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
)

Ява

// 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 . Возвращаемое значение — это количество обновленных строк.

Котлин

// 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
)

Ява

// 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" . Метод возвращает количество удаленных строк.

Котлин

// 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
)

Ява

// 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() . Чтобы узнать больше об этом, прочтите раздел Защита от вредоносного ввода .

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

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

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

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

Тип данных для каждого столбца поставщика обычно указан в его документации. Типы данных для поставщика пользовательского словаря перечислены в справочной документации по его классу контракта 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. Ваша активность возвращается на передний план, и система вызывает метод onActivityResult() вашей активности. Этот метод получает намерение результата, созданное действием выбора в приложении «Люди».
  5. С помощью URI содержимого из намерения результата вы можете прочитать данные контакта у поставщика контактов, даже если вы не запрашивали у поставщика разрешение на постоянное чтение в своем манифесте. Затем вы можете получить информацию о дне рождения или адресе электронной почты контакта, а затем отправить электронное поздравление.

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

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

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

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

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

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

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

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

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

Котлин

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

Ява

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, также называемые типами 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 контента .