Как создать поставщика контента

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

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

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

Подготовка

Прежде чем создавать поставщика, учтите следующее:

  • Решите, нужен ли вам поставщик контента. Поставщик контента необходим, если вы хотите реализовать одну или несколько из следующих функций:
    • Вы хотите предоставить другим приложениям доступ к сложным данным или файлам.
    • Вы хотите разрешить пользователям копировать сложные данные из вашего приложения в другие приложения.
    • Вы хотите предоставлять собственные подсказки при поиске, используя фреймворк поиска.
    • Вы хотите предоставить виджетам доступ к данным приложения.
    • Вы хотите реализовать классы AbstractThreadedSyncAdapter, CursorAdapter или CursorLoader.

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

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

Затем выполните следующие действия:

  1. Разработайте необработанное хранилище для своих данных. Поставщик контента предлагает данные двумя способами:
    данные файлов;
    Данные, которые обычно хранятся в файлах, например фотографии, аудио и видео. Храните файлы в частном пространстве приложения. В ответ на запрос файла из другого приложения ваш поставщик может предложить дескриптор файла.
    Структурированные данные
    Данные, которые обычно помещаются в базу данных, массив или аналогичную структуру. Хранить данные в форме, совместимой с таблицами строк и столбцов. Строка представляет собой объект, например человека или товар на складе. Столбец представляет собой определенные данные для объекта, например имя человека или цену товара. Обычно такие данные хранятся в базе данных SQLite, но вы можете использовать любой тип постоянного хранилища. Чтобы узнать больше о типах хранилищ, доступных в системе Android, ознакомьтесь с разделом Проектирование хранилища данных.
  2. Определите конкретную реализацию класса ContentProvider и его обязательных методов. Этот класс является интерфейсом между вашими данными и остальной частью системы Android. Подробную информацию об этом классе можно найти в разделе Реализация класса ContentProvider.
  3. Определите строку источника, URI контента и названия столбцов поставщика. Если вы хотите, чтобы приложение поставщика обрабатывало намерения, также определите действия намерения, дополнительные данные и флаги. Также определите разрешения, которые требуются для приложений, запрашивающих доступ к вашим данным. Рекомендуем определить все эти значения как константы в отдельном классе контракта. Позже вы сможете предоставить доступ к этому классу другим разработчикам. Подробнее о URI контента… Подробную информацию о намерениях можно найти в разделе Намерения и доступ к данным.
  4. Добавьте другие необязательные элементы, например выборку данных или реализацию AbstractThreadedSyncAdapter, которая может синхронизировать данные между поставщиком и облачными данными.

Проектирование хранилища данных

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

Вот некоторые из технологий хранения данных, доступных на Android:

  • Если вы работаете со структурированными данными, то можете использовать реляционную базу данных, например SQLite, или нереляционное хранилище данных типа "ключ-значение", например LevelDB. Если вы работаете с неструктурированными данными, такими как аудио, изображения или видео, то лучше хранить их в виде файлов. При необходимости вы можете использовать несколько разных типов хранилищ и предоставлять к ним доступ через один поставщик контента.
  • Система Android может взаимодействовать с библиотекой персистентности Room, которая предоставляет доступ к API базы данных SQLite, используемому собственными поставщиками Android для хранения табличных данных. Чтобы создать базу данных с помощью этой библиотеки, создайте экземпляр подкласса RoomDatabase, как описано в статье Сохранение данных в локальной базе данных с помощью Room.

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

  • Для хранения данных файлов в Android есть различные API, ориентированные на файлы. Подробнее о хранении данных и файлов… Если вы разрабатываете поставщика, который предлагает данные, связанные с медиаконтентом, например музыкой или видео, вы можете создать поставщика, который объединяет данные таблиц и файлы.
  • В редких случаях может быть полезно реализовать несколько поставщиков контента для одного приложения. Например, вы можете передавать определенные данные виджету с помощью одного поставщика контента, а другой набор данных предоставлять для совместного использования с другими приложениями.
  • Для работы с данными, передаваемыми по сети, используйте классы из пакетов java.net и android.net. Вы также можете синхронизировать сетевые данные с локальным хранилищем, например базой данных, а затем предоставлять их в виде таблиц или файлов.

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

Рекомендации по проектированию данных

