Поставщик контента управляет доступом к центральному хранилищу данных. Поставщик — это часть приложения Android, которое часто предоставляет собственный пользовательский интерфейс для работы с данными. Однако поставщики контента в основном используются другими приложениями, которые обращаются к поставщику с помощью клиентского объекта поставщика. Вместе поставщики и клиенты поставщиков предлагают согласованный стандартный интерфейс для данных, который также обеспечивает межпроцессное взаимодействие и безопасный доступ к данным.
Обычно вы работаете с поставщиками контента по одному из двух сценариев: реализация кода для доступа к существующему поставщику контента в другом приложении или создание нового поставщика контента в вашем приложении для обмена данными с другими приложениями.
На этой странице описаны основы работы с существующими поставщиками контента. Чтобы узнать о реализации поставщиков контента в ваших собственных приложениях, см. Создание поставщика контента .
В этой теме описывается следующее:
- Как работают контент-провайдеры.
- API, который вы используете для получения данных от поставщика контента.
- API, который вы используете для вставки, обновления или удаления данных в поставщике контента.
- Другие функции API, облегчающие работу с провайдерами.
Обзор
Поставщик контента представляет данные внешним приложениям в виде одной или нескольких таблиц, аналогичных таблицам в реляционной базе данных. Строка представляет собой экземпляр некоторого типа данных, которые собирает поставщик, а каждый столбец в строке представляет отдельный фрагмент данных, собранных для экземпляра.
Поставщик контента координирует доступ к уровню хранения данных в вашем приложении для ряда различных API и компонентов. Как показано на рисунке 1, они включают в себя следующее:
- Предоставление доступа к данным вашего приложения другим приложениям
- Отправка данных в виджет
- Возврат пользовательских поисковых предложений для вашего приложения через платформу поиска с помощью
SearchRecentSuggestionsProvider
- Синхронизация данных приложения с вашим сервером с помощью реализации
AbstractThreadedSyncAdapter
- Загрузка данных в ваш пользовательский интерфейс с помощью
CursorLoader
Доступ к провайдеру
Если вы хотите получить доступ к данным в поставщике контента, вы используете объект ContentResolver
в Context
вашего приложения для связи с поставщиком в качестве клиента. Объект ContentResolver
взаимодействует с объектом поставщика — экземпляром класса, реализующего ContentProvider
.
Объект поставщика получает запросы данных от клиентов, выполняет запрошенное действие и возвращает результаты. Этот объект имеет методы, которые вызывают методы с идентичными именами в объекте поставщика, экземпляре одного из конкретных подклассов ContentProvider
. Методы ContentResolver
предоставляют базовые функции CRUD (создание, получение, обновление и удаление) постоянного хранилища.
Распространенный шаблон доступа к ContentProvider
из вашего пользовательского интерфейса использует CursorLoader
для запуска асинхронного запроса в фоновом режиме. Activity
или Fragment
в вашем пользовательском интерфейсе вызывает CursorLoader
для запроса, который, в свою очередь, получает ContentProvider
с помощью ContentResolver
.
Это позволяет пользовательскому интерфейсу оставаться доступным пользователю во время выполнения запроса. Этот шаблон предполагает взаимодействие ряда различных объектов, а также базового механизма хранения, как показано на рисунке 2.
Примечание. Чтобы получить доступ к поставщику, ваше приложение обычно должно запрашивать определенные разрешения в своем файле манифеста. Более подробно этот шаблон разработки описан в разделе «Разрешения контент-провайдера» .
Одним из встроенных поставщиков платформы Android является поставщик пользовательского словаря, который хранит нестандартные слова, которые пользователь хочет сохранить. Таблица 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:
аргумент 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
, который более подробно описан в руководстве по загрузчикам . Кроме того, строки кода представляют собой только фрагменты. Они не показывают полное приложение.
Чтобы получить данные от поставщика, выполните следующие основные шаги:
- Запросить разрешение на чтение для провайдера.
- Определите код, который отправляет запрос поставщику.
Запросить разрешение на чтение
Чтобы получить данные от поставщика, вашему приложению необходимо разрешение на чтение для поставщика. Вы не можете запросить это разрешение во время выполнения. Вместо этого вам необходимо указать, что вам нужно это разрешение в вашем манифесте, используя элемент <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 контента, которые действуют до тех пор, пока не завершится действие, которое их получило. Приложение, имеющее постоянные разрешения, предоставляет временные разрешения, устанавливая флаг в намерении результата:
- Разрешение на чтение:
FLAG_GRANT_READ_URI_PERMISSION
. - Разрешение на запись:
FLAG_GRANT_WRITE_URI_PERMISSION
.
Примечание. Эти флаги не предоставляют общий доступ на чтение или запись поставщику, чьи полномочия содержатся в URI контента. Доступ предоставляется только для самого URI.
Когда вы отправляете URI контента в другое приложение, включите хотя бы один из этих флагов. Флаги предоставляют следующие возможности любому приложению, которое получает намерение и предназначено для Android 11 (уровень API 30) или выше:
- Чтение или запись данных, которые представляет URI контента, в зависимости от флага, включенного в намерение.
- Получите видимость пакета в приложении, содержащем поставщика контента, который соответствует авторитету URI. Приложение, отправляющее намерение, и приложение, содержащее поставщика контента, могут быть двумя разными приложениями.
Поставщик определяет разрешения URI для URI контента в своем манифесте, используя атрибут android:grantUriPermissions
элемента <provider>
, а также дочерний элемент <grant-uri-permission>
элемента <provider>
. Механизм разрешений URI более подробно описан в руководстве «Разрешения для Android» .
Например, вы можете получить данные о контакте в поставщике контактов, даже если у вас нет разрешения READ_CONTACTS
. Возможно, вы захотите сделать это в приложении, которое отправляет электронное поздравление контакту в день его рождения. Вместо запроса READ_CONTACTS
, который дает вам доступ ко всем контактам пользователя и всей его информации, позвольте пользователю контролировать, какие контакты использует ваше приложение. Для этого используйте следующий процесс:
- В своем приложении отправьте намерение, содержащее действие
ACTION_PICK
и MIME-тип «контактов»CONTENT_ITEM_TYPE
, используя методstartActivityForResult()
. - Поскольку это намерение соответствует фильтру намерений для действия «выбор» приложения «Люди», это действие выходит на передний план.
- В процессе выбора пользователь выбирает контакт для обновления. Когда это происходит, действие выбора вызывает
setResult(resultcode, intent)
чтобы настроить намерение вернуть ваше приложение. Намерение содержит URI содержимого контакта, выбранного пользователем, и флаги «дополнительно»FLAG_GRANT_READ_URI_PERMISSION
. Эти флаги предоставляют вашему приложению разрешение URI на чтение данных для контакта, на который указывает URI контента. Затем действие выбора вызываетfinish()
, чтобы вернуть управление вашему приложению. - Ваша активность возвращается на передний план, и система вызывает метод
onActivityResult()
вашей активности. Этот метод получает намерение результата, созданное действием выбора в приложении «Люди». - С помощью 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 контента .