1. Поставщики контента

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

Каждое приложение работает внутри Android Application Sandbox, что предотвращает доступ других приложений к вашему коду и данным в оперативной памяти, внутренние файлы недоступны другим приложениям, а в качестве базы данных используется встроенная база SQLite, которая подключается в виде библиотеки (таким образом, данные в базе доступны только вашему приложению).

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

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

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

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

Создадим новый проект для приложения «Музыкальный плеер». В первом окне создадим списковый элемент RecyclerView, который будет показывать список mp3 файлов в устройстве.

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

MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        ...

        // Получим разрешения
        requestPermission();
    }

    private void requestPermission() {
        if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (requestCode == 1) {
            String msg = "Permission to read external storage";
            msg += (grantResults[0] == PackageManager.PERMISSION_GRANTED) ? " granted" : " not granted";
            Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
        }
    }
}

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

SELECT projection FROM uri WHERE selection ORDER BY sortOrder

Сначала займемся условием WHERE. Нам необходимо создать строку для параметров поиска, – какие конкретно записи в базе данных мы ищем. Мы должны указать, что мы ищем аудиоданные, которые классифицированы как музыка, с MIME-форматом audio/mpeg и длиной более 1 минуты (чтобы отсечь различные джинглы и другие короткие аудиофайлы). Чтобы сформировать такой запрос, нам необходимо обратиться к специальному классу.

За поставку медиа-данных в Android отвечает специальный класс MediaStore. Вложенный класс MediaStore.Audio отвечает за поставку аудиоданных. В классе MediaStore.Audio.Media содержится ряд строковых констант, используя которые, мы формируем параметры поиска.

MainActivity.java
String selection = MediaStore.Audio.Media.IS_MUSIC + "= 1"
        + " AND " + MediaStore.Audio.Media.MIME_TYPE + "= 'audio/mpeg'"
        + " AND " + MediaStore.Audio.Media.DURATION + "> 60000";

Далее, нам необходимо сформировать поля для выборки, какие столбцы необходимо вернуть в результате запроса (такой объект называется «проекция» или projection). Очень часто нам нужна не вся информация об mp3-файле, а только часть, например, только исполнитель, альбом и название песни. Колонки для выборки предоставляются в виде массива строк, каждый элемент массива – одна колонка из таблицы. Чтобы вручную не запоминать названия колонок, воспользуемся константами уже знакомого класса MediaStore.Audio.Media.

MainActivity.java
final String[] projection = new String[]{
        MediaStore.Audio.Media.ALBUM_ID,
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.ARTIST,
        MediaStore.Audio.Media.ALBUM
};

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

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

Опять же, нам нет необходимости запоминать URI таблицы, нужная нам константа находится в классе MediaStore.Audio.Media. Обратите внимание, что мы обращается к строке URI, которая хранит данные из внешнего хранилища.

MainActivity.java
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;

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

MainActivity.java
final String sortOrder = MediaStore.Audio.AudioColumns.TITLE
        + " COLLATE LOCALIZED ASC";

Итак, параметры запроса сформированы, теперь необходимо запросить данные у поставщика контента. Для доступа приложения к данным из поставщика контента используется объект ContentResolver.

Объект ContentResolver – это специальный объект, который создается операционной системой и служит посредником между поставщиками контента и его «потребителями». Исходя из переданного URI, он находит нужного поставщика данных и передает ему различные запросы для работы с данными.

Чтобы получить доступ к объекту ContentResolver, необходимо воспользоваться методом контекста getContentResolver(). После получения ссылки на объект, вызываем метод query(), в который передаем параметры сформированного запроса.

MainActivity.java
Cursor cursor = null;
cursor = getContentResolver().query(uri, projection, selection, null, sortOrder);

Результат запроса нам будет возвращен в виде объекта класса Cursor. Давайте познакомимся с этим классом поближе.

Обращение к поставщику данных в главном потоке происходит только в демонстрационных целях. В реальных приложениях рекомендуется использовать класс CursorLoader, который реализует асинхронную загрузку данных. Класс CursorLoader реализует протокол асинхронной загрузки данных (класс Loader) из различных источников в Activity или Fragment и в данном курсе не рассматривается.

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

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

Работа с курсором относится к тематике использования базы данных в Android. Более подробно про курсор здесь – http://developer.alexanderklimov.ru/android/sqlite/cursor.php, а также http://bit.ly/2wPLYuE.

Работа с записями в курсоре напоминает работу с итератором в коллекциях. Изначально курсор не указывает на запись, поэтому первый метод работы с курсором – вызов метода moveToFirst(), который перемещает курсор на первую запись из результата запроса.

Далее мы проверяем, не вышли ли мы за пределы полученных записей. Если нет – с помощью методов getXXX() получаем данные из текущей записи, куда указывает наш курсор. Нам надо знать тип данных и порядковый номер колонки или ее имя. Колонки совпадают с содержимым массива projection. После считывания данных, передвигаем курсор на следующую надпись с помощью метода moveToNext().

MainActivity.java
if (cursor != null) {
    // Передвигаем курсор на первую запись результата
    cursor.moveToFirst();

    while (!cursor.isAfterLast()) {
        Song song = new Song();

        song.setTitle(cursor.getString(1));     // title
        song.setArtist(cursor.getString(2));    // artist
        song.setAlbum(cursor.getString(3));     // album

        // Добавляем объект песни в коллекцию с песнями
        songs.add(song);

        // Получаем картинку для альбома
        song.setPath("content://media/external/audio/albumart/" + cursor.getLong(0));

        cursor.moveToNext();
    }
    cursor.close();
}

В итоге, метод getSongs() вернет нам коллекцию с данными, которые мы можем использовать при построении адаптера для спискового элемента. Используем метод getSongs() для вывода списка композиций в нашем музыкальном плеере. Запустим приложение и посмотрим на результат

Как вы могли заметить, в нашем приложении дополнительно выводится картинка обложки альбома. Разберемся, как это было реализовано.

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

Так как каждое изображение является отдельным файлом и, следовательно, отдельным ресурсом, нам нет необходимости прописывать условия выборки или поля в таблице – нам необходимо правильно сформировать URI, чтобы получить именно то изображение, которое нам нужно. За хранение изображений отвечает класс MediaStore.Images, а URI выглядит следующим образом:

content://media/external/audio/albumart/{id}

где id – идентификатор альбома. Его создает специальная служба, которая индексирует файлы. Этот id содержится в таблице, записи из которой мы извлекли ранее. Таким образом, в объекте класса Song есть поле album_id, которое содержит id альбома, которому принадлежит эта песня. Наша задача – сформировать URI с учетом id альбома.

Сформируем URI изображения, который позже используем для получения изображения

MainActivity.java
song.setPath("content://media/external/audio/albumart/" + cursor.getLong(0));

Загружать изображение будем в классе адаптера.

SongsAdapter.java
public class SongsAdapter extends RecyclerView.Adapter<SongsAdapter.SongViewHolder> {

    ...
    
    @Override
    public void onBindViewHolder(@NonNull SongViewHolder songViewHolder, int i) {
        Song song = songList.get(i);

        songViewHolder.title.setText(song.getTitle());
        songViewHolder.artist.setText(song.getArtist() + " (" + song.getAlbum() + ")");

        // Получаем обложку для альбома
        Bitmap bitmap = null;
        try {
            bitmap = MediaStore.Images.Media.getBitmap(songViewHolder.title.getContext().getContentResolver(), Uri.parse(song.getPath()));
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (bitmap != null) {
            songViewHolder.album_image.setImageBitmap(bitmap);
        }
    }
}

Здесь всё просто – получаем ContentResolver, получаем URI, после чего передаем их методу getBitmap(). Если для этого альбома картинка будет – мы ее получим и передадим в ImageView, если нет – получим null и оставим стандартную картинку.

Запустим приложение и посмотрим на результат

Ниже приведены листинги классов

MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView = findViewById(R.id.recycler);
        LinearLayoutManager manager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        recyclerView.setLayoutManager(manager);

        // Получим разрешения
        requestPermission();

        findViewById(R.id.button).setOnClickListener(v -> {
            List<Song> songList = getSongs();
            recyclerView.setAdapter(new SongsAdapter(songList));
        });
    }

    private List<Song> getSongs() {

        List<Song> songs = new ArrayList<>();

        // WHERE
        String selection = MediaStore.Audio.Media.IS_MUSIC + "= 1"
                + " AND " + MediaStore.Audio.Media.MIME_TYPE + "= 'audio/mpeg'"
                + " AND " + MediaStore.Audio.Media.DURATION + "> 60000";

        // SELECT
        final String[] projection = new String[]{
                MediaStore.Audio.Media.ALBUM_ID,
                MediaStore.Audio.Media.TITLE,
                MediaStore.Audio.Media.ARTIST,
                MediaStore.Audio.Media.ALBUM
        };

        // FROM
        Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;

        // ORDER BY
        final String sortOrder = MediaStore.Audio.AudioColumns.TITLE
                + " COLLATE LOCALIZED ASC";

        // Получаем данные с помощью Content Resolver
        Cursor cursor = null;
        cursor = getContentResolver().query(uri, projection, selection, null, sortOrder);

        if (cursor != null) {
            // Передвигаем курсор на первую запись результата
            cursor.moveToFirst();

            while (!cursor.isAfterLast()) {
                Song song = new Song();

                song.setTitle(cursor.getString(1));     // title
                song.setArtist(cursor.getString(2));    // artist
                song.setAlbum(cursor.getString(3));     // album

                // Получаем картинку для альбома
                song.setPath("content://media/external/audio/albumart/" + cursor.getLong(0));

                // Добавляем объект песни в коллекцию с песнями
                songs.add(song);

                cursor.moveToNext();
            }
            cursor.close();
        }

        return songs;
    }

    private void requestPermission() {
        if (checkSelfPermission(Manifest.permission.RECEIVE_SMS) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (requestCode == 1) {
            String msg = "Permission to read external storage";
            msg += (grantResults[0] == PackageManager.PERMISSION_GRANTED) ? " granted" : " not granted";
            Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
        }
    }
}

Last updated