ziginsider
ContentProvider – это класс, предоставляющий унифицированный интерфейс для доступа к данным приложения. Этот класс позволяет вам использовать единый источник данных в вашем приложении.
Задуман, как способ предоставлять данные вашего приложения для сторонних приложений, + для реализации поиска среди данных вашего приложения с использованием 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 говорит следующее:
Как уже было сказано, ContentProvider предоставляет унифицированный интерфейс для доступа к данным. Это говорит о том, что не нужно беспокоится о реализации хранения данных. Она может меняться, но данные остаются доступны.
Не нужно управлять жизненным циклом объекта для доступа к данным (к примеру, экземпляра SQLiteDatabase). Ведь в случае прямого использования таких объектов возникает немало вопросов: где хранить этот объект? Когда закрывать базу данных? Когда уничтожать этот объект? ContentProvider позволяет получать доступ к данным из любого места, где доступен контекст приложения (экземпляр Context).
ContentProvider полностью соответствует концепциям REST (о которых мы говорили в этих заметках)
Немного о последнем пункте. Итак, соответствие REST:
Создание своего СontentProvider’a
На практике нам необходимо наследоваться от класса ContentProvider и реализовать следующие методы:
Следует помнить, что все перечисленные методы кроме onCreate() могут выполняться одновременно в нескольких потоках, и поэтому должны быть потоко-безопасными (thread-safe).
Допустим, мы создали свой ContentProvider, добавили его в манифесте и теперь можем обращаться к нему в качестве интерфейса для работы с данными. Но здесь есть небольшая тонкость – мы создавали объект ContentProvider, но обращаться к данным нужно через объект ContentResolver, который можно получить через метод getContentResolver в классе Context:
Манифест
Регистрация ContentProvider’a в манифесте под тегом
Атрибут android:exporter задает возможность использования нашего ContentProvider’a сторонними приложениями. До версии Android 16 по умолчанию был true, в версиях >= 16 по умолчанию false. Поэтому необходимо обращать внимание на состояние этого атрибута.
Атрибут android:authorities задает ключ по которому мы будем обращаться к нашему ContentProvider’у. Стоит обратить внимание на его уникальность. Установка приложений с одним и тем же значением authorities на одно устройство выдаст ошибку.
Например, в манифесте записано следующее:
Тогда в манифесте стороннего приложения, которое должно иметь доступ на чтение данных из ContentProvider’a должно быть указано разрешение:
Формирование URI
Соответсвие ContentProvider’a концепции REST выражается в том, что доступ к данным осуществляется с помощью URI. Соответственно, URI необходимо как-то формировать для запроса к необходимым данным.
Мы уже говорили, что URI ContentProvider’a формируется по следующей схеме:
Проверяем на соответствие в операторе Switch:
Введение в ContentProvider
Для упрощения доступа к общим данным, таким как файлы мультимедиа, контакты и сведения о календаре, операционная система Android использует поставщики содержимого. В этой статье представлен класс ContentProvider, а также два примера его использования.
Общие сведения о поставщиках содержимого
Поставщик содержимого инкапсулирует репозиторий данных и предоставляет API для доступа к нему. Поставщик существует как часть приложения Android, которая обычно также предоставляет пользовательский интерфейс для отображения данных и управления ими. Основное преимущество поставщика содержимого заключается в том, что другие приложения могут легко получать доступ к инкапсулированным данным с помощью клиентского объекта поставщика (называемого ContentResolver). Вместе поставщик и сопоставитель содержимого обеспечивают единообразный работающий между приложениями API для доступа к данным. Этот API прост в создании и использовании. Любое приложение может использовать ContentProviders для внутреннего управления данными, а также для их предоставления другим приложениям.
ContentProvider также необходимо для приложения, чтобы предоставить настраиваемые варианты поиска, или если вы хотите предоставить возможность копирования сложных данных из приложения для вставки в другие приложения. В этом документе показано, как получить доступ к ContentProviders и создавать его с помощью Xamarin.Android.
Эта статья имеет следующую структуру.
Принцип работы – обзор того, для чего предназначен ContentProvider и как он работает.
Использование поставщика содержимого – пример доступа к списку контактов.
Использование ContentProvider для совместного использования данных – запись и использование ContentProvider в одном приложении.
ContentProviders и курсоры, работающие с их данными, часто используются для заполнения ListViews. Дополнительные сведения об использовании этих классов см. в руководстве Xamarin.Android ListView.
Настраиваемые классы ContentProviders — это удобный способ упаковки данных для использования в собственном приложении или для использования другими приложениями (включая особые варианты использования, такие как пользовательский поиск и копирование или вставка).
Полный список
— создаем свой ContentProvider
Content Provider – это способ расшарить для общего пользования данные вашего приложения. Обычно это данные из БД. И создание класса провайдера похоже на создание обычного класса для работы с БД. Мы используем SQLiteOpenHelper для управления базой, и реализуем методы query, insert, update, delete класса ContentProvider.
ru.startandroid.provider.AdressBook– это authority. Определяет провайдера (если проводить аналогию с базой данных, то это имя базы).
contacts – это path. Какие данные от провайдера нужны (таблица).
7 – это ID. Какая конкретно запись нужна (ID записи)
path может быть составным – например contacts/phones или contacts/email. Это используется, если структура данных достаточно обширна, и данные хранятся в нескольких таблицах в соответствии с некоторой логикой и организацией.
ID может быть не указан. Это означает, что будем работать со всеми записями из path.
Вышерассмотренный пример Uri указывает системе, что мы хотим достучаться до провайдера адресной книги ru.startandroid.provider.AdressBook, и получить доступ к контакту с >
Попробуем создать свой провайдер. Пусть это будет некая адресная книга – список контактов. Для каждого контакта будем хранить всего два атрибута: имя и емэйл.
И отдельно создадим приложение, которое будет к этому провайдеру обращаться и манипулировать данными – читать, добавлять, изменять, удалять.
Начнем с провайдера. Создадим проект без Activity:
Project name: P1011_ContentProvider
Build Target: Android 2.3.3
Application name: ContentProvider
Package name: ru.startandroid.develop.p1011contentprovider
Создаем класс MyContactsProvider, наследующий android.content.ContentProvider
Он предлагает нам реализовать кучу методов. Реализуем.
Кода много, но практически ничего нового для нас нет. В основном идет работа с БД.
В начале идет куча констант. Константы для БД должны быть знакомы и понятны по прошлым урокам, их я не объясняю. Поясню только, что у нас в БД будет всего одна таблица contacts с тремя полями: _id, name и email.
У нас тут получилось, что имя таблицы в БД совпало с path в Uri. Это вовсе необязательно, они могут быть разными.
Далее описываем MIME-типы данных, предоставляемых провайдером. Один для набора данных, другой для конкретной записи. У меня пока что мало опыта работы с провайдерами, и я не очень понимаю, где и как можно эти типы данных использовать. Но реализовать их надо, поэтому делаем это. Мы будем возвращать их в методе getType нашего провайдера.
uriMatcher.addURI(AUTHORITY, CONTACT_PATH, URI_CONTACTS);
означает, что мы добавили в uriMatcher комбинацию значений AUTHORITY, CONTACT_PATH и URI_CONTACTS.
uriMatcher.addURI(AUTHORITY, CONTACT_PATH + «/#», URI_CONTACTS_ID);
И теперь, если мы попросим uriMatcher проверить Uri, состоящий из AUTHORITY и CONTACT_PATH, он вернет нам значение URI_CONTACTS. А если дадим ему Uri, состоящий из AUTHORITY, CONTACT_PATH и числа (ID), то он вернет нам URI_CONTACTS_ID. А мы по этим константам определим – работать со всеми записями или какой-то конкретной.
В общем, на словах все очень сложно получилось, в коде все проще будет. Но главный смысл этого uriMatcher в том, что он определит, какой Uri к нам пришел – общий или с ID. Если общий – то вернет URI_CONTACTS, если с ID – то вернет URI_CONTACTS_ID.
В OnCreate создаем DBHelper – уже знакомый нам помощник для работы с БД.
Далее мы отдаем uri в метод match объекта uriMatcher. Он его разбирает, сверяет с теми комбинациями authority/path, которые мы давали ему в методах addURI и выдает константу из соответствующей комбинации. Если это URI_CONTACTS, значит нам пришел общий Uri и от провайдера хотят получить все его записи. В этом случае мы проверим, указана ли сортировка. Если нет, то поставим сортировку по имени. Как вы понимаете, эта операция с сортировкой совершенно необязательна. Мы могли и ничего не делать. Если же мы получили URI_CONTACTS_ID, то провайдер должен вернуть запись по конкретному ID. Для этого мы извлекаем ID из Uri методом getLastPathSegment и добавляем его в условие selection.
Если uriMatcher не смог опознать Uri, то будем выдавать IllegalArgumentException. Вы, разумеется, можете тут прописать свое решение этой проблемы.
В insert мы проверяем, что нам пришел наш общий Uri. Если все ок, то вставляем данные в таблицу, получаем ID. Этот ID мы добавляем к общему Uri и получаем Uri с ID. По идее, это можно сделать и обычным сложением строк, но рекомендуется использовать метод withAppendedId объекта. Далее мы уведомляем систему, что поменяли данные, соответствующие resultUri. Система посмотрит, не зарегистрировано ли слушателей на этот Uri. Увидит, что мы регистрировали курсор, и даст ему знать, что данные обновились. В конце мы возвращаем resultUri, соответствующий новой добавленной записи.
В delete мы проверяем, какой Uri нам пришел. Если с ID, то фиксим selection – добавляем туда условие по ID. Выполняем удаление в БД, получаем кол-во удаленных записей. Уведомляем, что данные изменились. Возвращаем кол-во удаленных записей.
В update мы проверяем, какой Uri нам пришел. Если с ID, то фиксим selection – добавляем туда условие по ID. Выполняем обновление в БД, получаем кол-во обновленных записей. Уведомляем, что данные изменились. Возвращаем кол-во обновленных записей.
В методе getType возвращаем типы соответственно типу Uri – общий или с ID.
Класс DBHelper помогает нам создать БД и наполнить ее первоначальными данными. Обновление здесь не реализуем.
Теперь, когда система получит запрос на получение данных по Uri с authority = ru.startandroid.providers.AdressBook, она будет работать с нашим провайдером.
С провайдером все. Его можно инсталлить на AVD. Делается это как обычно, просто на экране ничего не появится, т.к. нет Activity. А в консоли будут примерно такие строки:
Uploading P1011_ContentProvider.apk onto device ’emulator-5554′
Installing P1011_ContentProvider.apk.
Success!
\P1011_ContentProvider\bin\P1011_ContentProvider.apk installed on device
Done!
Теперь пишем приложение, которое будет к провайдеру обращаться. Создадим проект:
Project name: P1012_ContProvClient
Build Target: Android 2.3.3
Application name: ContProvClient
Package name: ru.startandroid.develop.p1012contprovclient
Create Activity: MainActivity
Добавим в strings.xml строки:
4 кнопки для операций с данными и список для вывода данных провайдера.
В CONTACT_URI мы храним общий Uri. В CONTACT_NAME и CONTACT_EMAIL – имена полей.
В onCreate мы используем метод getContentResolver, чтобы получить ContentResolver. Этот объект – посредник между нами и провайдером. Мы вызываем его метод query и передаем туда Uri. Остальные параметры оставляем пустыми – т.е. нам вернутся все записи, все поля и сортировку мы не задаем. Полученный курсор мы передаем в Activity на управление – метод startManagingCursor. Далее создаем адаптер и присваиваем его списку.
В onClickInsert мы используем метод insert для добавления записей в провайдер. Этот метод возвращает нам Uri, соответствующий новой записи.
В onClickUpdate мы создаем Uri, соответствующий записи с и апдейтим эту запись в провайдере.
В onClickDelete мы создаем Uri, соответствующий записи с и удаляем эту запись в провайдере.
В onClickError мы пытаемся получить записи по Uri, который не знает провайдер. В его uriMatcher не добавляли информации об этом Uri. В этом случае мы генерировали в провайдере ошибку. Здесь попробуем поймать ее.
Все сохраняем и запускаем приложение.
onCreate
query, content://ru.startandroid.providers.AdressBook/contacts
URI_CONTACTS
Жмем Insert. Появилась новая строка в списке.
Тут надо отметить, что мы вообще не трогали ни курсор, ни адаптер, ни список. Мы только добавили запись в провайдер, а наш список сам обновился. Это работают уведомления, которые мы прописывали в методах провайдера. Курсор ждет уведомления об обновлениях провайдера, а метод вставки ему такое уведомление отправил.
insert, content://ru.startandroid.providers.AdressBook/contacts
insert, result Uri : content://ru.startandroid.providers.AdressBook/contacts/4
Выполнился метод insert, получил на вход общий Uri, в котором указано, в какую именно таблицу вставлять данные. Данные добавлены и провайдер вернул Uri новой записи: content://ru.startandroid.providers.AdressBook/contacts/4
Мы обновили вторую запись, и она ушла в конец списка, т.к. сортировку мы еще в провайдере настроили по имени, если не указано иное.
update, content://ru.startandroid.providers.AdressBook/contacts/2
URI_CONTACTS_ID, 2
update, count = 1
Сработал метод update, получил на вход Uri = content://ru.startandroid.providers.AdressBook/contacts/2. UriMatcher верно распознал, что полученный Uri содержит ID. Провайдер обновил запись и вернул нам кол-во обновленных записей.
delete, content://ru.startandroid.providers.AdressBook/contacts/3
URI_CONTACTS_ID, 3
delete, count = 1
Сработал метод delete, получил на вход Uri = content://ru.startandroid.providers.AdressBook/contacts/3. UriMatcher определил, что Uri с ID. Запись была удалена и мы получили кол-во удаленных записей.
query, content://ru.startandroid.providers.AdressBook/phones
Error: class java.lang.IllegalArgumentException, Wrong URI: content://ru.startandroid.providers.AdressBook/phones
Был выполнен метод query с Uri = content://ru.startandroid.providers.AdressBook/phones. Но UriMatcher не знает такую комбинацию authority (ru.startandroid.providers.AdressBook) и path (phones). В этой ситуации мы настроили провайдер так, что он генерит ошибку. В приложении мы эту ошибку ловим и выдаем в лог.
Есть несколько моментов, которые хотелось бы отдельно отметить.
managedQuery
Константы провайдера
getWritableDatabase
Метод getWritableDatabase по причинам производительности не рекомендуется вызывать в onCreate методе провайдера. Поэтому мы в onCreate только создавали DBHelper, а в методах query, insert и прочих вызывали getWritableDatabase() и получали доступ к БД.
Условия выборки
Я не стал в этом уроке использовать возможности выборки и сортировки при работе с провайдером. Они полностью аналогичны тем, что мы проходили в уроках по SQLite. Не забывайте про них.
Более подробную инфу об этом всем можно найти на офиц.сайте.
На следующем уроке:
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Контент-провайдеры — слабое место в Android-приложениях
Содержание статьи
WARNING
Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.
Сам себе злобный Буратино
На страницах журнала уже не раз обсуждались особенности разработки приложений для платформы Android, поэтому мы не будем лишний раз углубляться в уже известные читателю детали. Но все-таки я немного расскажу про архитектуру Android и о некоторых ее особенностях.
Важной возможностью для всех операционных систем общего назначения всегда были разнообразные методы межпроцессного взаимодействия. В относительно молодой ОС Android было использовано очень много удобных решений, которые должны были облегчить жизнь разработчикам. Одним из таких решений стали контент-провайдеры. Контент-провайдер — это поставщик данных. Любое приложение может создать своего контент-провайдера, который после установки приложения будет зарегистрирован операционной системой (см. врезку «Как задаются свойства контент-провайдера»).
Разграничение доступа и контент-провайдеры
Андроид проектировался как достаточно защищенная платформа, о контент-провайдерах разработчики операционной системы позаботились. Они предоставили очень гибкую систему разграничения доступа, которая позволяет на многих уровнях тонко отрегулировать все возможности взаимодействия.
На самом верхнем уровне можно просто сделать провайдер неэкспортируемым и пользоваться им внутри своего приложения. Если мы все-таки решили его экспортировать, то можно глобально ограничить к нему доступ с помощью параметра android:permission в секцииманифеста. В качестве разрешения можно использовать любое, уже определенное в системе, или задать свое собственное. Это очень удобно, если мы хотим разрешить доступ к провайдеру для группы своих приложений. Мы просто даем всем своим приложениям нестандартное разрешение, точно таким же разрешением закрываем доступ к провайдеру. После этого все приложения из нашей уютной инфраструктуры получат доступ к провайдеру на чтение и запись.
Для более тонкой регулировки можно использовать параметры android:readPermission и android:writePermission. Как ясно из их названий, они позволяют установить отдельно ограничение доступа на чтение или запись. Причем эти параметры имеют больший приоритет, чем более общий параметр android:permission.
Но есть и еще один, более глубокий уровень регулировки доступа. Он позволяет разрешить доступ к определенному набору информации, который поставляет провайдер. На полную катушку в этом случае используется то, что доступ к провайдеру осуществляется через URI bit.ly.
К сожалению, не все разработчики уделяют внимание вопросам безопасности, поэтому многие приложения регистрируют в операционной системе контент-провайдеры полностью открытые как на чтение, так и на запись.
Проблемы и решения
Итак, мы уже можем сделать предварительные выводы о том, чем нам грозит плохо реализованный контент-провайдер:
Хакер #170. Малварь для OS X
Пишем свою утилиту для анализа контент-провайдеров
Ранее я уже упомянул Mercury от MWR Labs. Это замечательный набор инструментов, и я рекомендую им пользоваться. К сожалению, лично меня повергает в уныние один взгляд на пользовательское лицензионное соглашение, под которое попадает этот продукт. Кроме того, лучше всего усвоить материал на практике, тем более когда программирование не представляет существенной сложности.
Напишем свое приложение под Android для анализа контент-провайдеров. Что мы сделаем?
Как я уже упоминал, провайдеры регистрируются в операционной системе при установке приложения. Поэтому самым удобным инструментом для извлечения информации будет PackageManager.
Объект типа ProviderInfo содержит всю необходимую нам информацию. Мы получим соответствующий ContentResolver и обработаем результат и возможные исключения.
Остальная часть кода нашего приложения служит для оформления пользовательского интерфейса, поэтому я ее опускаю. На выходе мы получили небольшую утилиту.



Раз утилита готова — как я и обещал, немного поэкспериментируем. Я использовал обычный смартфон, старый Samsung Galaxy S с последней официальной прошивкой и некоторым набором самых распространенных приложений.
В списке контент-провайдеров своего телефона ты самостоятельно можешь найти что-то забавное. Например, мое внимание привлек провайдер com.sec.provider.facekey. В advisory MWR Labs про него ничего не сказано, тем не менее он представляет определенный интерес. Дело в том, что он устанавливается и используется системой «биометрической» блокировки экрана по снимку лица. Удивительно, что привилегии, запрещающие чтение и запись, в данном случае не установлены. Попробуем передать провайдеру SQL injection вектор «* from sqlite_master—».

С интересом узнаем, что мы получили доступ к базе данных с табличкой facefeature следующего вида:
Данные из этой таблицы легко читаются, кстати, замечу, что наша тестовая утилита не запрашивает никаких привилегий. Но при этом вполне может считать особенности твоего лица :).
Немного порывшись в списке, можно считать настройки телефона из провайдера com.settings (например, content://com.settings/secure). Это несмотря на то, что разрешения READ_SETTINGS мы не имеем.
Интересный результат дает обращение к «content://com.google.settings/sqlite_master—» (что ж, и Google промахивается):
Дальнейшие эксперименты я оставляю читателю. Все исходные тексты утилиты доступны на GitHub.
Как задаются свойства контент-провайдера
Если обратиться к Android SDK (где весь процесс расписан очень подробно и по шагам), то видно, что для того, чтобы зарегистрировать свой контент-провайдер, тебе придется добавить описание провайдера в файл AndroidManifest.xml в секцию. Параметров при этом можно указать множество bit.ly, но мы рассмотрим лишь важные для нас:
Важная особенность — доступ к данным будет осуществляться при помощи специального URI со схемой «content». Параметр android:authorities является уникальным идентификатором провайдера и представляет собой первую часть URI, вторая часть будет указывать на то, какие именно данные мы хотим получить. Параметр android:exported показывает, доступен ли провайдер для других приложений. Уже тут есть маленькая особенность, которая может испортить жизнь разработчику. Этот параметр в версиях Android до 16-й включительно (Android 4.1 JELLY_BEAN) по умолчанию установлен в «true», и все контент-провайдеры экспортируются, естественно, такая ситуация не безопасна. Но только с версии 17 (Android 4.2 JELLY_BEAN_MR1) в ОС было внесено исправление, и теперь, чтобы экспортировать провайдер, необходимо самостоятельно изменить используемый по умолчанию «false».
Итак, в нашем случае для доступа к контент-провайдеру можно будет использовать URI вида:
Фантазия разработчика при написании провайдера мало чем ограничена, дело в том, что обязательно нужно переопределить всего лишь шесть абстрактных методов (query(), insert(), update(), delete(), getType(), onCreate()). При этом никто не запрещает сделать эти методы пустыми, возвращать на любой запрос константу, результат чтения файла или обращения к сети. Тем более если твой провайдер предоставляет доступ к данным только на чтение, то методы insert(), update() и так далее ему совсем не нужны.
И хотя никто не принуждает программиста прятать «под капотом» своего контент-провайдера базу данных, но очень часто тут встречается хорошо знакомая всем разработчикам мобильных приложений для Android SQLite. Тем более что используемый для получения данных ContentResolver всегда возвращает объект типа Cursor, что тонко намекает…
MEMENTO MORI
Итак, мы рассмотрели одно из слабых мест в Android-приложениях. Я постарался показать, насколько важно использовать по максимуму возможности по разграничению доступа, которые предлагает операционная система.
Разработчикам мобильных приложений хочется напомнить, что, написав контент-провайдер, вы принимаете решение поделиться информацией, поэтому стоит подумать, кто и в каком объеме сможет получить к ней доступ. Также многие забывают, что уязвимостям типа SQL injection подвержены не только веб-приложения. Поэтому санитизацию пользовательского ввода и использование prepared statements никто не отменял, даже под Android.







