В статье рассматривается архитектура и 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 года, но вполне актуален.
Ура!
