Создание музыкального проигрывателя на Android: настройка проекта
Russian (Pусский) translation by Yuri Yuriev (you can also view the original English article)
Платформа Android предоставляет ресурсы для управления воспроизведением мультимедиа, которые ваши приложения могут использовать для создания интерфейса между пользователем и его музыкальными файлами. В этой серии уроков мы создадим базовый музыкальный плеер для Android. Приложение представит список песен на устройстве, чтобы пользователь мог выбирать их для воспроизведения. Приложение будет управлять воспроизведением и продолжит исполнение, когда пользователь выйдет из приложения, при этом будет отображаться уведомление.
Введение
Для создания music player потребуется класс ContentResolver для извлечения треков на устройстве, класс MediaPlayer для воспроизведения звука и класс MediaController для управления воспроизведением. Мы также будем использовать Service для воспроизведения звука, когда пользователь не взаимодействует напрямую с приложением. Дальше можете не смотреть, если вы промежуточный разработчик Android, и, если уже создали несколько приложений, вам будет неинтересно. Вот предварительный вид готового приложения:
В этом уроке мы создадим приложение и запросим пользовательское устройство для аудиофайлов с использованием классов ContentResolver и Cursor . В следующей части мы будем использовать образец Adapter для показа песен в виде списка, начала воспроизведения при выборе элемента из списка. В последнем выпуске серии мы используем класс MediaController , чтобы дать пользователю контроль над воспроизведением, реализовать функции перемотки вперёд и назад и включить функцию shuffle. По окончании серии мы рассмотрим другие аспекты воспроизведения media, которые могут улучшить приложение, такие как обработка фокуса аудио, представление медиафайлов в разных видах и воспроизведение потокового media .
1. Создание и настройка нового проекта
Шаг 1
Создайте новый проект Android. Если вы используете Eclipse, то пусть IDE (Integrated Development Environment) создаст для вас основной класс Activity и файл макета. Для некоторых кодов, которые мы используем в этой серии, понадобится минимальный уровень API 16, так что вам нужно будет сделать дополнительные шаги для поддержки более старых версий. Когда проект будет создан, откройте файл Manifest проекта. Внутри элемента manifest добавьте следующее разрешение:
Мы сделаем это разрешение, чтобы воспроизведение музыки продолжалось, когда устройство не работает. В вашем Manifest должен быть элемент для основного класса Activity . Добавьте следующие атрибуты в элемент activity , чтобы установить screenOrientation и launchMode :
Для простоты мы будем придерживаться книжной ориентации. launchMode поможет процессу перехода к приложению после его удаления. Мы отобразим уведомление о текущей песне, нажатие на уведомление вернёт пользователя в приложение. Мы также будем использовать класс Service для воспроизведения музыки. Добавьте следующую строку в Manifest проекта внутри элемента application и после элемента activity :
Измените имя пакета, как хотите и измените имя класса, если хотите.
Шаг 2
Откройте основной файл layout проекта и замените его на следующий layout:
Обязательно измените атрибут tools:context, если ваш основной класс Activity называется по-другому. Макет включает ListView , в котором мы представим список песен.
Мы собираемся включить два пункта меню для переключения функции перемешивания и выхода из приложения. Откройте файл главного меню (res/menu/main.xml) и замените его содержимое следующим:
Если нравится, вы можете хранить строки заголовка в файле res/values/strings.xml. Эти два элемента относятся к drawable файлам. Создайте собственные или используйте эти два изображения для начала:
Мы будем использовать и значок уведомления о воспроизведении. Создайте его или используйте следующий:
Код будет ссылаться на изображения, используя имена rand, end и play, поэтому убедитесь, что вы используете одни и те же имена файлов. Скопируйте изображения в папку (-и) drawables вашего проекта. Мы будем реализовывать эти действия позже.
2. Запрос устройства для песен
Шаг 1
Попробуем запросить устройство пользователя для аудиофайлов. Во-первых, добавьте новый класс Song . Мы будем использовать этот класс для моделирования данных одного звукового файла. В объявлении класса добавьте три переменные экземпляра данных, которые мы сохраним для каждого трека:
Затем добавьте метод конструктора, в котором мы создаём экземпляры переменных:
Наконец, добавьте методы get для переменных экземпляра:
Если вы планируете использовать больше информации о треке, вы можете добавить дополнительные переменные экземпляра в класс.
Шаг 2
Откройте основной класс Activity и добавьте imports:
Объявите следующие переменные экземпляра перед методом onCreate :
Мы будем хранить песни в списке и отображать их в экземпляре ListView в главном layout. В onCreate после установки вида содержимого извлеките экземпляр ListView , используя ID, который мы дали ему в главном макете:
Создайте список, как показано ниже:
Затем в объявлении основного класса Activity после существующих методов создайте вспомогательный метод для извлечения информации о звуковом файле:
Внутри этого метода создайте экземпляр ContentResolver , извлеките URI для внешних музыкальных файлов и создайте экземпляр Cursor , используя экземпляр ContentResolver для запроса музыкальных файлов:
Теперь мы можем перебрать результаты, сначала проверив, что у нас достоверные данные:
Сначала мы извлекаем индексы столбцов для интересующих нас элементов данных для каждой песни, затем мы используем их для создания нового объекта Song и добавления его в список, перед тем как продолжить обзор результатов.
Вернитесь в onCreate после кода, который мы добавили, вызовите этот новый метод:
3. Отображение композиций
Шаг 1
Теперь мы можем отобразить список песен в пользовательском интерфейсе. В методе onCreate после вызова вспомогательного метода, который мы создали минуту назад, давайте отсортируем данные так, чтобы песни представлялись в алфавитном порядке:
Мы используем переменную title в классе Song , используя добавленные методы get, чтобы реализовать метод compare , сортируя песни по заголовкам.
Шаг 2
Определим layout для представления каждой песни в списке. Добавьте новый файл в папку res/layout вашего проекта, назвав его song.xml и введя следующее:
Не стесняйтесь изменять layout по своему вкусу. Каждая песня в списке будет представлена строками названия и артиста, поэтому мы будем использовать TextViews для отображения этих данных. Обратите внимание: в теге открытия LinearLayout указан атрибут onClick . Мы будем использовать этот метод в основном классе Activity , чтобы реагировать на выбор песни в списке, проигрывая песню, представленную элементом списка, который был выбран.
Шаг 3
Мы будем использовать Adapter для отображения песен в виде списка. Добавьте в приложение новый класс SongAdapter или назовите иначе. При создании класса назначьте ему суперкласс android.widget.BaseAdapter . Eclipse должен вставить следующую схему:
Вам нужно будет добавить следующие imports:
В объявлении класса задайте следующие переменные:
Мы передадим список композиций из основного класса Activity и используем LayoutInflater для сопоставления строк заголовка и исполнителя с TextViews в созданном нами layout песни.
После переменных экземпляра придайте адаптеру метод конструктора для их создания:
Измените содержимое метода getCount , чтобы вернуть размер списка:
Вы можете оставить методы getItem и getItemId нетронутыми. Обновите реализацию метода getView , как показано ниже:
Мы устанавливаем текст названия и исполнителя, выбирая правильный экземпляр Song из списка, используя индекс позиции, сопоставляя эти строки с представлениями, добавленными в файл макета песни. Мы также устанавливаем позицию в качестве тега вида, которая позволит нам играть правильную песню, когда пользователь нажимает на элемент в списке. Помните, что файл формата song.xml включал атрибут onClick . Мы будем использовать метод, указанный там, чтобы получить тег в Activity .
Шаг 3
Вернувшись в основной класс Activity , в методе onCreate после сортировки списка создайте новый экземпляр класса Adapter и установите его в ListView :
Когда вы запускаете приложение, оно должно отображать список песен на устройстве, щёлкнув по ним, приложение будет вызывать исключение на данный момент, но мы реализуем обработчик щелчка в следующем уроке.
Заключение
Теперь мы настроили приложение для чтения песен с пользовательского устройства. В следующей части мы начнем воспроизведение, когда пользователь выберет песню, используя класс MediaPlayer . Мы реализуем воспроизведение с использованием класса Service , чтобы оно продолжалось, пока пользователь занят другими приложениями. Наконец, мы будем использовать класс MediaController , чтобы дать пользователю управлять воспроизведением.
Источник
Android и звук: как делать правильно
В статье рассматривается архитектура и API для создания приложений, воспроизводящих музыку. Мы напишем простое приложение, которое будет проигрывать небольшой заранее заданный плейлист, но «по-взрослому» — с использованием официально рекомендуемых практик. Мы применим MediaSession и MediaController для организации единой точки доступа к медиаплееру, и MediaBrowserService для поддержки Android Auto. А также оговорим ряд шагов, которые обязательны, если мы не хотим вызвать ненависти пользователя.
В первом приближении задача выглядит просто: в activity создаем MediaPlayer, при нажатии кнопки Play начинаем воспроизведение, а Stop — останавливаем. Все прекрасно работает ровно до тех пор, пока пользователь не выйдет из activity. Очевидным решением будет перенос MediaPlayer в сервис. Однако теперь у нас встают вопросы организации доступа к плееру из UI. Нам придется реализовать binded-сервис, придумать для него API, который позволил бы управлять плеером и получать от него события. Но это только половина дела: никто, кроме нас, не знает API сервиса, соответственно, наша activity будет единственным средством управления. Пользователю придется зайти в приложение и нажать Pause, если он хочет позвонить. В идеале нам нужен унифицированный способ сообщить Android, что наше приложение является плеером, им можно управлять и что в настоящий момент мы играем такой-то трек из такого-то альбома. Чтобы система со своей стороны подсобила нам с UI. В Lollipop (API 21) был представлен такой механизм в виде классов MediaSession и MediaController. Немногим позже в support library появились их близнецы MediaSessionCompat и MediaControllerCompat.
Следует сразу отметить, что MediaSession не имеет отношения к воспроизведению звука, он только об управлении плеером и его метаданными.
MediaSession
Итак, мы создаем экземпляр MediaSession в сервисе, заполняем его сведениями о нашем плеере, его состоянии и отдаем MediaSession.Callback, в котором определены методы onPlay, onPause, onStop, onSkipToNext и прочие. В эти методы мы помещаем код управления MediaPlayer (в примере воспользуемся ExoPlayer). Наша цель, чтобы события и от аппаратных кнопок, и из окна блокировки, и с часов под Android Wear вызывали эти методы.
Полностью рабочий код доступен на GitHub (ветка master). В статьи приводятся только переработанные выдержки из него.
Для доступа извне к MediaSession требуется токен. Для этого научим сервис его отдавать
и пропишем в манифест
MediaController
Теперь реализуем activity с кнопками управления. Создаем экземпляр MediaController и передаем в конструктор полученный из сервиса токен.
MediaController предоставляет как методы управления плеером play, pause, stop, так и коллбэки onPlaybackStateChanged(PlaybackState state) и onMetadataChanged(MediaMetadata metadata). К одному MediaSession могут подключиться несколько MediaController, таким образом можно легко обеспечить консистентность состояний кнопок во всех окнах.
Наша activity работает, но ведь идея исходно была, чтобы из окна блокировки тоже можно было управлять. И тут мы приходим к важному моменту: в API 21 полностью переделали окно блокировки, теперь там отображаются уведомления и кнопки управления плеером надо делать через уведомления. К этому мы вернемся позже, давайте пока рассмотрим старое окно блокировки.
Как только мы вызываем mediaSession.setActive(true), система магическим образом присоединяется без всяких токенов к MediaSession и показывает кнопки управления на фоне картинки из метаданных.
Однако в силу исторических причин события о нажатии кнопок приходят не напрямую в MediaSession, а в виде бродкастов. Соответственно, нам надо еще подписаться на эти бродкасты и перебросить их в MediaSession.
MediaButtonReceiver
Для этого разработчики Android любезно предлагают нам воспользоваться готовым ресивером MediaButtonReceiver.
Добавим его в манифест
MediaButtonReceiver при получении события ищет в приложении сервис, который также принимает «android.intent.action.MEDIA_BUTTON» и перенаправляет его туда. Поэтому добавим аналогичный интент-фильтр в сервис
Если подходящий сервис не найден или их несколько, будет выброшен IllegalStateException.
Теперь в сервис добавим
Метод handleIntent анализирует коды кнопок из intent и вызывает соответствующие коллбэки в mediaSession. Получилось немного плясок с бубном, но зато почти без написания кода.
На системах с API >= 21 система не использует бродкасты для отправки событий нажатия на кнопки и вместо этого напрямую обращается в MediaSession. Однако, если наш MediaSession неактивен (setActive(false)), его пробудят бродкастом. И для того, чтобы этот механизм работал, надо сообщить MediaSession, в какой ресивер отправлять бродкасты.
Добавим в onCreate сервиса
На системах с API Так это выглядит
Android 4.4
MIUI 8 (базируется на Android 6, то есть теоретически окно блокировки не должно отображать наш трек, но здесь уже сказывается кастомизация MIUI).
Уведомления
Однако, как ранее упоминалось, начиная с API 21 окно блокировки научилось отображать уведомления. И по этому радостному поводу, вышеописанный механизм был выпилен. Так что теперь давайте еще формировать уведомления. Это не только требование современных систем, но и просто удобно, поскольку пользователю не придется выключать и включать экран, чтобы просто нажать паузу. Заодно применим это уведомление для перевода сервиса в foreground-режим.
Нам не придется рисовать кастомное уведомление, поскольку Android предоставляет специальный стиль для плееров — Notification.MediaStyle.
Добавим в сервис два метода
И добавим вызов refreshNotificationAndForegroundStatus(int playbackState) во все коллбэки MediaSession.
Android 4.4
Android 7.1.1
Android Wear
Started service
В принципе у нас уже все работает, но есть засада: наша activity запускает сервис через binding. Соответственно, после того, как activity отцепится от сервиса, он будет уничтожен и музыка остановится. Поэтому нам надо в onPlay добавить
Никакой обработки в onStartCommand не надо, наша цель не дать системе убить сервис после onUnbind.
А в onStop добавить
В случае, если к сервису привязаны клиенты, stopSelf ничего не делает, только взводит флаг, что после onUnbind сервис можно уничтожить. Так что это вполне безопасно.
ACTION_AUDIO_BECOMING_NOISY
Продолжаем полировать сервис. Допустим пользователь слушает музыку в наушниках и выдергивает их. Если эту ситуацию специально не обработать, звук переключится на динамик телефона и его услышат все окружающие. Было бы хорошо в этом случае встать на паузу.
Для этого в Android есть специальный бродкаст AudioManager.ACTION_AUDIO_BECOMING_NOISY.
Добавим в onPlay
В onPause и onStop
И по факту события встаем на паузу
Android Auto
Начиная с API 21 появилась возможность интегрировать телефон с экраном в автомобиле. Для этого необходимо поставить приложение Android Auto и подключить телефон к совместимому автомобилю. На экран автомобиля будет выведены крупные контролы для управления навигацией, сообщениями и музыкой. Давайте предложим Android Auto наше приложение в качестве поставщика музыки.
Если у вас под рукой нет совместимого автомобиля, что, согласитесь, иногда бывает, можно просто запустить приложение и экран самого телефона будет работать в качестве автомобильного.
Исходный код выложен на GitHub (ветка MediaBrowserService).
Прежде всего надо указать в манифесте, что наше приложение совместимо с Android Auto.
Добавим в манифест
Здесь automotive_app_desc — это ссылка на файл automotive_app_desc.xml, который надо создать в папке xml
Преобразуем наш сервис в MediaBrowserService. Его задача, помимо всего ранее сделанного, отдавать токен в Android Auto и предоставлять плейлисты.
Поправим декларацию сервиса в манифесте
Во-первых, теперь наш сервис экспортируется, поскольку к нему будут подсоединяться снаружи.
И, во-вторых, добавлен интент-фильтр android.media.browse.MediaBrowserService.
Меняем родительский класс на MediaBrowserServiceCompat.
Поскольку теперь сервис должен отдавать разные IBinder в зависимости от интента, поправим onBind
Имплементируем два абстрактных метода, возвращающие плейлисты
И, наконец, имплементируем новый коллбэк MediaSession
Здесь mediaId — это тот, который мы отдали в setMediaId в onLoadChildren.
Плейлист
Трек
UPDATE от 27.10.2017: Пример на GitHub переведен на targetSdkVersion=26. Из релевантных теме статьи изменений необходимо отметить следующее:
- android.support.v7.app.NotificationCompat.MediaStyle теперь deprecated. Вместо него следует использовать android.support.v4.media.app.NotificationCompat.MediaStyle. Соответственно, больше нет необходимости использовать android.support.v7.app.NotificationCompat, теперь можно использовать android.support.v4.app.NotificationCompat
- Метод AudioManager.requestAudioFocus(OnAudioFocusChangeListener l, int streamType, int durationHint) теперь тоже deprecated. Вместо него надо использовать AudioManager.requestAudioFocus(AudioFocusRequest focusRequest). AudioFocusRequest — новый класс, добавленный с API 26, поэтому не забывайте проверять на API level.
Создание AudioFocusRequest выглядит следующим образом
Теперь запрашиваем фокус
Разумеется, все вышеописанные изменения вносить необязательно, старые методы работать не перестали.
Вот мы и добрались до конца. В целом тема эта довольно запутанная. Плюс отличия реализаций на разных API level и у разных производителей. Очень надеюсь, что я ничего не упустил. Но если у вас есть, что исправить и добавить, с удовольствием внесу изменения в статью.
Еще очень рекомендую к просмотру доклад Ian Lake. Доклад от 2015 года, но вполне актуален.
Источник