Препарирование ContentProvider

Теги: Java, Android, ContentProvider

What?

ContentProvider – это класс, предоставляющий унифицированный интерфейс для доступа к данным приложения. Этот класс позволяет вам использовать единый источник данных в вашем приложении.

Sources:

Задуман, как способ предоставлять данные вашего приложения для сторонних приложений, + для реализации поиска среди данных вашего приложения с использованием search suggestions (подсказки при вводе слов для поиска).

На практике часто используется для создания единого источника данных внутри приложения (В противовес заявлению в оф.докумментации: “You don’t need to develop your own provider if you don’t intend to share your data with other applications.”). При этом получаем связки: ContentProvider & SQLite, ContentProvider & CursorLoader, ContentProvider & Loader…

В пользу только лишь внутреннего использования (польза для внешнего очевидна) ContentProvider’a говорит следующее:

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

  2. Не нужно управлять жизненным циклом объекта для доступа к данным (к примеру, экземпляра SQLiteDatabase). Ведь в случае прямого использования таких объектов возникает немало вопросов: где хранить этот объект? Когда закрывать базу данных? Когда уничтожать этот объект? ContentProvider позволяет получать доступ к данным из любого места, где доступен контекст приложения (экземпляр Context).

  3. ContentProvider полностью соответствует концепциям REST (о которых мы говорили в этих заметках)

Немного о последнем пункте. Итак, соответствие REST:

  • Для каждой сущности ContentProvider’a есть свой URI.
  • Регистрация в манифесте ContentProvider’a выглядит следующим образом (пока без пояснения и др. возможных атрибутов):
<provider
   android:name=".peopleprovider"
   android:authorities="io.github.ziginsider.peopleprovider"
   android:exported="false"/>
  • рекомендуется использовать в качестве authority имя пакета с префиксом поясняющим суть провайдера, например для нашего списка контактов можно было бы применить: io.github.ziginsider.peopleprovider
  • URI образуется по схеме:
<prefix>://<authority>/<data_type>/<id>
  • У ContentProvider’a приложения всегда определен базовый URI. Он складывается из префикса “content://” и authorities. Таким образом, для нашего примера базовый URI = “content://io.github.ziginsider.peopleprovider”
  • Далее, допустим мы хотим создать группу, в которой будет храниться информация о писателях (в рамках базы данных это будет таблица). Тогда URI для этой группы должно выглядеть следующим образом: “content://io.github.ziginsider.peopleprovider/writers”. Если мы обратимся к данным в ContentProvider по этому URI, то получим все экземпляры, сохраненные в этой группе.
  • И наконец, если нам нужно URI для отдельного объекта, то оно будет выглядеть следующим образом: “content://io.github.ziginsider.peopleprovider/writers/4”. Где 4 – это номер добавленного экземпляра.

Создание своего СontentProvider’a

На практике нам необходимо наследоваться от класса ContentProvider и реализовать следующие методы:

public boolean onCreate()
  • инициализирует ContentProvider. Провайдер будет создан как только вы обратитесь к нему с помощью ContentResolver’a. Возвращает true, если ContentProvider создан успешно. В общем случае, не рекомендуется делать в onCreate() ContentProvider’a длительных операций т.к. это может повесить onCreate() Application (??)
public Cursor query(@NonNull Uri uri,     // The content URI of the words table (FROM table_name)
            String[] projection,          // The columns to return for each row (col, col, col, ...)
            String selection,             // Selection criteria (WHERE col = value)
            String[] selectionArgs,       // Selection criteria args (instead '?')
            String sortOrder)             // The sort order for the returned rows (ORDER BY col, col, ...)
  • Возвращает объект Cursor по полученному URI. URI парсится, согласно заданным нами правилам. В случае использования в качестве источника данных БД SQLite извлекает данные из БД, и возвращает их в виде Cursor. (соответствует HTTP методу GET).
public Uri insert(@NonNull Uri uri, 
            ContentValues values)
  • добавляет новые данные, возвращает URI новой записи. (соответствует HTTP методу POST)
public final int bulkInsert(@NonNull Uri uri, 
            @NonNull ContentValues[] values)
  • добавляет массив элементов (не является обязательным для реализации в отличии от всех остальных методов)
public final int update(@NonNull Uri uri, 
            ContentValues values, 
            String selection, 
            String[] selectionArgs)
  • обновляет строки в хранилище данных согласно заданным условиям. (в терминологии HTTP методов – PUT или PATCH)
public final int delete(@NonNull Uri uri, 
            String selection, 
            String[] selectionArgs)
  • удаляет данные. ( единственный метод ContentProvider, HTTP аналог которого имеет такое же название)
public final String getType(@NonNull Uri uri)
  • возвращает MIME-тип для заданной content URI. Обычно этот метод используют для сопоставления имени таблицы и URI, чтобы потом выполнить запрос к базе данных.