Вот несколько советов по разработке структуры данных поставщика:

  • В таблице данных всегда должен быть столбец "первичный ключ", который поставщик поддерживает как уникальное числовое значение для каждой строки. С помощью этого значения можно связать строку с другими строками в других таблицах (используя его как внешний ключ). Хотя вы можете использовать любое название для этого столбца, лучше всего выбрать BaseColumns._ID, поскольку для связывания результатов запроса поставщика с ListView необходимо, чтобы один из полученных столбцов имел название _ID.
  • Если вы хотите предоставить растровые изображения или другие очень большие фрагменты данных, ориентированных на файлы, храните данные в файле, а затем предоставляйте их косвенно, а не храните их непосредственно в таблице. В этом случае вам нужно сообщить пользователям вашего поставщика, что для доступа к данным им необходимо использовать метод файла ContentResolver.
  • Используйте тип данных BLOB (большой двоичный объект) для хранения данных, которые различаются по размеру или имеют разную структуру. Например, в столбце BLOB можно хранить буфер протокола или структуру JSON.

    BLOB также можно использовать для реализации таблицы, не зависящей от схемы. В таблице такого типа вы определяете столбец первичного ключа, столбец MIME-типа и один или несколько общих столбцов в качестве BLOB. Значение данных в столбцах BLOB определяется значением в столбце MIME-типа. Это позволяет хранить разные типы строк в одной таблице. Таблица data поставщика контактов –ContactsContract.Dataпример таблицы, не зависящей от схемы.

Как создавать URI контента

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

Подробнее об URI контента см. в разделе Основные сведения о поставщиках контента.

Создание авторитета

Обычно у поставщика есть один орган, который служит его внутренним именем Android. Чтобы избежать конфликтов с другими поставщиками, используйте в качестве основы для идентификатора поставщика доменное имя в интернете (в обратном порядке). Поскольку эта рекомендация также справедлива для названий пакетов Android, вы можете определить центр сертификации поставщика как расширение названия пакета, содержащего поставщика.

Например, если название пакета Android – com.example.<appname>, укажите для поставщика значение com.example.<appname>.provider.

Разработка структуры пути

Обычно разработчики создают URI контента из источника, добавляя пути, указывающие на отдельные таблицы. Например, если у вас есть две таблицы, table1 и table2, вы можете объединить их с источником из предыдущего примера, чтобы получить URI контента com.example.<appname>.provider/table1 и com.example.<appname>.provider/table2. Пути не ограничены одним сегментом, и для каждого уровня пути не обязательно должна быть таблица.

Как обрабатывать идентификаторы URI контента

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

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

Затем пользователь выбирает одну из строк, чтобы посмотреть или изменить данные. Приложение получает соответствующую строку из Cursor, поддерживающего ListView, получает значение _ID для этой строки, добавляет его к URI контента и отправляет запрос на доступ поставщику. Затем поставщик может выполнить запрос или изменение в отношении конкретной строки, выбранной пользователем.

Шаблоны URI контента

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

Шаблон URI контента сопоставляет URI контента с помощью подстановочных знаков:

  • * соответствует строке из любых допустимых символов любой длины.
  • # соответствует строке цифровых символов любой длины.

В качестве примера разработки и кодирования обработки URI контента рассмотрим поставщика с источником com.example.app.provider, который распознает следующие URI контента, указывающие на таблицы:

  • content://com.example.app.provider/table1: таблица с названием table1.
  • content://com.example.app.provider/table2/dataset1: таблица с названием dataset1.
  • content://com.example.app.provider/table2/dataset2: таблица с названием dataset2.
  • content://com.example.app.provider/table3: таблица с названием table3.

Поставщик также распознает эти URI контента, если к ним добавлен идентификатор строки, например content://com.example.app.provider/table3/1 для строки, идентифицируемой 1 в table3.

Возможны следующие шаблоны URI контента:

content://com.example.app.provider/*
Соответствует любому URI контента в поставщике.
content://com.example.app.provider/table2/*
Соответствует URI контента для таблиц dataset1 и dataset2, но не соответствует URI контента для table1 или table3.
content://com.example.app.provider/table3/#
Соответствует URI контента для отдельных строк в table3, например content://com.example.app.provider/table3/6 для строки, идентифицируемой 6.

В приведенном ниже фрагменте кода показано, как работают методы в UriMatcher. Этот код обрабатывает URI для всей таблицы иначе, чем URI для одной строки, используя шаблон URI контента content://<authority>/<path> для таблиц и content://<authority>/<path>/<id> для отдельных строк.

Метод addURI() сопоставляет источник и путь с целочисленным значением. Метод match() возвращает целочисленное значение для URI. Оператор switch позволяет выбрать, запрашивать ли всю таблицу или только одну запись.

Kotlin

private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
    /*
     * The calls to addURI() go here for all the content URI patterns that the provider
     * recognizes. For this snippet, only the calls for table 3 are shown.
     */

    /*
     * Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
     * in the path.
     */
    addURI("com.example.app.provider", "table3", 1)

    /*
     * Sets the code for a single row to 2. In this case, the # wildcard is
     * used. content://com.example.app.provider/table3/3 matches, but
     * content://com.example.app.provider/table3 doesn't.
     */
    addURI("com.example.app.provider", "table3/#", 2)
}
...
class ExampleProvider : ContentProvider() {
    ...
    // Implements ContentProvider.query()
    override fun query(
            uri: Uri?,
            projection: Array<out String>?,
            selection: String?,
            selectionArgs: Array<out String>?,
            sortOrder: String?
    ): Cursor? {
        var localSortOrder: String = sortOrder ?: ""
        var localSelection: String = selection ?: ""
        when (sUriMatcher.match(uri)) {
            1 -> { // If the incoming URI was for all of table3
                if (localSortOrder.isEmpty()) {
                    localSortOrder = "_ID ASC"
                }
            }
            2 -> {  // If the incoming URI was for a single row
                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query.
                 */
                localSelection += "_ID ${uri?.lastPathSegment}"
            }
            else -> { // If the URI isn't recognized,
                // do some error handling here
            }
        }

        // Call the code to actually do the query
    }
}

Java

public class ExampleProvider extends ContentProvider {
...
    // Creates a UriMatcher object.
    private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        /*
         * The calls to addURI() go here for all the content URI patterns that the provider
         * recognizes. For this snippet, only the calls for table 3 are shown.
         */

        /*
         * Sets the integer value for multiple rows in table 3 to one. No wildcard is used
         * in the path.
         */
        uriMatcher.addURI("com.example.app.provider", "table3", 1);

        /*
         * Sets the code for a single row to 2. In this case, the # wildcard is
         * used. content://com.example.app.provider/table3/3 matches, but
         * content://com.example.app.provider/table3 doesn't.
         */
        uriMatcher.addURI("com.example.app.provider", "table3/#", 2);
    }
...
    // Implements ContentProvider.query()
    public Cursor query(
        Uri uri,
        String[] projection,
        String selection,
        String[] selectionArgs,
        String sortOrder) {
...
        /*
         * Choose the table to query and a sort order based on the code returned for the incoming
         * URI. Here, too, only the statements for table 3 are shown.
         */
        switch (uriMatcher.match(uri)) {


            // If the incoming URI was for all of table3
            case 1:

                if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
                break;

            // If the incoming URI was for a single row
            case 2:

                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query.
                 */
                selection = selection + "_ID = " + uri.getLastPathSegment();
                break;

            default:
            ...
                // If the URI isn't recognized, do some error handling here
        }
        // Call the code to actually do the query
    }

Другой класс, ContentUris, предоставляет удобные методы для работы с частью id URI контента. Классы Uri и Uri.Builder включают удобные методы для анализа существующих объектов Uri и создания новых.

Реализуйте класс ContentProvider

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

Обязательные методы

Абстрактный класс ContentProvider определяет шесть абстрактных методов, которые вы реализуете в рамках своего конкретного подкласса. Все эти методы, кроме onCreate(), вызываются клиентским приложением, которое пытается получить доступ к вашему поставщику контента.

query()
Получите данные от поставщика. Используйте аргументы, чтобы выбрать таблицу для запроса, строки и столбцы, которые нужно вернуть, и порядок сортировки результата. Верните данные в виде объекта Cursor.
insert()
Вставьте новую строку в поставщик. Используйте аргументы, чтобы выбрать целевую таблицу и получить значения столбцов. Возвращает URI контента для новой строки.
update()
Обновите существующие строки в поставщике. Используйте аргументы, чтобы выбрать таблицу и строки, которые нужно обновить, и получить обновленные значения столбцов. Возвращает количество обновленных строк.
delete()
Удалите строки из таблицы поставщика. Используйте аргументы, чтобы выбрать таблицу и строки, которые нужно удалить. Возвращает количество удаленных строк.
getType()
Возвращает MIME-тип, соответствующий URI контента. Этот метод подробно описан в разделе Реализация MIME-типов поставщика контента.
onCreate()
Инициализируйте поставщика. Система Android вызывает этот метод сразу после создания поставщика. Поставщик не будет создан, пока объект ContentResolver не попытается получить к нему доступ.

