Создайте поставщика контента

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

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

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

Прежде чем начать строить

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

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

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

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

Далее выполните следующие действия, чтобы создать своего провайдера:

  1. Создайте необработанное хранилище для ваших данных. Поставщик контента предлагает данные двумя способами:
    Данные файла
    Данные, которые обычно сохраняются в файлах, например фотографии, аудио или видео. Храните файлы в личном пространстве вашего приложения. В ответ на запрос файла из другого приложения ваш провайдер может предложить дескриптор файла.
    «Структурированные» данные
    Данные, которые обычно помещаются в базу данных, массив или подобную структуру. Сохраняйте данные в форме, совместимой с таблицами строк и столбцов. Строка представляет объект, например человека или предмет в инвентаре. Столбец представляет некоторые данные об объекте, например имя человека или цену товара. Распространенным способом хранения данных этого типа является база данных SQLite, но вы можете использовать любой тип постоянного хранилища. Подробнее о типах хранилищ, доступных в системе Android, можно узнать в разделе «Хранилище проектных данных» .
  2. Определите конкретную реализацию класса ContentProvider и его необходимых методов. Этот класс является интерфейсом между вашими данными и остальной частью системы Android. Дополнительные сведения об этом классе см. в разделе «Реализация класса ContentProvider» .
  3. Определите строку полномочий поставщика, URI контента и имена столбцов. Если вы хотите, чтобы приложение поставщика обрабатывало намерения, также определите действия намерения, дополнительные данные и флаги. Также определите разрешения, необходимые для приложений, которым требуется доступ к вашим данным. Рассмотрите возможность определения всех этих значений как констант в отдельном классе контракта. Позже вы сможете предоставить доступ к этому классу другим разработчикам. Дополнительные сведения об 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. Это позволяет хранить разные типы строк в одной таблице. Таблица «данных» поставщика контактов ContactsContract.Data является примером независимой от схемы таблицы.

Проектирование URI контента

URI контента — это URI, который идентифицирует данные в поставщике. URI контента включают символическое имя всего провайдера (его полномочия ) и имя, указывающее на таблицу или файл ( путь ). Необязательная часть идентификатора указывает на отдельную строку в таблице. Каждый метод доступа к данным 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 контента с помощью подстановочных знаков:

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

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

Котлин

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

Ява

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.

Реализуйте метод запроса()

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

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

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

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

Реализуйте метод вставки().

Метод 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() , в которой создается объект базы данных и создаются дескрипторы объектов доступа к данным:

Котлин

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

Ява

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 .

Для URI контента, которые указывают на строку или строки табличных данных, getType() возвращает тип MIME в формате 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> в файле манифеста. Чтобы сделать разрешение уникальным для вашего провайдера, используйте область видимости в стиле Java для атрибута android:name . Например, назовите разрешение на чтение 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 контента или 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 . Вместо этого он отправляет намерение, которое запускает действие, которое часто является частью собственного приложения провайдера. Целевое действие отвечает за получение и отображение данных в своем пользовательском интерфейсе.

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

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

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

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

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