Поставщики содержимого – основы

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

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

Обзор

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

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

word app_id frequency locale _ID
воронка user1 100 ru_RU 1
кашель user23 200 ru_RU 2
applet user2 225 fr_CA 3
int user72 100 en_UK 4

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

Примечание: первичный ключ для поставщика не является обязательным, как не является обязательным и имя “_ID”. Однако, если вы хотите отображать данные поставщика в компоненте ListView, один из столбцов должен называться “_ID”.

Доступ к поставщику

Приложение получает данные из поставщика содержимого с помощью клиентского объекта типа ContentResolver. Клиентский объект имеет методы, вызывающие идентичные методы объекта поставщика, который представлен конкретным подклассом ContentProvider. Методы класса ContentResolver предоставляют базовые CRUD (create, retrieve, update и delete) операции для постоянного хранилища.

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

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

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

В следующей таблице показано как аргументы метод query(Uri, projection, selection, selectionArgs, sortOrder) соответствуют SELECT запросу 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 типа content

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

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

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

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

где user_dictionary это authority, а words это path(путь) таблицы. Строка content:// (схема URI) используется всегда и означает, что URI указан для данных.

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

Вы часто будете использовать такой подход, когда необходимо изменить или удалить запись в таблице.

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

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

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

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

Для получения данных из поставщика необходимо проделать два основных шага:

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

Получение прав на чтение

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

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

Пользовательский словарь содержит разрешение android.permission.READ_USER_DICTIONARY.

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

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

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

Набор столбцов, которые должен возвращать запрос называется проекция(projection).

Выражение для отбора записей разделено на две части: условие и аргументы. Условие это набор логических выражений, названий столбцов и значений. Если вы указали символ ? вместо значения, он будет заменен значением из массива аргументов.

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

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

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

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

Рассмотрим следующий запрос:

Такой код позволяет создать вредоносный SQL запрос. Например, может ввести “nothing; DROP TABLE *;”. После обработки запроса с такой строкой, все данные поставщика будут стерты.

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

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

Отображение результата

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

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

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

При возникновении ошибки результат зависит от конкретного поставщика. Одни возвращают null, другие выбрасывают исключение.

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

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

Получение данных из результата запроса

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

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

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

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

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

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

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

Вот пример запроса разрешения на чтение для пользовательского словаря:

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

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

Вставка данных

Для вставки данных в поставщик, вы можете использовать метод ContentResolver.insert(). Метод добавляет новую запись и возвращает URI этой записи. Пример:

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

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

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

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

Для получения значения _ID из данного URI, используйте метод ContentUris.parseId().

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

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

Следующий пример меняет все строки, в которых указан язык “en” и устанавливает для языка значение null. Возвращаются номера строк, которые были обновлены:

Удаление данных

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

Типы данных поставщика содержимого

Поставщики часто содержат различные типы данных:

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

Также поставщики часто используют бинарный тип – Binary Large OBject (BLOB) – реализованный в виде байтового массива размером 64Кб. Доступные типы данных можете посмотреть в документации к классу Cursor.

Тип данных каждого столбца обычно описан в документации к поставщику. Типы данных для пользовательского словаря описаны в документации к классу UserDictionary.Words.

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

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

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

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

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

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

Для доступа в пакетном режиме, необходимо создать массив объектов типа ContentProviderOperaion и передать его в поставщик содержимого с помощью метода ContentResolver.applyBatch(). В метод передается только authority поставщика, а не весь URI. Это позволяет для каждого объекта массива использовать различные таблицы. Метод applyBatch() возвращает массив с результатом.

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

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

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

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

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

Получение временного доступа

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

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

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

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

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

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

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

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

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

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

В качестве примера можем привести контрактный класс для поставщика контактов ContactsContract.

MIME типы

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

MIME типы имеют формат:

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

Собственные MIME типы, также известные как “vendor-specific”, имеют более сложный тип и подтип, вроде следующих:

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

Здесь подтип имеет значение phone_v2

Другие разработчики могут создавать и собственные шаблоны подтипов, основанные на authority и именах таблиц. Например, рассмотрим поставщик, который содержит расписание поездов. Authority поставщика com.example.trains, а имена таблиц Line1, Line2, Line3:

Для таблицы Line1 поставщик может возвращать MIME тип:

А для пятой строки таблицы Line2 с URI

поставщик может вернуть MIME тип

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

Добавить комментарий