Эти методы имеют ту же сигнатуру, что и одноименные методы ContentResolver.

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

  • Все эти методы, кроме onCreate(), могут вызываться несколькими потоками одновременно, поэтому они должны быть потокобезопасными. Подробнее о процессах и потоках…
  • Не выполняйте длительные операции в onCreate(). Откладывайте задачи инициализации до тех пор, пока они действительно не понадобятся. Подробнее об этом рассказывается в разделе Реализация метода onCreate().
  • Хотя вы должны реализовать эти методы, ваш код не должен делать ничего, кроме возврата ожидаемого типа данных. Например, вы можете запретить другим приложениям вставлять данные в некоторые таблицы, игнорируя вызов insert() и возвращая 0.

Реализуйте метод query()

Метод ContentProvider.query() должен возвращать объект Cursor или, в случае сбоя, выдавать ошибку Exception. Если вы используете базу данных SQLite в качестве хранилища данных, вы можете вернуть Cursor, возвращенный одним из методов query() класса SQLiteDatabase.

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

Если вы не используете базу данных SQLite в качестве хранилища данных, используйте один из конкретных подклассов Cursor. Например, класс MatrixCursor реализует курсор, в котором каждая строка является массивом экземпляров Object. Для этого класса используйте addRow(), чтобы добавить новую строку.

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

  • IllegalArgumentException. Вы можете выбрать, чтобы это исключение генерировалось, если поставщик получает недопустимый URI контента.
  • NullPointerException

Реализуйте метод insert()

Метод insert() добавляет новую строку в соответствующую таблицу, используя значения в аргументе ContentValues. Если название столбца не указано в аргументе ContentValues, вы можете задать для него значение по умолчанию в коде поставщика или в схеме базы данных.

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

Реализуйте метод delete()

Метод delete() не должен удалять строки из хранилища данных. Если вы используете адаптер синхронизации с поставщиком, рекомендуется помечать удаленную строку флагом "удалено", а не удалять ее полностью. Адаптер синхронизации может проверять наличие удаленных строк и удалять их с сервера перед удалением из поставщика.

Реализуйте метод update()

Метод update() принимает тот же аргумент ContentValues, что и insert(), и те же аргументы selection и selectionArgs, что и delete() и ContentProvider.query(). Это позволит вам повторно использовать код.

Реализуйте метод onCreate()

Система Android вызывает метод onCreate() при запуске поставщика. В этом методе выполняйте только быстрые задачи инициализации, а создание базы данных и загрузку данных отложите до тех пор, пока поставщик не получит запрос на данные. Если вы выполняете длительные задачи в onCreate(), вы замедляете запуск поставщика. Это, в свою очередь, замедляет ответ от поставщика другим приложениям.

В следующих двух фрагментах кода показано взаимодействие между ContentProvider.onCreate() и Room.databaseBuilder(). В первом фрагменте показана реализация ContentProvider.onCreate(), где создается объект базы данных и обрабатываются объекты доступа к данным.

Kotlin

// Defines the database name
private const val DBNAME = "mydb"
...
class ExampleProvider : ContentProvider() {

    // Defines a handle to the Room database
    private lateinit var appDatabase: AppDatabase

    // Defines a Data Access Object to perform the database operations
    private var userDao: UserDao? = null

    override fun onCreate(): Boolean {

        // Creates a new database object
        appDatabase = Room.databaseBuilder(context, AppDatabase::class.java, DBNAME).build()

        // Gets a Data Access Object to perform the database operations
        userDao = appDatabase.userDao

        return true
    }
    ...
    // Implements the provider's insert method
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        // Insert code here to determine which DAO to use when inserting data, handle error conditions, etc.
    }
}

Java

public class ExampleProvider extends ContentProvider

    // Defines a handle to the Room database
    private AppDatabase appDatabase;

    // Defines a Data Access Object to perform the database operations
    private UserDao userDao;

    // Defines the database name
    private static final String DBNAME = "mydb";

    public boolean onCreate() {

        // Creates a new database object
        appDatabase = Room.databaseBuilder(getContext(), AppDatabase.class, DBNAME).build();

        // Gets a Data Access Object to perform the database operations
        userDao = appDatabase.getUserDao();

        return true;
    }
    ...
    // Implements the provider's insert method
    public Cursor insert(Uri uri, ContentValues values) {
        // Insert code here to determine which DAO to use when inserting data, handle error conditions, etc.
    }
}

Как реализовать MIME-типы ContentProvider