Следует помнить, что все перечисленные методы кроме onCreate() могут выполняться одновременно в нескольких потоках, и поэтому должны быть потоко-безопасными (thread-safe).

Допустим, мы создали свой ContentProvider, добавили его в манифесте и теперь можем обращаться к нему в качестве интерфейса для работы с данными. Но здесь есть небольшая тонкость – мы создавали объект ContentProvider, но обращаться к данным нужно через объект ContentResolver, который можно получить через метод getContentResolver в классе Context:

Cursor cursor = mContext.getContentResolver().query(table.getUri(), 
            null, 
            where.where(), 
            where.whereArgs(), 
            where.limit());

Манифест

Регистрация ContentProvider’a в манифесте под тегом <provider>. Подробная иформация здесь.

синтаксис:

<provider android:authorities="list"
          android:directBootAware=["true" | "false"]
          android:enabled=["true" | "false"]
          android:exported=["true" | "false"]
          android:grantUriPermissions=["true" | "false"]
          android:icon="drawable resource"
          android:initOrder="integer"
          android:label="string resource"
          android:multiprocess=["true" | "false"]
          android:name="string"
          android:permission="string"
          android:process="string"
          android:readPermission="string"
          android:syncable=["true" | "false"]
          android:writePermission="string" >
    . . .
</provider>

Атрибут android:exporter задает возможность использования нашего ContentProvider’a сторонними приложениями. До версии Android 16 по умолчанию был true, в версиях >= 16 по умолчанию false. Поэтому необходимо обращать внимание на состояние этого атрибута.

Атрибут android:authorities задает ключ по которому мы будем обращаться к нашему ContentProvider’у. Стоит обратить внимание на его уникальность. Установка приложений с одним и тем же значением authorities на одно устройство выдаст ошибку.

Атрибуты android:permission, android:readPermission, android:writePermission - задают ограничения на использование нашего ContentProvider’a сторонними приложениями (на доступ к ContentProvider’у внутри нашего приложения эти атрибуты не влияют). Атрибут android:readPermission и android:writePermission имеют приоритет над атрибутом android:permission, и, соответственно, отвечают за доступ к функции query() и фунциям insert(), bulkInsert(), update(), delete() нашего ContentProvider’a для стороннего приложения.

Например, в манифесте записано следующее:

<provider android:name="bbct.android.common.BaseballCardProvider"
              android:label="@string/provider_name"
              android:authorities="bbct.android.baseballcard"
              android:readPermission="bbct.android.lite.permission.READ"
/>

Тогда в манифесте стороннего приложения, которое должно иметь доступ на чтение данных из ContentProvider’a должно быть указано разрешение:

<permission android:name="bbct.android.lite.permission.READ" />

Формирование URI

Соответсвие ContentProvider’a концепции REST выражается в том, что доступ к данным осуществляется с помощью URI. Соответственно, URI необходимо как-то формировать для запроса к необходимым данным.

URI - (Uniform Resource Identifier) унифицированный (единообразный) идентификатор ресурса. По-сути, символьная строка, позволяющая идентифицировать какой-либо ресурс.

Мы уже говорили, что URI ContentProvider’a формируется по следующей схеме:

<prefix>://<authority>/<data_type>/<id>
  • <prefix>:// - “content://”
  • <authority> - уникальный идентификатор нашего ContentProvider’a. Вместе с <prefix> составляет базовый URI - часть которая будет присутствовать во всех запросах к нашему ContentProvider’у. Еще раз скажем, что его нужно делать уникальным. Обычно составляется по приципу “имя_пакета + имя_приложения + имя_провайдера”.

UriMatcher

UriMatcher - класс Android SDK, который хранит соответствие между URI и неким заданным значением integer. Это значение можно использовать в операторе switch, чтобы описать поведение для каждого Content URI. Весьма нужная штука для работы c ContentProvider’ом.

Два метода, которые необходимо знать void AddUri(String authority, String path, int code) и int match(Uri uri). Первый ставит в соответствие path (т.е., по-сути, Uri) и code (надеюсь, что такое authority объяснять не надо), второй по uri возвращает code. Данный класс используется для разбора Uri в REST-методах ContentProvider’a (getType(), query(), etc.) для идентификации действия по URI-запросу. Ниже мы полностью рассмотрим его применение на своем месте, пока - основновные моменты использования:

Объявление:

private static final UriMatcher sUriMatcher;

Задаем соответствие:

sURIMatcher.addURI("contacts", "people", PEOPLE);
// PEOPLE = unique integer constant

Проверяем на соответствие в операторе Switch:

public String getType(Uri url)
    {
        int match = sURIMatcher.match(url);
        switch (match)
        {
            case PEOPLE:
                return "vnd.android.cursor.dir/person";
. . .

Практика использования Content Provider’a

18 08 2017

Теги заметки: