Статьи

Пошаговое руководство по созданию приложения для Android Audio Player

Это руководство представляет собой пошаговое руководство по созданию приложения Android Audio с использованием лучших и наиболее эффективных методологий и API. Это длинные уроки, поэтому я разделил их на две отдельные части, если их слишком много, чтобы переварить за одно чтение:

  1. Создание медиаплеера в службе, важной для воспроизведения мультимедиа в фоновом режиме.
  2. Взаимодействие с сервисом через BroadcastReceiver s (PLAY, PAUSE, NEXT, PREVIOUS). Как обрабатывать крайние варианты использования, такие как входящие вызовы, смена аудио выходов (например, снятие наушников)

Часть первая — настройка проекта.

Создайте новый проект в Android Studio и добавьте следующие разрешения в файл AndroidManifest.xml .

 <uses-permission android:name="android.permission.INTERNET" /> <permission android:name="android.permission.MEDIA_CONTENT_CONTROL" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> 

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

Шаг второй — создание службы MediaPlayer

Ядром приложения Audio Player является сервис медиаплеера. Следующий класс является примером этого сервиса. Класс имеет несколько реализаций MediaPlayer для обработки событий, которые могут произойти во время воспроизведения аудио. Последняя реализация из AudioManager.OnAudioFocusChangeListener необходима для обработки запросов на AudioFocus от других приложений, которые хотят воспроизводить медиа-файлы.

 public class MediaPlayerService extends Service implements MediaPlayer.OnCompletionListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnSeekCompleteListener, MediaPlayer.OnInfoListener, MediaPlayer.OnBufferingUpdateListener, AudioManager.OnAudioFocusChangeListener { // Binder given to clients private final IBinder iBinder = new LocalBinder(); @Override public IBinder onBind(Intent intent) { return iBinder; } @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { //Invoked indicating buffering status of //a media resource being streamed over the network. } @Override public void onCompletion(MediaPlayer mp) { //Invoked when playback of a media source has completed. } //Handle errors @Override public boolean onError(MediaPlayer mp, int what, int extra) { //Invoked when there has been an error during an asynchronous operation. return false; } @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { //Invoked to communicate some info. return false; } @Override public void onPrepared(MediaPlayer mp) { //Invoked when the media source is ready for playback. } @Override public void onSeekComplete(MediaPlayer mp) { //Invoked indicating the completion of a seek operation. } @Override public void onAudioFocusChange(int focusChange) { //Invoked when the audio focus of the system is updated. } public class LocalBinder extends Binder { public MediaPlayerService getService() { return MediaPlayerService.this; } } } 

Приведенный выше код является шаблоном всех методов, которые будут обрабатывать события MediaPlayer . Единственный завершенный код — это привязка Service . Вам необходимо связать этот сервис, потому что он взаимодействует с активностью для получения аудиофайлов. Вы можете больше полагаться на связанные услуги в документации .

Объявите Service в файле AndroidManifest.xml

 <application <service android:name=".MediaPlayerService" /> ... </application> 

Шаг третий — Построение MediaPlayer

Мультимедийный каркас Android поддерживает множество распространенных типов мультимедиа. Одним из ключевых компонентов этой платформы является класс MediaPlayer , который с минимальными настройками можно использовать для воспроизведения аудио и видео. Вы можете найти базовый пример реализации MediaPlayer в документации , но вам потребуется больше, чем этот пример Service для воспроизведения медиа. Далее я опишу необходимые методы, которые необходимо настроить в классе MediaPlayerService .

Создайте следующие глобальные экземпляры MediaPlayer и путь String аудио в классе Service .

 private MediaPlayer mediaPlayer; //path to the audio file private String mediaFile; 

Теперь инициализируйте mediaPlayer :

 private void initMediaPlayer() { mediaPlayer = new MediaPlayer(); //Set up MediaPlayer event listeners mediaPlayer.setOnCompletionListener(this); mediaPlayer.setOnErrorListener(this); mediaPlayer.setOnPreparedListener(this); mediaPlayer.setOnBufferingUpdateListener(this); mediaPlayer.setOnSeekCompleteListener(this); mediaPlayer.setOnInfoListener(this); //Reset so that the MediaPlayer is not pointing to another data source mediaPlayer.reset(); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); try { // Set the data source to the mediaFile location mediaPlayer.setDataSource(mediaFile); } catch (IOException e) { e.printStackTrace(); stopSelf(); } mediaPlayer.prepareAsync(); } 

При работе с мультимедиа необходимо реализовать некоторые функции для обработки основных действий при воспроизведении мультимедиа. Этими основными функциями являются воспроизведение, остановка, пауза и возобновление.

Сначала добавьте еще одну глобальную переменную для хранения позиции паузы / возобновления.

 //Used to pause/resume MediaPlayer private int resumePosition; 

Добавьте операторы if чтобы убедиться в отсутствии проблем при воспроизведении мультимедиа.

 private void playMedia() { if (!mediaPlayer.isPlaying()) { mediaPlayer.start(); } } private void stopMedia() { if (mediaPlayer == null) return; if (mediaPlayer.isPlaying()) { mediaPlayer.stop(); } } private void pauseMedia() { if (mediaPlayer.isPlaying()) { mediaPlayer.pause(); resumePosition = mediaPlayer.getCurrentPosition(); } } private void resumeMedia() { if (!mediaPlayer.isPlaying()) { mediaPlayer.seekTo(resumePosition); mediaPlayer.start(); } } 

Теперь, когда вы создали функции инициализации, пришло время реализовать методы @Override в начальном шаблоне Service . Эти методы важны для MediaPlayer потому что все ключевые действия, которые будет выполнять проигрыватель, будут вызываться из этих методов. Замените оригинальные методы в шаблоне Service следующими.

 @Override public void onCompletion(MediaPlayer mp) { //Invoked when playback of a media source has completed. stopMedia(); //stop the service stopSelf(); } //Handle errors @Override public boolean onError(MediaPlayer mp, int what, int extra) { //Invoked when there has been an error during an asynchronous operation switch (what) { case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK: Log.d("MediaPlayer Error", "MEDIA ERROR NOT VALID FOR PROGRESSIVE PLAYBACK " + extra); break; case MediaPlayer.MEDIA_ERROR_SERVER_DIED: Log.d("MediaPlayer Error", "MEDIA ERROR SERVER DIED " + extra); break; case MediaPlayer.MEDIA_ERROR_UNKNOWN: Log.d("MediaPlayer Error", "MEDIA ERROR UNKNOWN " + extra); break; } return false; } @Override public void onPrepared(MediaPlayer mp) { //Invoked when the media source is ready for playback. playMedia(); } 

Примечание . В исходном шаблоне Service реализовано больше методов @Override . Они полезны в определенных событиях MediaPlayer , но так как основное внимание в этом руководстве уделяется созданию универсального медиаплеера, я не буду их реализовывать.

Шаг четвертый — Обработка аудио фокуса

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

Чтобы обеспечить этот хороший пользовательский опыт, MediaPlayerService должен будет обрабатывать события AudioFocus и они обрабатываются в последнем методе переопределения onAudioFocusChange() . Этот метод является оператором switch с событиями фокуса как его case: s. Имейте в виду, что этот метод переопределения вызывается после того, как запрос на AudioFocus был сделан из системы или другого мультимедийного приложения.

Что происходит в каждом case:

  • AudioManager.AUDIOFOCUS_GAIN — Сервис получил аудио-фокус, поэтому он должен начать воспроизведение.
  • AudioManager.AUDIOFOCUS_LOSS — сервис потерял фокус на аудио, пользователь, вероятно, перешел на воспроизведение мультимедиа в другом приложении, поэтому выпустите медиаплеер.
  • AudioManager.AUDIOFOCUS_LOSS_TRANSIENT — Fucos потерян на короткое время, приостановите MediaPlayer .
  • AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK — Потеря фокуса на короткое время, возможно, на устройство поступило уведомление, уменьшите громкость воспроизведения.

В дополнение к методам переопределения вам нужны две другие функции для запроса и освобождения фокуса аудио от MediaPlayer . Следующий кодовый блок содержит все методы фокусировки звука, описанные выше. Я взял onAudioFocusChange() из документации разработчика Android и внес некоторые изменения, но этот код выполняет работу для этого примера.

Сначала добавьте новую глобальную переменную в класс Service .

 private AudioManager audioManager; 

Замените метод onAudioFocusChange() следующим и добавьте функции, которые он использует.

 @Override public void onAudioFocusChange(int focusState) { //Invoked when the audio focus of the system is updated. switch (focusState) { case AudioManager.AUDIOFOCUS_GAIN: // resume playback if (mediaPlayer == null) initMediaPlayer(); else if (!mediaPlayer.isPlaying()) mediaPlayer.start(); mediaPlayer.setVolume(1.0f, 1.0f); break; case AudioManager.AUDIOFOCUS_LOSS: // Lost focus for an unbounded amount of time: stop playback and release media player if (mediaPlayer.isPlaying()) mediaPlayer.stop(); mediaPlayer.release(); mediaPlayer = null; break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // Lost focus for a short time, but we have to stop // playback. We don't release the media player because playback // is likely to resume if (mediaPlayer.isPlaying()) mediaPlayer.pause(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // Lost focus for a short time, but it's ok to keep playing // at an attenuated level if (mediaPlayer.isPlaying()) mediaPlayer.setVolume(0.1f, 0.1f); break; } } private boolean requestAudioFocus() { audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { //Focus gained return true; } //Could not gain focus return false; } private boolean removeAudioFocus() { return AudioManager.AUDIOFOCUS_REQUEST_GRANTED == audioManager.abandonAudioFocus(this); } 

Если вы хотите узнать больше об аудио-фокусе, то статья SitePoint имеет отличный учебник.

Шаг пятый — Методы жизненного цикла обслуживания

В этом разделе я остановлюсь на жизненном цикле Service . Эти методы имеют решающее значение для MediaPlayer поскольку жизненный цикл Service тесно связан с MediaPlayer . Эти методы будут обрабатывать инициализацию и управление ресурсами для MediaPlayer .

У меня есть встроенные комментарии, чтобы было легче понять.

 //The system calls this method when an activity, requests the service be started @Override public int onStartCommand(Intent intent, int flags, int startId) { try { //An audio file is passed to the service through putExtra(); mediaFile = intent.getExtras().getString("media"); } catch (NullPointerException e) { stopSelf(); } //Request audio focus if (requestAudioFocus() == false) { //Could not gain focus stopSelf(); } if (mediaFile != null && mediaFile != "") initMediaPlayer(); return super.onStartCommand(intent, flags, startId); } 

onStartCommand() обрабатывает инициализацию MediaPlayer и запрос фокуса, чтобы убедиться, что другие приложения не воспроизводят мультимедиа. В onStartCommand() я добавил дополнительный блок try-catch чтобы убедиться, что метод getExtras() не getExtras() NullPointerException .

Еще один важный метод, который вам нужно реализовать — это onDestroy() . В этом методе ресурсы MediaPlayer должны быть освобождены, так как эта служба собирается быть разрушенной, и приложение не должно контролировать медиаресурсы.

 @Override public void onDestroy() { super.onDestroy(); if (mediaPlayer != null) { stopMedia(); mediaPlayer.release(); } removeAudioFocus(); } 

Метод onDestroy() также освобождает аудио-фокус, это скорее личный выбор. Если вы освободите фокус в этом методе, MediaPlayerService будет иметь фокус аудио до уничтожения, если нет никаких прерываний от других приложений мультимедиа для фокусировки аудио.

Если вы хотите более динамическое управление фокусировкой, вы можете запросить фокусировку звука, когда начинается воспроизведение нового мультимедиа, и освободить его в методе onCompletion() , чтобы у службы было управление фокусировкой только во время воспроизведения чего-либо.

Шаг шестой — привязка аудиоплеера

В этом разделе я расскажу о последних шагах, чтобы связать класс MediaPlayerService с MainActivity и предоставить ему аудио для воспроизведения. Вы должны привязать Service к Activity чтобы они могли взаимодействовать друг с другом.

Добавьте следующие глобальные переменные в класс MainActivity .

 private MediaPlayerService player; boolean serviceBound = false; 

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

Для обработки привязки Service добавьте следующее в класс MainActivity .

 //Binding this Client to the AudioPlayer Service private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { // We've bound to LocalService, cast the IBinder and get LocalService instance MediaPlayerService.LocalBinder binder = (MediaPlayerService.LocalBinder) service; player = binder.getService(); serviceBound = true; Toast.makeText(MainActivity.this, "Service Bound", Toast.LENGTH_SHORT).show(); } @Override public void onServiceDisconnected(ComponentName name) { serviceBound = false; } }; 

Теперь пришло время воспроизвести аудио. Следующая функция создает новый экземпляр MediaPlayerService и отправляет мультимедийный файл для воспроизведения, поэтому добавьте его в MainActivity .

 private void playAudio(String media) { //Check is service is active if (!serviceBound) { Intent playerIntent = new Intent(this, MediaPlayerService.class); playerIntent.putExtra("media", media); startService(playerIntent); bindService(playerIntent, serviceConnection, Context.BIND_AUTO_CREATE); } else { //Service is active //Send media with BroadcastReceiver } } 

Функция playAudio() не завершена. Я вернусь к этому позже при отправке мультимедийных файлов на Service с BroadcastReceiver .

Вызовите функцию onCreate() метода onCreate() Activity и onCreate() ссылку на аудиофайл.

 playAudio("https://upload.wikimedia.org/wikipedia/commons/6/6c/Grieg_Lyric_Pieces_Kobold.ogg"); 

Шаг седьмой — Методы жизненного цикла активности

В этом разделе я расскажу об основных, но важных реализациях MainActivity жизненного цикла MainActivity . Если вы вызываете функцию onCreate() метода onCreate() Activity Service начнет воспроизведение, но приложение может легко аварийно завершить работу.

Добавьте следующие методы в MainActivity чтобы исправить это. Все эти методы сохраняют и восстанавливают состояние переменной serviceBound и отменяют привязку Service когда пользователь закрывает приложение.

 @Override public void onSaveInstanceState(Bundle savedInstanceState) { savedInstanceState.putBoolean("ServiceState", serviceBound); super.onSaveInstanceState(savedInstanceState); } @Override public void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); serviceBound = savedInstanceState.getBoolean("ServiceState"); } @Override protected void onDestroy() { super.onDestroy(); if (serviceBound) { unbindService(serviceConnection); //service is active player.stopSelf(); } } 

Дополнительно — Загрузка локальных аудио файлов

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

Создайте новый класс Java, используемый в качестве аудиообъекта. Следующий класс содержит важную информацию, необходимую для аудиофайла, но вы можете добавить больше, если это необходимо.

 public class Audio implements Serializable { private String data; private String title; private String album; private String artist; public Audio(String data, String title, String album, String artist) { this.data = data; this.title = title; this.album = album; this.artist = artist; } public String getData() { return data; } public void setData(String data) { this.data = data; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getAlbum() { return album; } public void setAlbum(String album) { this.album = album; } public String getArtist() { return artist; } public void setArtist(String artist) { this.artist = artist; } } 

Добавьте разрешение в AndroidManifest.xml .

 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> 

Это необходимо для загрузки локальных мультимедийных файлов с устройства Android.

В классе MainActivity создайте глобальный ArrayList объектов Audio .

 ArrayList<Audio> audioList; 

Чтобы получить данные с устройства, добавьте следующую функцию в MainActivity . Он получает данные с устройства в порядке возрастания.

 private void loadAudio() { ContentResolver contentResolver = getContentResolver(); Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; String selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0"; String sortOrder = MediaStore.Audio.Media.TITLE + " ASC"; Cursor cursor = contentResolver.query(uri, null, selection, null, sortOrder); if (cursor != null && cursor.getCount() > 0) { audioList = new ArrayList<>(); while (cursor.moveToNext()) { String data = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)); String title = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)); String album = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM)); String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)); // Save to audioList audioList.add(new Audio(data, title, album, artist)); } } cursor.close(); } 

После получения данных с устройства playAudio() может воспроизводить их в Service .

В MainActivity onCreate() добавьте следующий код. Убедитесь, что у вас есть хотя бы одна звуковая дорожка, которую может воспроизводить служба, иначе приложение вылетит.

 loadAudio(); //play the first audio in the ArrayList playAudio(audioList.get(0).getData()); 

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

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

Ключевым компонентом для взаимодействия с фоновыми сервисами является BroadcastReceiver .

Что такое BroadcastReceiver ?

Системные компоненты и приложения Android совершают системные вызовы через намерения с помощью методов sendBroadcast() , sendStickyBroadcast() или sendOrderedBroadcast() для уведомления заинтересованных приложений. Цели трансляции могут быть полезны для обеспечения системы обмена сообщениями и событиями между компонентами приложения или могут использоваться системой Android для уведомления заинтересованных приложений о ключевых системных событиях. Зарегистрированные BroadcastReceiver перехватывает эти события, передаваемые по всей системе Android. Цель BroadcastReceiver состоит в том, чтобы ждать определенных событий и реагировать на эти события, но BroadcastReceiver не реагирует на все входящие события, только на определенные события. Когда BroadcastReceiver обнаруживает соответствующее намерение, он вызывает его onReceive() для его обработки.

Вы можете зарегистрировать BroadcastReceiver двумя способами: статически в AndroidManifest.xml или динамически, используя метод registerReceiver() во время выполнения.

В этом руководстве BroadcastReceiver создаются динамически, поскольку важно, чтобы MediaPlayerService прослушивал события только тогда, когда проигрыватель активен. Это не очень удобно для пользователя, если приложение начинает воспроизводить звук неожиданно после запуска события. Если вы регистрируете получателя, вы должны отменить его регистрацию, когда он больше не нужен.

Вернемся к приложению Audio Player

Для более полного звукового приложения я добавил RecyclerView и с помощью функции loadAudio() загрузил локальные аудиофайлы в RecyclerView . Я также внес изменения в цветовую схему и макет. Я не буду вдаваться в подробности, описывающие процесс добавления RecyclerView в приложение, но вы можете увидеть конечный результат на GitHub .

Если вы хотите узнать больше о RecyclerView , то прочитайте мою статью .

Другим изменением является функция onStartCommand() метод onStartCommand() , но я вернусь к этим изменениям позже и сосредоточусь на событиях BroadcastReceiver и взаимодействии пользователя с Service .

Смена аудиовыходов (наушники сняты)

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

В классе MediaPlayerService создайте BroadcastReceiver который прослушивает ACTION_AUDIO_BECOMING_NOISY , что означает, что звук собирается стать «шумным» из-за изменения в аудиовыходах. Добавьте следующие функции в класс обслуживания.

 //Becoming noisy private BroadcastReceiver becomingNoisyReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { //pause audio on ACTION_AUDIO_BECOMING_NOISY pauseMedia(); buildNotification(PlaybackStatus.PAUSED); } }; private void registerBecomingNoisyReceiver() { //register after getting audio focus IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); registerReceiver(becomingNoisyReceiver, intentFilter); } 

Экземпляр BroadcastReceiver приостановит MediaPlayer когда система ACTION_AUDIO_BECOMING_NOISY вызов ACTION_AUDIO_BECOMING_NOISY . Чтобы сделать BroadcastReceiver доступным, вы должны зарегистрировать его. Функция registerBecomingNoisyReceiver() обрабатывает это и указывает действие намерения BECOMING_NOISY которое будет запускать этот BroadcastReceiver .

Вы еще не реализовали buildNotification() , так что не беспокойтесь, когда он показывает ошибку.

Обработка входящих звонков

Следующие функции позволяют избежать воспроизведения звука во время разговора, что будет ужасно для пользователя.

Сначала создайте следующие глобальные переменные в классе MediaPlayerService .

 //Handle incoming phone calls private boolean ongoingCall = false; private PhoneStateListener phoneStateListener; private TelephonyManager telephonyManager; 

Добавьте следующую функцию.

 //Handle incoming phone calls private void callStateListener() { // Get the telephony manager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); //Starting listening for PhoneState changes phoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String incomingNumber) { switch (state) { //if at least one call exists or the phone is ringing //pause the MediaPlayer case TelephonyManager.CALL_STATE_OFFHOOK: case TelephonyManager.CALL_STATE_RINGING: if (mediaPlayer != null) { pauseMedia(); ongoingCall = true; } break; case TelephonyManager.CALL_STATE_IDLE: // Phone idle. Start playing. if (mediaPlayer != null) { if (ongoingCall) { ongoingCall = false; resumeMedia(); } } break; } } }; // Register the listener with the telephony manager // Listen for changes to the device call state. telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } 

Функция callStateListener() является реализацией PhoneStateListener который прослушивает изменения состояния TelephonyManager . TelephonyManager обеспечивает доступ к информации об услугах телефонии на устройстве, прослушивает изменения состояния вызова устройства и реагирует на эти изменения.

Переопределить методы

Я упомянул, что я изменил методы, описанные ранее в этой статье. Я также внес изменения в способ передачи аудиофайлов в Service . Аудиофайлы загружаются с устройства с помощью функции loadAudio() . Когда пользователь хочет воспроизвести аудио, вызовите playAudio(int audioIndex) с индексом требуемого аудио из ArrayList загруженных аудиофайлов.

При playAudio() вызове функции playAudio() ArrayList сохраняется в SharedPreferences вместе с номером индекса аудио, а когда MediaPlayerService хочет воспроизвести новый звук, он загружает его из SharedPreferences . Это один из способов загрузки массива Audio в Service , но есть и другие.

Откройте build.gradle (приложение) и добавьте зависимости для библиотеки Gson .

 dependencies { ... compile group: 'com.google.code.gson', name: 'gson', version: '2.7', changing: true } 

Следующий класс обрабатывает хранение данных.

 public class StorageUtil { private final String STORAGE = " com.valdioveliu.valdio.audioplayer.STORAGE"; private SharedPreferences preferences; private Context context; public StorageUtil(Context context) { this.context = context; } public void storeAudio(ArrayList<Audio> arrayList) { preferences = context.getSharedPreferences(STORAGE, Context.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); Gson gson = new Gson(); String json = gson.toJson(arrayList); editor.putString("audioArrayList", json); editor.apply(); } public ArrayList<Audio> loadAudio() { preferences = context.getSharedPreferences(STORAGE, Context.MODE_PRIVATE); Gson gson = new Gson(); String json = preferences.getString("audioArrayList", null); Type type = new TypeToken<ArrayList<Audio>>() { }.getType(); return gson.fromJson(json, type); } public void storeAudioIndex(int index) { preferences = context.getSharedPreferences(STORAGE, Context.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.putInt("audioIndex", index); editor.apply(); } public int loadAudioIndex() { preferences = context.getSharedPreferences(STORAGE, Context.MODE_PRIVATE); return preferences.getInt("audioIndex", -1);//return -1 if no data found } public void clearCachedAudioPlaylist() { preferences = context.getSharedPreferences(STORAGE, Context.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.clear(); editor.commit(); } } 

Теперь пришло время изменить playAudio() . Сначала в классе MainActivity создайте глобальную статическую String .

 public static final String Broadcast_PLAY_NEW_AUDIO = "com.valdioveliu.valdio.audioplayer.PlayNewAudio"; // Change to your package name 

Эта строка отправляет широковещательные намерения в MediaPlayerService что пользователь хочет воспроизвести новое аудио, и обновил кэшированный индекс аудио, которое он хочет воспроизвести. BroadcastReceiver который обрабатывает это намерение, еще не создан, на данный момент замените вашу старую playAudio() в MainActivity следующим.

 private void playAudio(int audioIndex) { //Check is service is active if (!serviceBound) { //Store Serializable audioList to SharedPreferences StorageUtil storage = new StorageUtil(getApplicationContext()); storage.storeAudio(audioList); storage.storeAudioIndex(audioIndex); Intent playerIntent = new Intent(this, MediaPlayerService.class); startService(playerIntent); bindService(playerIntent, serviceConnection, Context.BIND_AUTO_CREATE); } else { //Store the new audioIndex to SharedPreferences StorageUtil storage = new StorageUtil(getApplicationContext()); storage.storeAudioIndex(audioIndex); //Service is active //Send a broadcast to the service -> PLAY_NEW_AUDIO Intent broadcastIntent = new Intent(Broadcast_PLAY_NEW_AUDIO); sendBroadcast(broadcastIntent); } } 

Аудио не передается putExtra() через putExtra() , поэтому Service должен загрузить данные из SharedPreferences и поэтому метод onStartCommand() необходимо переписать. Я вернусь к этому методу в конце этого урока, чтобы дать полную реализацию onStartCommand() . А пока добавьте следующие глобальные переменные в класс MediaPlayerService .

 //List of available Audio files private ArrayList<Audio> audioList; private int audioIndex = -1; private Audio activeAudio; //an object of the currently playing audio 

Воспроизвести новую аудио трансляцию

Когда MediaPlayerService что-то воспроизводит, а пользователь хочет воспроизвести новую дорожку, вы должны уведомить службу о том, что ей нужно перейти на новое аудио. Service нужен способ прослушивать эти вызовы «play new Audio» и реагировать на них. Как? Еще один BroadcastReceiver . Я упомянул эти вызовы «play new Audio» в разделе « Переопределить методы » при вызове функции playAudio() .

В классе MediaPlayerService добавьте следующие функции.

 private BroadcastReceiver playNewAudio = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { //Get the new media index form SharedPreferences audioIndex = new StorageUtil(getApplicationContext()).loadAudioIndex(); if (audioIndex != -1 && audioIndex < audioList.size()) { //index is in a valid range activeAudio = audioList.get(audioIndex); } else { stopSelf(); } //A PLAY_NEW_AUDIO action received //reset mediaPlayer to play the new Audio stopMedia(); mediaPlayer.reset(); initMediaPlayer(); updateMetaData(); buildNotification(PlaybackStatus.PLAYING); } }; private void register_playNewAudio() { //Register playNewMedia receiver IntentFilter filter = new IntentFilter(MainActivity.Broadcast_PLAY_NEW_AUDIO); registerReceiver(playNewAudio, filter); } 

При перехвате намерения PLAY_NEW_AUDIO этот BroadcastReceiver загружает обновленный индекс и обновляет объект activeAudio на новом носителе, а MediaPlayer сбрасывается для воспроизведения нового звука. Функция buildNotification() еще не реализована, поэтому buildNotification() ошибку.

Регистрация BroadcastReceiver S

В onCreate() Service добавьте регистрационные вызовы для BroadcastReceiver .

 @Override public void onCreate() { super.onCreate(); // Perform one-time setup procedures // Manage incoming phone calls during playback. // Pause MediaPlayer on incoming call, // Resume on hangup. callStateListener(); //ACTION_AUDIO_BECOMING_NOISY -- change in audio outputs -- BroadcastReceiver registerBecomingNoisyReceiver(); //Listen for new Audio to play -- BroadcastReceiver register_playNewAudio(); } 

Вы должны отменить регистрацию всех зарегистрированных BroadcastReceiver когда они больше не нужны. Это происходит в onDestroy() Service . Замените ваш текущий onDestroy() следующим. Опять же, не беспокойтесь об removeNotification() , это будет реализовано позже.

 @Override public void onDestroy() { super.onDestroy(); if (mediaPlayer != null) { stopMedia(); mediaPlayer.release(); } removeAudioFocus(); //Disable the PhoneStateListener if (phoneStateListener != null) { telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); } removeNotification(); //unregister BroadcastReceivers unregisterReceiver(becomingNoisyReceiver); unregisterReceiver(playNewAudio); //clear cached playlist new StorageUtil(getApplicationContext()).clearCachedAudioPlaylist(); } 

Когда Service уничтожен, он должен прекратить прослушивание входящих вызовов и освободить ресурсы TelephonyManager . Еще одна последняя вещь, которую Service обрабатывает перед уничтожением, — это очистка данных, хранящихся в SharedPreferences .

Взаимодействие с пользователем

Взаимодействие с MediaPlayerService является одной из ключевых функций приложения аудиоплеера, поскольку пользователям не нужно воспроизводить мультимедиа, но также необходимо иметь контроль над приложением. Это не так просто, как кажется при работе с фоновыми службами, поскольку в фоновых потоках отсутствует пользовательский интерфейс. Android Lollipop представил новые функции, в том числе уведомления Android MediaStyle .

Notification.MediaStyle позволяет добавлять мультимедийные кнопки без необходимости создания пользовательских уведомлений. В этом примере я буду использовать библиотеку поддержки MediaStyle NotificationCompat.MediaStyle для поддержки более старых версий Android.

Чтобы получить полный контроль над воспроизведением мультимедиа в MediaPlayerService , необходимо создать экземпляр MediaSession . MediaSession позволяет взаимодействовать с контроллерами мультимедиа, клавишами громкости, мультимедийными кнопками и элементами управления транспортом. Приложение создает экземпляр MediaSession когда оно хочет опубликовать информацию о воспроизведении мультимедиа или обработать ключи мультимедиа.

Чтобы создать уведомление MediaStyle для этого примера, MediaPlayerService будет использовать MediaSession управления транспортом MediaSession для добавления элементов управления уведомлениями и публикации MetaData чтобы система Android знала, что она воспроизводит звук.

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

 public static final String ACTION_PLAY = "com.valdioveliu.valdio.audioplayer.ACTION_PLAY"; public static final String ACTION_PAUSE = "com.valdioveliu.valdio.audioplayer.ACTION_PAUSE"; public static final String ACTION_PREVIOUS = "com.valdioveliu.valdio.audioplayer.ACTION_PREVIOUS"; public static final String ACTION_NEXT = "com.valdioveliu.valdio.audioplayer.ACTION_NEXT"; public static final String ACTION_STOP = "com.valdioveliu.valdio.audioplayer.ACTION_STOP"; //MediaSession private MediaSessionManager mediaSessionManager; private MediaSessionCompat mediaSession; private MediaControllerCompat.TransportControls transportControls; //AudioPlayer notification ID private static final int NOTIFICATION_ID = 101; 

Переменные String используются для уведомления о том, какое действие вызвано из MediaSession обратного вызова MediaSession . Остальные экземпляры относятся к MediaSession и идентификатору уведомления для уникальной идентификации уведомления MediaStyle.

Следующие функции обрабатывают инициализацию MediaSession и устанавливают MetaData для активного сеанса. Важной частью следующей функции initMediaSession() является настройка MediaSession вызовов MediaSession для обработки событий, поступающих от кнопок уведомлений.

Добавьте следующие функции в классе MediaPlayerService .

 private void initMediaSession() throws RemoteException { if (mediaSessionManager != null) return; //mediaSessionManager exists mediaSessionManager = (MediaSessionManager) getSystemService(Context.MEDIA_SESSION_SERVICE); // Create a new MediaSession mediaSession = new MediaSessionCompat(getApplicationContext(), "AudioPlayer"); //Get MediaSessions transport controls transportControls = mediaSession.getController().getTransportControls(); //set MediaSession -> ready to receive media commands mediaSession.setActive(true); //indicate that the MediaSession handles transport control commands // through its MediaSessionCompat.Callback. mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); //Set mediaSession's MetaData updateMetaData(); // Attach Callback to receive MediaSession updates mediaSession.setCallback(new MediaSessionCompat.Callback() { // Implement callbacks @Override public void onPlay() { super.onPlay(); resumeMedia(); buildNotification(PlaybackStatus.PLAYING); } @Override public void onPause() { super.onPause(); pauseMedia(); buildNotification(PlaybackStatus.PAUSED); } @Override public void onSkipToNext() { super.onSkipToNext(); skipToNext(); updateMetaData(); buildNotification(PlaybackStatus.PLAYING); } @Override public void onSkipToPrevious() { super.onSkipToPrevious(); skipToPrevious(); updateMetaData(); buildNotification(PlaybackStatus.PLAYING); } @Override public void onStop() { super.onStop(); removeNotification(); //Stop the service stopSelf(); } @Override public void onSeekTo(long position) { super.onSeekTo(position); } }); } private void updateMetaData() { Bitmap albumArt = BitmapFactory.decodeResource(getResources(), R.drawable.image); //replace with medias albumArt // Update the current metadata mediaSession.setMetadata(new MediaMetadataCompat.Builder() .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt) .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, activeAudio.getArtist()) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, activeAudio.getAlbum()) .putString(MediaMetadataCompat.METADATA_KEY_TITLE, activeAudio.getTitle()) .build()); } 

У updateMetaData()метода есть Bitmapизображение, которое нужно создать, поэтому добавьте изображение в папку для рисования проекта. В Callback()переопределяют методы делают использование функций ключевых медиаплеера описывалось ранее. Затем добавьте функции медиаплеера, упомянутые ранее, в Service.

 private void skipToNext() { if (audioIndex == audioList.size() - 1) { //if last in playlist audioIndex = 0; activeAudio = audioList.get(audioIndex); } else { //get next in playlist activeAudio = audioList.get(++audioIndex); } //Update stored index new StorageUtil(getApplicationContext()).storeAudioIndex(audioIndex); stopMedia(); //reset mediaPlayer mediaPlayer.reset(); initMediaPlayer(); } private void skipToPrevious() { if (audioIndex == 0) { //if first in playlist //set index to the last of audioList audioIndex = audioList.size() - 1; activeAudio = audioList.get(audioIndex); } else { //get previous in playlist activeAudio = audioList.get(--audioIndex); } //Update stored index new StorageUtil(getApplicationContext()).storeAudioIndex(audioIndex); stopMedia(); //reset mediaPlayer mediaPlayer.reset(); initMediaPlayer(); } 

Теперь службе нужен способ создания MediaStyleуведомления, но службе нужен способ отслеживать его состояние воспроизведения. Для этого создайте новое перечисление.

В вашем проекте создайте следующий класс.

 public enum PlaybackStatus { PLAYING, PAUSED } 

Теперь у сервиса есть возможность отслеживать его статус воспроизведения и добавить следующую функцию для создания уведомлений.

 private void buildNotification(PlaybackStatus playbackStatus) { int notificationAction = android.R.drawable.ic_media_pause;//needs to be initialized PendingIntent play_pauseAction = null; //Build a new notification according to the current state of the MediaPlayer if (playbackStatus == PlaybackStatus.PLAYING) { notificationAction = android.R.drawable.ic_media_pause; //create the pause action play_pauseAction = playbackAction(1); } else if (playbackStatus == PlaybackStatus.PAUSED) { notificationAction = android.R.drawable.ic_media_play; //create the play action play_pauseAction = playbackAction(0); } Bitmap largeIcon = BitmapFactory.decodeResource(getResources(), R.drawable.image); //replace with your own image // Create a new Notification NotificationCompat.Builder notificationBuilder = (NotificationCompat.Builder) new NotificationCompat.Builder(this) .setShowWhen(false) // Set the Notification style .setStyle(new NotificationCompat.MediaStyle() // Attach our MediaSession token .setMediaSession(mediaSession.getSessionToken()) // Show our playback controls in the compact notification view. .setShowActionsInCompactView(0, 1, 2)) // Set the Notification color .setColor(getResources().getColor(R.color.colorPrimary)) // Set the large and small icons .setLargeIcon(largeIcon) .setSmallIcon(android.R.drawable.stat_sys_headset) // Set Notification content information .setContentText(activeAudio.getArtist()) .setContentTitle(activeAudio.getAlbum()) .setContentInfo(activeAudio.getTitle()) // Add playback actions .addAction(android.R.drawable.ic_media_previous, "previous", playbackAction(3)) .addAction(notificationAction, "pause", play_pauseAction) .addAction(android.R.drawable.ic_media_next, "next", playbackAction(2)); ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notificationBuilder.build()); } private void removeNotification() { NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(NOTIFICATION_ID); } 

При вызове эта функция создаст уведомление в соответствии с PlaybackStatus.

Основное назначение buildNotification()функции — создание пользовательского интерфейса для уведомлений и настройка всех событий, которые будут срабатывать при нажатии пользователем кнопки уведомления. Вы генерируете действия через PendingIntents из playbackAction()функции. Добавьте это к MediaPlayerService.

 private PendingIntent playbackAction(int actionNumber) { Intent playbackAction = new Intent(this, MediaPlayerService.class); switch (actionNumber) { case 0: // Play playbackAction.setAction(ACTION_PLAY); return PendingIntent.getService(this, actionNumber, playbackAction, 0); case 1: // Pause playbackAction.setAction(ACTION_PAUSE); return PendingIntent.getService(this, actionNumber, playbackAction, 0); case 2: // Next track playbackAction.setAction(ACTION_NEXT); return PendingIntent.getService(this, actionNumber, playbackAction, 0); case 3: // Previous track playbackAction.setAction(ACTION_PREVIOUS); return PendingIntent.getService(this, actionNumber, playbackAction, 0); default: break; } return null; } 

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

 private void handleIncomingActions(Intent playbackAction) { if (playbackAction == null || playbackAction.getAction() == null) return; String actionString = playbackAction.getAction(); if (actionString.equalsIgnoreCase(ACTION_PLAY)) { transportControls.play(); } else if (actionString.equalsIgnoreCase(ACTION_PAUSE)) { transportControls.pause(); } else if (actionString.equalsIgnoreCase(ACTION_NEXT)) { transportControls.skipToNext(); } else if (actionString.equalsIgnoreCase(ACTION_PREVIOUS)) { transportControls.skipToPrevious(); } else if (actionString.equalsIgnoreCase(ACTION_STOP)) { transportControls.stop(); } } 

Эта функция выясняет, какое из действий воспроизведения запущено, и выполняет один из MediaSessionметодов обратного вызова через свои элементы управления транспортировкой. Методы обратного вызова, реализованные в initMediaSession()функции, обрабатывают все MediaPlayerдействия.

Заканчивать

Осталось только определить onStartCommand()метод услуг . Этот метод будет обрабатывать инициализацию MediaSession, то MediaPlayer, загружая кэшированные аудио воспроизведения и создание MediaStyleуведомления. В классе обслуживания замените старый onStartCommand()метод следующим.

 @Override public int onStartCommand(Intent intent, int flags, int startId) { try { //Load data from SharedPreferences StorageUtil storage = new StorageUtil(getApplicationContext()); audioList = storage.loadAudio(); audioIndex = storage.loadAudioIndex(); if (audioIndex != -1 && audioIndex < audioList.size()) { //index is in a valid range activeAudio = audioList.get(audioIndex); } else { stopSelf(); } } catch (NullPointerException e) { stopSelf(); } //Request audio focus if (requestAudioFocus() == false) { //Could not gain focus stopSelf(); } if (mediaSessionManager == null) { try { initMediaSession(); initMediaPlayer(); } catch (RemoteException e) { e.printStackTrace(); stopSelf(); } buildNotification(PlaybackStatus.PLAYING); } //Handle Intent action from MediaSession.TransportControls handleIncomingActions(intent); return super.onStartCommand(intent, flags, startId); } 

В initMediaPlayer()функции замените setDataSource()вызов следующей строкой

 mediaPlayer.setDataSource(activeAudio.getData()); 

Это подводит итог для воспроизведения звука в фоновом сервисе в Android. Теперь запустите приложение и воспроизводите аудио правильно. Вот пример того, как выглядит мое примерное приложение. Я добавил RecyclerViewв приложение, и макет может выглядеть по-другому, но представление уведомлений и элементы управления одинаковы.

Скриншот

Скриншот

Скриншот

Перемотка вперед

Вот и все! Я понимаю, что в этом уроке можно было многое понять и понять, поэтому, если у вас есть какие-либо вопросы или комментарии, сообщите мне об этом ниже.