В классе ContentProvider есть два метода для возврата MIME-типов:

getType()
Один из обязательных методов, который вы реализуете для любого поставщика.
getStreamTypes()
Метод, который необходимо реализовать, если ваш поставщик предлагает файлы.

MIME-типы для таблиц

Метод getType() возвращает String в формате MIME, который описывает тип данных, возвращаемых аргументом URI контента. Аргумент Uri может быть шаблоном, а не конкретным URI. В этом случае возвращается тип данных, связанных с URI контента, которые соответствуют шаблону.

Для распространенных типов данных, таких как текст, HTML или JPEG, getType() возвращает стандартный MIME-тип. Полный список стандартных типов можно найти на сайте IANA MIME Media Types.

Для URI контента, указывающих на одну или несколько строк таблицы, метод getType() возвращает MIME-тип в формате, который зависит от поставщика Android:

  • Тип детали: vnd
  • Часть подтипа:
    • Если шаблон URI предназначен для одной строки: android.cursor.item/
    • Если шаблон URI относится к нескольким строкам: android.cursor.dir/.
  • Часть, относящаяся к поставщику: vnd.<name>.<type>

    Вы предоставляете <name> и <type>. Значение <name> является глобально уникальным, а значение <type> – уникальным для соответствующего шаблона URI. В качестве <name> можно использовать название компании или часть названия пакета Android вашего приложения. В качестве <type> рекомендуется использовать строку, которая идентифицирует таблицу, связанную с URI.

Например, если у поставщика есть полномочия com.example.app.provider и он предоставляет таблицу table1, то MIME-тип для нескольких строк в table1 будет следующим:

vnd.android.cursor.dir/vnd.com.example.provider.table1

Для одной строки table1 MIME-тип будет следующим:

vnd.android.cursor.item/vnd.com.example.provider.table1

MIME-типы для файлов

Если ваш поставщик предлагает файлы, реализуйте getStreamTypes(). Метод возвращает массив String MIME-типов для файлов, которые поставщик может вернуть для заданного URI контента. Отфильтруйте предлагаемые MIME-типы по аргументу фильтра MIME-типов, чтобы возвращать только те MIME-типы, которые клиент хочет обработать.

Например, поставщик предлагает фотографии в форматах JPG, PNG и GIF. Если приложение вызывает ContentResolver.getStreamTypes() со строкой фильтра image/* для объекта, который является изображением, то метод ContentProvider.getStreamTypes() возвращает массив:

{ "image/jpeg", "image/png", "image/gif"}

Если приложению нужны только файлы JPG, оно может вызвать ContentResolver.getStreamTypes() со строкой фильтра *\/jpeg, и getStreamTypes() вернет:

{"image/jpeg"}

Если поставщик не предлагает ни один из MIME-типов, указанных в строке фильтра, getStreamTypes() возвращает null.

Реализация класса контракта

Класс контракта – это класс public final, который содержит определения констант для URI, названий столбцов, MIME-типов и других метаданных, относящихся к поставщику. Класс устанавливает контракт между поставщиком и другими приложениями, гарантируя, что к поставщику можно будет правильно обращаться, даже если фактические значения URI, названия столбцов и т. д. изменятся.

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

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

Класс ContactsContract и его вложенные классы – это примеры классов контракта.

Как настроить разрешения для поставщика контента

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

  • По умолчанию файлы данных, хранящиеся во внутреннем хранилище устройства, являются конфиденциальными для вашего приложения и поставщика.
  • Базы данных SQLiteDatabase доступны только вашему приложению и поставщику.
  • По умолчанию файлы данных, которые вы сохраняете во внешнем хранилище, являются общедоступными и доступными для чтения всем. Вы не можете использовать поставщика контента, чтобы ограничить доступ к файлам во внешнем хранилище, поскольку другие приложения могут использовать другие вызовы API для их чтения и записи.
  • Вызовы методов для открытия или создания файлов или баз данных SQLite во внутреннем хранилище устройства могут предоставлять доступ на чтение и запись всем остальным приложениям. Если вы используете внутренний файл или базу данных в качестве хранилища поставщика и предоставляете ему доступ на чтение или запись для всех, разрешения, заданные для поставщика в манифесте, не защитят ваши данные. По умолчанию доступ к файлам и базам данных во внутренней памяти ограничен. Не меняйте эту настройку для хранилища поставщика.

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

Как настроить разрешения

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

Разрешения для поставщика определяются с помощью одного или нескольких элементов <permission> в файле манифеста. Чтобы разрешение было уникальным для вашего поставщика, используйте для атрибута android:name область действия в стиле Java. Например, назовите разрешение на чтение com.example.app.provider.permission.READ_PROVIDER.

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

Одно разрешение на чтение и запись на уровне поставщика
Одно разрешение, которое управляет доступом на чтение и запись ко всему поставщику, указанное с помощью атрибута android:permission элемента <provider>.
Раздельные разрешения на чтение и запись на уровне поставщика
Разрешение на чтение и разрешение на запись для всего поставщика. Они задаются с помощью атрибутов android:readPermission и android:writePermission элемента <provider>. Они имеют приоритет над разрешением, которое требуется для android:permission.
Разрешение на уровне путей
Разрешение на чтение, запись или чтение и запись для URI контента в вашем поставщике. Каждый URI, который вы хотите контролировать, указывается в дочернем элементе <path-permission> элемента <provider>. Для каждого URI контента можно указать разрешение на чтение и запись, разрешение на чтение, разрешение на запись или все три. Разрешения на чтение и запись имеют приоритет над разрешением на чтение/запись. Кроме того, разрешение на уровне пути имеет приоритет над разрешениями на уровне поставщика.
Временное разрешение
Уровень разрешений, который предоставляет временный доступ к приложению, даже если у него нет разрешений, которые обычно требуются. Функция временного доступа позволяет сократить количество разрешений, которые приложение должно запрашивать в манифесте. Если вы включите временные разрешения, постоянные разрешения для вашего поставщика потребуются только тем приложениям, которым постоянно нужен доступ ко всем вашим данным.

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

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

Чтобы включить временные разрешения, задайте атрибут android:grantUriPermissions элемента <provider> или добавьте один или несколько дочерних элементов <grant-uri-permission> в элемент <provider>. Вызывайте Context.revokeUriPermission(), когда удаляете поддержку URI контента, связанного с временным разрешением от поставщика.

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

Если для этого параметра задано значение "false", добавьте дочерние элементы <grant-uri-permission> в элемент <provider>. Каждый дочерний элемент указывает URI контента, к которому предоставляется временный доступ.

Чтобы делегировать временный доступ к приложению, намерение должно содержать флаг FLAG_GRANT_READ_URI_PERMISSION, флаг FLAG_GRANT_WRITE_URI_PERMISSION или оба флага. Они задаются с помощью метода setFlags().

Если атрибут android:grantUriPermissions не указан, по умолчанию используется значение "false".

Элемент <provider>

Как и компоненты Activity и Service, подкласс ContentProvider определяется в файле манифеста для приложения с помощью элемента <provider>. Система Android получает из элемента следующую информацию:

"Организация" (android:authorities)
Символические имена, которые идентифицируют поставщика в системе. Этот атрибут подробно описан в разделе Разработка URI контента.
Название класса поставщика (android:name)
Класс, реализующий ContentProvider. Этот класс подробно описан в разделе Реализация класса ContentProvider.
Разрешения
Атрибуты, указывающие разрешения, которые должны иметь другие приложения для доступа к данным поставщика:

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

Атрибуты запуска и управления
Эти атрибуты определяют, как и когда система Android запускает поставщика, характеристики процесса поставщика и другие настройки среды выполнения:
  • android:enabled: флаг, позволяющий системе запустить поставщика.
  • android:exported: флаг, позволяющий другим приложениям использовать этот поставщик.
  • android:initOrder – порядок, в котором запускается этот поставщик относительно других поставщиков в том же процессе.
  • android:multiProcess: флаг, позволяющий системе запустить поставщика в том же процессе, что и вызывающий клиент.
  • android:process – название процесса, в котором выполняется поставщик.
  • android:syncable: флаг, указывающий, что данные поставщика должны быть синхронизированы с данными на сервере.

Эти атрибуты подробно описаны в руководстве по элементу <provider>.

Информационные атрибуты
Необязательный значок и ярлык для поставщика:
  • android:icon – прорисовываемый ресурс, содержащий значок поставщика. Значок показывается рядом с ярлыком поставщика в списке приложений в разделе Настройки > Приложения > Все.
  • android:label – информационный ярлык с описанием поставщика, его данных или того и другого. Ярлык появится в списке приложений в разделе Настройки > Приложения > Все.

Эти атрибуты подробно описаны в руководстве по элементу <provider>.

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

Намерения и доступ к данным

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

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

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

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

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

Дополнительную информацию можно найти в статье Обзор поставщика календаря.