В статье рассматривается архитектура и 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). В статьи приводятся только переработанные выдержки из него.
// Закешируем билдеры
// ...метаданных трека
final MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder();
// ...состояния плеера
// Здесь мы указываем действия, которые собираемся обрабатывать в коллбэках.
// Например, если мы не укажем ACTION_PAUSE,
// то нажатие на паузу не вызовет onPause.
// ACTION_PLAY_PAUSE обязателен, иначе не будет работать
// управление с Android Wear!
final PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_STOP
| PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS);
MediaSessionCompat mediaSession;
@Override
public void onCreate() {
super.onCreate();
// "PlayerService" - просто tag для отладки
mediaSession = new MediaSessionCompat(this, "PlayerService");
// FLAG_HANDLES_MEDIA_BUTTONS - хотим получать события от аппаратных кнопок
// (например, гарнитуры)
// FLAG_HANDLES_TRANSPORT_CONTROLS - хотим получать события от кнопок
// на окне блокировки
mediaSession.setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
// Отдаем наши коллбэки
mediaSession.setCallback(mediaSessionCallback);
Context appContext = getApplicationContext()
// Укажем activity, которую запустит система, если пользователь
// заинтересуется подробностями данной сессии
Intent activityIntent = new Intent(appContext, MainActivity.class);
mediaSession.setSessionActivity(
PendingIntent.getActivity(appContext, 0, activityIntent, 0));
}
@Override
public void onDestroy() {
super.onDestroy();
// Ресурсы освобождать обязательно
mediaSession.release();
}
MediaSessionCompat.Callback mediaSessionCallback = new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
MusicRepository.Track track = musicRepository.getCurrent();
// Заполняем данные о треке
MediaMetadataCompat metadata = metadataBuilder
.putBitmap(MediaMetadataCompat.METADATA_KEY_ART,
BitmapFactory.decodeResource(getResources(), track.getBitmapResId()));
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, track.getTitle());
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, track.getArtist());
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, track.getArtist());
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, track.getDuration())
.build();
mediaSession.setMetadata(metadata);
// Указываем, что наше приложение теперь активный плеер и кнопки
// на окне блокировки должны управлять именно нами
mediaSession.setActive(true);
// Сообщаем новое состояние
mediaSession.setPlaybackState(
stateBuilder.setState(PlaybackStateCompat.STATE_PLAYING,
PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1).build());
// Загружаем URL аудио-файла в ExoPlayer
prepareToPlay(track.getUri());
// Запускаем воспроизведение
exoPlayer.setPlayWhenReady(true);
}
@Override
public void onPause() {
// Останавливаем воспроизведение
exoPlayer.setPlayWhenReady(false);
// Сообщаем новое состояние
mediaSession.setPlaybackState(
stateBuilder.setState(PlaybackStateCompat.STATE_PAUSED,
PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1).build());
}
@Override
public void onStop() {
// Останавливаем воспроизведение
exoPlayer.setPlayWhenReady(false);
// Все, больше мы не "главный" плеер, уходим со сцены
mediaSession.setActive(false);
// Сообщаем новое состояние
mediaSession.setPlaybackState(
stateBuilder.setState(PlaybackStateCompat.STATE_STOPPED,
PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1).build());
}
}
Для доступа извне к MediaSession требуется токен. Для этого научим сервис его отдавать
@Override
public IBinder onBind(Intent intent) {
return new PlayerServiceBinder();
}
public class PlayerServiceBinder extends Binder {
public MediaSessionCompat.Token getMediaSessionToken() {
return mediaSession.getSessionToken();
}
}
и пропишем в манифест
<service
android:name=".service.PlayerService"
android:exported="false">
</service>
MediaController
Теперь реализуем activity с кнопками управления. Создаем экземпляр MediaController и передаем в конструктор полученный из сервиса токен.
MediaController предоставляет как методы управления плеером play, pause, stop, так и коллбэки onPlaybackStateChanged(PlaybackState state) и onMetadataChanged(MediaMetadata metadata). К одному MediaSession могут подключиться несколько MediaController, таким образом можно легко обеспечить консистентность состояний кнопок во всех окнах.
PlayerService.PlayerServiceBinder playerServiceBinder;
MediaControllerCompat mediaController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Button playButton = (Button) findViewById(R.id.play);
final Button pauseButton = (Button) findViewById(R.id.pause);
final Button stopButton = (Button) findViewById(R.id.stop);
bindService(new Intent(this, PlayerService.class), new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
playerServiceBinder = (PlayerService.PlayerServiceBinder) service;
try {
mediaController = new MediaControllerCompat(
MainActivity.this, playerServiceBinder.getMediaSessionToken());
mediaController.registerCallback(
new MediaControllerCompat.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
if (state == null)
return;
boolean playing =
state.getState() == PlaybackStateCompat.STATE_PLAYING;
playButton.setEnabled(!playing);
pauseButton.setEnabled(playing);
stopButton.setEnabled(playing);
}
}
);
}
catch (RemoteException e) {
mediaController = null;
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
playerServiceBinder = null;
mediaController = null;
}
}, BIND_AUTO_CREATE);
playButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mediaController != null)
mediaController.getTransportControls().play();
}
});
pauseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mediaController != null)
mediaController.getTransportControls().pause();
}
});
stopButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mediaController != null)
mediaController.getTransportControls().stop();
}
});
}
Наша activity работает, но ведь идея исходно была, чтобы из окна блокировки тоже можно было управлять. И тут мы приходим к важному моменту: в API 21 полностью переделали окно блокировки, теперь там отображаются уведомления и кнопки управления плеером надо делать через уведомления. К этому мы вернемся позже, давайте пока рассмотрим старое окно блокировки.
Как только мы вызываем mediaSession.setActive(true), система магическим образом присоединяется без всяких токенов к MediaSession и показывает кнопки управления на фоне картинки из метаданных.
Однако в силу исторических причин события о нажатии кнопок приходят не напрямую в MediaSession, а в виде бродкастов. Соответственно, нам надо еще подписаться на эти бродкасты и перебросить их в MediaSession.
MediaButtonReceiver
Для этого разработчики Android любезно предлагают нам воспользоваться готовым ресивером MediaButtonReceiver.
Добавим его в манифест
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
MediaButtonReceiver при получении события ищет в приложении сервис, который также принимает "android.intent.action.MEDIA_BUTTON" и перенаправляет его туда. Поэтому добавим аналогичный интент-фильтр в сервис
<service
android:name=".service.PlayerService"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
Если подходящий сервис не найден или их несколько, будет выброшен IllegalStateException.
Теперь в сервис добавим
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
MediaButtonReceiver.handleIntent(mediaSession, intent);
return super.onStartCommand(intent, flags, startId);
}
Метод handleIntent анализирует коды кнопок из intent и вызывает соответствующие коллбэки в mediaSession. Получилось немного плясок с бубном, но зато почти без написания кода.
На системах с API >= 21 система не использует бродкасты для отправки событий нажатия на кнопки и вместо этого напрямую обращается в MediaSession. Однако, если наш MediaSession неактивен (setActive(false)), его пробудят бродкастом. И для того, чтобы этот механизм работал, надо сообщить MediaSession, в какой ресивер отправлять бродкасты.
Добавим в onCreate сервиса
Intent mediaButtonIntent = new Intent(
Intent.ACTION_MEDIA_BUTTON, null, appContext, MediaButtonReceiver.class);
mediaSession.setMediaButtonReceiver(
PendingIntent.getBroadcast(appContext, 0, mediaButtonIntent, 0));
На системах с API < 21 метод setMediaButtonReceiver ничего не делает.
Ок, хорошо. Запускаем, переходим в окно блокировки и… ничего нет. Потому что мы забыли важный момент, без которого ничего не работает, — получение аудиофокуса.
Аудиофокус
Всегда существует вероятность, что несколько приложений захотят одновременно воспроизвести звук. Или поступил входящий звонок и надо срочно остановить музыку. Для решения этих проблем в системный сервис AudioManager включили возможность запроса аудиофокуса. Аудиофокус является правом воспроизводить звук и выдается только одному приложению в каждый момент времени. Если приложению отказали в предоставлении аудиофокуса или забрали его позже, воспроизведение звука необходимо остановить. Как правило фокус всегда предоставляется, то есть когда у приложения нажимают play, все остальные приложения замолкают. Исключение бывает только при активном телефонном разговоре. Технически нас никто не заставляет получать фокус, но мы же не хотим раздражать пользователя? Ну и плюс окно блокировки игнорирует приложения без аудиофокуса.
Фокус необходимо запрашивать в onPlay() и освобождать в onStop().
Получаем AudioManager в onCreate
@Override
public void onCreate() {
super.onCreate();
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
...
}
Запрашиваем фокус в onPlay
@Override
public void onPlay() {
...
int audioFocusResult = audioManager.requestAudioFocus(
audioFocusChangeListener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
if (audioFocusResult != AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
return;
// Аудиофокус надо получить строго до вызова setActive!
mediaSession.setActive(true);
...
}
И освобождаем в onStop
@Override
public void onStop() {
...
audioManager.abandonAudioFocus(audioFocusChangeListener);
...
}
При запросе фокуса мы отдали коллбэк
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener =
new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
// Фокус предоставлен.
// Например, был входящий звонок и фокус у нас отняли.
// Звонок закончился, фокус выдали опять
// и мы продолжили воспроизведение.
mediaSessionCallback.onPlay();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// Фокус отняли, потому что какому-то приложению надо
// коротко "крякнуть".
// Например, проиграть звук уведомления или навигатору сказать
// "Через 50 метров поворот направо".
// В этой ситуации нам разрешено не останавливать вопроизведение,
// но надо снизить громкость.
// Приложение не обязано именно снижать громкость,
// можно встать на паузу, что мы здесь и делаем.
mediaSessionCallback.onPause();
break;
default:
// Фокус совсем отняли.
mediaSessionCallback.onPause();
break;
}
}
};
Все, теперь окно блокировки на системах с API < 21 работает.
Android 4.4
MIUI 8 (базируется на Android 6, то есть теоретически окно блокировки не должно отображать наш трек, но здесь уже сказывается кастомизация MIUI).
Уведомления
Однако, как ранее упоминалось, начиная с API 21 окно блокировки научилось отображать уведомления. И по этому радостному поводу, вышеописанный механизм был выпилен. Так что теперь давайте еще формировать уведомления. Это не только требование современных систем, но и просто удобно, поскольку пользователю не придется выключать и включать экран, чтобы просто нажать паузу. Заодно применим это уведомление для перевода сервиса в foreground-режим.
Нам не придется рисовать кастомное уведомление, поскольку Android предоставляет специальный стиль для плееров — Notification.MediaStyle.
Добавим в сервис два метода
void refreshNotificationAndForegroundStatus(int playbackState) {
switch (playbackState) {
case PlaybackStateCompat.STATE_PLAYING: {
startForeground(NOTIFICATION_ID, getNotification(playbackState));
break;
}
case PlaybackStateCompat.STATE_PAUSED: {
// На паузе мы перестаем быть foreground, однако оставляем уведомление,
// чтобы пользователь мог play нажать
NotificationManagerCompat.from(PlayerService.this)
.notify(NOTIFICATION_ID, getNotification(playbackState));
stopForeground(false);
break;
}
default: {
// Все, можно прятать уведомление
stopForeground(true);
break;
}
}
}
Notification getNotification(int playbackState) {
// MediaStyleHelper заполняет уведомление метаданными трека.
// Хелпер любезно написал Ian Lake / Android Framework Developer at Google
// и выложил здесь: https://gist.github.com/ianhanniballake/47617ec3488e0257325c
NotificationCompat.Builder builder = MediaStyleHelper.from(this, mediaSession);
// Добавляем кнопки
// ...на предыдущий трек
builder.addAction(
new NotificationCompat.Action(
android.R.drawable.ic_media_previous, getString(R.string.previous),
MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)));
// ...play/pause
if (playbackState == PlaybackStateCompat.STATE_PLAYING)
builder.addAction(
new NotificationCompat.Action(
android.R.drawable.ic_media_pause, getString(R.string.pause),
MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_PLAY_PAUSE)));
else
builder.addAction(
new NotificationCompat.Action(
android.R.drawable.ic_media_play, getString(R.string.play),
MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_PLAY_PAUSE)));
// ...на следующий трек
builder.addAction(
new NotificationCompat.Action(android.R.drawable.ic_media_next, getString(R.string.next),
MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_SKIP_TO_NEXT)));
builder.setStyle(new NotificationCompat.MediaStyle()
// В компактном варианте показывать Action с данным порядковым номером.
// В нашем случае это play/pause.
.setShowActionsInCompactView(1)
// Отображать крестик в углу уведомления для его закрытия.
// Это связано с тем, что для API < 21 из-за ошибки во фреймворке
// пользователь не мог смахнуть уведомление foreground-сервиса
// даже после вызова stopForeground(false).
// Так что это костыль.
// На API >= 21 крестик не отображается, там просто смахиваем уведомление.
.setShowCancelButton(true)
// Указываем, что делать при нажатии на крестик или смахивании
.setCancelButtonIntent(
MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_STOP))
// Передаем токен. Это важно для Android Wear. Если токен не передать,
// кнопка на Android Wear будет отображаться, но не будет ничего делать
.setMediaSession(mediaSession.getSessionToken()));
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setColor(ContextCompat.getColor(this, R.color.colorPrimaryDark));
// Не отображать время создания уведомления. В нашем случае это не имеет смысла
builder.setShowWhen(false);
// Это важно. Без этой строчки уведомления не отображаются на Android Wear
// и криво отображаются на самом телефоне.
builder.setPriority(NotificationCompat.PRIORITY_HIGH);
// Не надо каждый раз вываливать уведомление на пользователя
builder.setOnlyAlertOnce(true);
return builder.build();
}
И добавим вызов refreshNotificationAndForegroundStatus(int playbackState) во все коллбэки MediaSession.
Android 4.4
Android 7.1.1
Android Wear
Started service
В принципе у нас уже все работает, но есть засада: наша activity запускает сервис через binding. Соответственно, после того, как activity отцепится от сервиса, он будет уничтожен и музыка остановится. Поэтому нам надо в onPlay добавить
startService(new Intent(getApplicationContext(), PlayerService.class));
Никакой обработки в onStartCommand не надо, наша цель не дать системе убить сервис после onUnbind.
А в onStop добавить
stopSelf();
В случае, если к сервису привязаны клиенты, stopSelf ничего не делает, только взводит флаг, что после onUnbind сервис можно уничтожить. Так что это вполне безопасно.
ACTION_AUDIO_BECOMING_NOISY
Продолжаем полировать сервис. Допустим пользователь слушает музыку в наушниках и выдергивает их. Если эту ситуацию специально не обработать, звук переключится на динамик телефона и его услышат все окружающие. Было бы хорошо в этом случае встать на паузу.
Для этого в Android есть специальный бродкаст AudioManager.ACTION_AUDIO_BECOMING_NOISY.
Добавим в onPlay
registerReceiver(
becomingNoisyReceiver,
new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
В onPause и onStop
unregisterReceiver(becomingNoisyReceiver);
И по факту события встаем на паузу
final BroadcastReceiver becomingNoisyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
mediaSessionCallback.onPause();
}
}
};
Android Auto
Начиная с API 21 появилась возможность интегрировать телефон с экраном в автомобиле. Для этого необходимо поставить приложение Android Auto и подключить телефон к совместимому автомобилю. На экран автомобиля будет выведены крупные контролы для управления навигацией, сообщениями и музыкой. Давайте предложим Android Auto наше приложение в качестве поставщика музыки.
Если у вас под рукой нет совместимого автомобиля, что, согласитесь, иногда бывает, можно просто запустить приложение и экран самого телефона будет работать в качестве автомобильного.
Исходный код выложен на GitHub (ветка MediaBrowserService).
Прежде всего надо указать в манифесте, что наше приложение совместимо с Android Auto.
Добавим в манифест
<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
Здесь automotive_app_desc — это ссылка на файл automotive_app_desc.xml, который надо создать в папке xml
<automotiveApp>
<uses name="media" />
</automotiveApp>
Преобразуем наш сервис в MediaBrowserService. Его задача, помимо всего ранее сделанного, отдавать токен в Android Auto и предоставлять плейлисты.
Поправим декларацию сервиса в манифесте
<service
android:name=".service.PlayerService"
android:exported="true"
tools:ignore="ExportedService" >
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
Во-первых, теперь наш сервис экспортируется, поскольку к нему будут подсоединяться снаружи.
И, во-вторых, добавлен интент-фильтр android.media.browse.MediaBrowserService.
Меняем родительский класс на MediaBrowserServiceCompat.
Поскольку теперь сервис должен отдавать разные IBinder в зависимости от интента, поправим onBind
@Override
public IBinder onBind(Intent intent) {
if (SERVICE_INTERFACE.equals(intent.getAction())) {
return super.onBind(intent);
}
return new PlayerServiceBinder();
}
Имплементируем два абстрактных метода, возвращающие плейлисты
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName,
int clientUid, @Nullable Bundle rootHints)
{
// Здесь мы возвращаем rootId - в нашем случае "Root".
// Значение RootId непринципиально, оно будет просто передано
// в onLoadChildren как parentId.
// Идея здесь в том, что мы можем проверить clientPackageName и
// в зависимости от того, что это за приложение, вернуть ему
// разные плейлисты.
// Если с неким приложением мы не хотим работать вообще,
// можно написать return null;
return new BrowserRoot("Root", null);
}
@Override
public void onLoadChildren(@NonNull String parentId,
@NonNull Result<List<MediaBrowserCompat.MediaItem>> result)
{
// Возвращаем плейлист. Элементы могут быть FLAG_PLAYABLE
// или FLAG_BROWSABLE.
// Элемент FLAG_PLAYABLE нас могут попросить проиграть,
// а FLAG_BROWSABLE отобразится как папка и, если пользователь
// в нее попробует войти, то вызовется onLoadChildren с parentId
// данного browsable-элемента.
// То есть мы можем построить виртуальную древовидную структуру,
// а не просто список треков.
ArrayList<MediaBrowserCompat.MediaItem> data =
new ArrayList<>(musicRepository.getTrackCount());
MediaDescriptionCompat.Builder descriptionBuilder =
new MediaDescriptionCompat.Builder();
for (int i = 0; i < musicRepository.getTrackCount() - 1; i++) {
MusicRepository.Track track = musicRepository.getTrackByIndex(i);
MediaDescriptionCompat description = descriptionBuilder
.setDescription(track.getArtist())
.setTitle(track.getTitle())
.setSubtitle(track.getArtist())
// Картинки отдавать только как Uri
//.setIconBitmap(...)
.setIconUri(new Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.authority(getResources()
.getResourcePackageName(track.getBitmapResId()))
.appendPath(getResources()
.getResourceTypeName(track.getBitmapResId()))
.appendPath(getResources()
.getResourceEntryName(track.getBitmapResId()))
.build())
.setMediaId(Integer.toString(i))
.build();
data.add(new MediaBrowserCompat.MediaItem(description, FLAG_PLAYABLE));
}
result.sendResult(data);
}
И, наконец, имплементируем новый коллбэк MediaSession
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
playTrack(musicRepository.getTrackByIndex(Integer.parseInt(mediaId)));
}
Здесь 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 выглядит следующим образом
AudioAttributes audioAttributes = new AudioAttributes.Builder() // Собираемся воспроизводить звуковой контент // (а не звук уведомления или звонок будильника) .setUsage(AudioAttributes.USAGE_MEDIA) // ...и именно музыку (а не трек фильма или речь) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setOnAudioFocusChangeListener(audioFocusChangeListener) // Если получить фокус не удалось, ничего не делаем // Если true - нам выдадут фокус как только это будет возможно // (например, закончится телефонный разговор) .setAcceptsDelayedFocusGain(false) // Вместо уменьшения громкости собираемся вставать на паузу .setWillPauseWhenDucked(true) .setAudioAttributes(audioAttributes) .build();
Теперь запрашиваем фокус
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { audioFocusResult = audioManager.requestAudioFocus(audioFocusRequest); } else { audioFocusResult = audioManager.requestAudioFocus( audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); }
и освобождаем
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { audioManager.abandonAudioFocusRequest(audioFocusRequest); } else { audioManager.abandonAudioFocus(audioFocusChangeListener); }
Разумеется, все вышеописанные изменения вносить необязательно, старые методы работать не перестали.
Вот мы и добрались до конца. В целом тема эта довольно запутанная. Плюс отличия реализаций на разных API level и у разных производителей. Очень надеюсь, что я ничего не упустил. Но если у вас есть, что исправить и добавить, с удовольствием внесу изменения в статью.
Еще очень рекомендую к просмотру доклад Ian Lake. Доклад от 2015 года, но вполне актуален.
Ура!