1 /* 2 * Copyright (c) 2016, The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.car.media.localmediaplayer; 17 18 import android.app.Notification; 19 import android.app.NotificationChannel; 20 import android.app.NotificationManager; 21 import android.app.PendingIntent; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.SharedPreferences; 25 import android.media.AudioManager; 26 import android.media.AudioManager.OnAudioFocusChangeListener; 27 import android.media.MediaDescription; 28 import android.media.MediaMetadata; 29 import android.media.MediaPlayer; 30 import android.media.MediaPlayer.OnCompletionListener; 31 import android.media.session.MediaSession; 32 import android.media.session.MediaSession.QueueItem; 33 import android.media.session.PlaybackState; 34 import android.media.session.PlaybackState.CustomAction; 35 import android.os.Build; 36 import android.os.Bundle; 37 import android.util.Log; 38 39 import com.android.car.media.localmediaplayer.nano.Proto.Playlist; 40 import com.android.car.media.localmediaplayer.nano.Proto.Song; 41 42 // Proto should be available in AOSP. 43 import com.google.protobuf.nano.MessageNano; 44 import com.google.protobuf.nano.InvalidProtocolBufferNanoException; 45 46 import java.io.IOException; 47 import java.io.File; 48 import java.util.ArrayList; 49 import java.util.Base64; 50 import java.util.Collections; 51 import java.util.List; 52 53 /** 54 * TODO: Consider doing all content provider accesses and player operations asynchronously. 55 */ 56 public class Player extends MediaSession.Callback { 57 private static final String TAG = "LMPlayer"; 58 private static final String SHARED_PREFS_NAME = "com.android.car.media.localmediaplayer.prefs"; 59 private static final String CURRENT_PLAYLIST_KEY = "__CURRENT_PLAYLIST_KEY__"; 60 private static final String CHANNEL_ID = "com.android.car.media.localmediaplayer.player"; 61 private static final int NOTIFICATION_ID = 42; 62 private static final int REQUEST_CODE = 94043; 63 64 private static final float PLAYBACK_SPEED = 1.0f; 65 private static final float PLAYBACK_SPEED_STOPPED = 1.0f; 66 private static final long PLAYBACK_POSITION_STOPPED = 0; 67 68 // Note: Queues loop around so next/previous are always available. 69 private static final long PLAYING_ACTIONS = PlaybackState.ACTION_PAUSE 70 | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_SKIP_TO_NEXT 71 | PlaybackState.ACTION_SKIP_TO_PREVIOUS | PlaybackState.ACTION_SKIP_TO_QUEUE_ITEM; 72 73 private static final long PAUSED_ACTIONS = PlaybackState.ACTION_PLAY 74 | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_SKIP_TO_NEXT 75 | PlaybackState.ACTION_SKIP_TO_PREVIOUS; 76 77 private static final long STOPPED_ACTIONS = PlaybackState.ACTION_PLAY 78 | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_SKIP_TO_NEXT 79 | PlaybackState.ACTION_SKIP_TO_PREVIOUS; 80 81 private static final String SHUFFLE = "android.car.media.localmediaplayer.shuffle"; 82 83 private final Context mContext; 84 private final MediaSession mSession; 85 private final AudioManager mAudioManager; 86 private final PlaybackState mErrorState; 87 private final DataModel mDataModel; 88 private final CustomAction mShuffle; 89 90 private List<QueueItem> mQueue; 91 private int mCurrentQueueIdx = 0; 92 private final SharedPreferences mSharedPrefs; 93 94 private NotificationManager mNotificationManager; 95 private Notification.Builder mPlayingNotificationBuilder; 96 private Notification.Builder mPausedNotificationBuilder; 97 98 // TODO: Use multiple media players for gapless playback. 99 private final MediaPlayer mMediaPlayer; 100 Player(Context context, MediaSession session, DataModel dataModel)101 public Player(Context context, MediaSession session, DataModel dataModel) { 102 mContext = context; 103 mDataModel = dataModel; 104 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 105 mSession = session; 106 mSharedPrefs = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); 107 108 mShuffle = new CustomAction.Builder(SHUFFLE, context.getString(R.string.shuffle), 109 R.drawable.shuffle).build(); 110 111 mMediaPlayer = new MediaPlayer(); 112 mMediaPlayer.reset(); 113 mMediaPlayer.setOnCompletionListener(mOnCompletionListener); 114 mErrorState = new PlaybackState.Builder() 115 .setState(PlaybackState.STATE_ERROR, 0, 0) 116 .setErrorMessage(context.getString(R.string.playback_error)) 117 .build(); 118 119 mNotificationManager = 120 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 121 122 // There are 2 forms of the media notification, when playing it needs to show the controls 123 // to pause & skip whereas when paused it needs to show controls to play & skip. Setup 124 // pre-populated builders for both of these up front. 125 Notification.Action prevAction = makeNotificationAction( 126 LocalMediaBrowserService.ACTION_PREV, R.drawable.ic_prev, R.string.prev); 127 Notification.Action nextAction = makeNotificationAction( 128 LocalMediaBrowserService.ACTION_NEXT, R.drawable.ic_next, R.string.next); 129 Notification.Action playAction = makeNotificationAction( 130 LocalMediaBrowserService.ACTION_PLAY, R.drawable.ic_play, R.string.play); 131 Notification.Action pauseAction = makeNotificationAction( 132 LocalMediaBrowserService.ACTION_PAUSE, R.drawable.ic_pause, R.string.pause); 133 134 // While playing, you need prev, pause, next. 135 mPlayingNotificationBuilder = new Notification.Builder(context) 136 .setVisibility(Notification.VISIBILITY_PUBLIC) 137 .setSmallIcon(R.drawable.ic_sd_storage_black) 138 .addAction(prevAction) 139 .addAction(pauseAction) 140 .addAction(nextAction); 141 142 // While paused, you need prev, play, next. 143 mPausedNotificationBuilder = new Notification.Builder(context) 144 .setVisibility(Notification.VISIBILITY_PUBLIC) 145 .setSmallIcon(R.drawable.ic_sd_storage_black) 146 .addAction(prevAction) 147 .addAction(playAction) 148 .addAction(nextAction); 149 150 createNotificationChannel(); 151 } 152 makeNotificationAction(String action, int iconId, int stringId)153 private Notification.Action makeNotificationAction(String action, int iconId, int stringId) { 154 PendingIntent intent = PendingIntent.getBroadcast(mContext, REQUEST_CODE, 155 new Intent(action), 156 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); 157 Notification.Action notificationAction = new Notification.Action.Builder(iconId, 158 mContext.getString(stringId), intent) 159 .build(); 160 return notificationAction; 161 } 162 requestAudioFocus(Runnable onSuccess)163 private boolean requestAudioFocus(Runnable onSuccess) { 164 int result = mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC, 165 AudioManager.AUDIOFOCUS_GAIN); 166 if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 167 onSuccess.run(); 168 return true; 169 } 170 Log.e(TAG, "Failed to acquire audio focus"); 171 return false; 172 } 173 174 @Override onPlay()175 public void onPlay() { 176 super.onPlay(); 177 if (Log.isLoggable(TAG, Log.DEBUG)) { 178 Log.d(TAG, "onPlay"); 179 } 180 // Check permissions every time we try to play 181 if (!Utils.hasRequiredPermissions(mContext)) { 182 setMissingPermissionError(); 183 } else { 184 requestAudioFocus(() -> resumePlayback()); 185 } 186 } 187 188 @Override onPause()189 public void onPause() { 190 super.onPause(); 191 if (Log.isLoggable(TAG, Log.DEBUG)) { 192 Log.d(TAG, "onPause"); 193 } 194 pausePlayback(); 195 mAudioManager.abandonAudioFocus(mAudioFocusListener); 196 } 197 destroy()198 public void destroy() { 199 stopPlayback(); 200 mNotificationManager.cancelAll(); 201 mAudioManager.abandonAudioFocus(mAudioFocusListener); 202 mMediaPlayer.release(); 203 } 204 saveState()205 public void saveState() { 206 if (mQueue == null || mQueue.isEmpty()) { 207 return; 208 } 209 210 Playlist playlist = new Playlist(); 211 playlist.songs = new Song[mQueue.size()]; 212 213 int idx = 0; 214 for (QueueItem item : mQueue) { 215 Song song = new Song(); 216 song.queueId = item.getQueueId(); 217 MediaDescription description = item.getDescription(); 218 song.mediaId = description.getMediaId(); 219 song.title = description.getTitle().toString(); 220 song.subtitle = description.getSubtitle().toString(); 221 song.path = description.getExtras().getString(DataModel.PATH_KEY); 222 223 playlist.songs[idx] = song; 224 idx++; 225 } 226 playlist.currentQueueId = mQueue.get(mCurrentQueueIdx).getQueueId(); 227 playlist.currentSongPosition = mMediaPlayer.getCurrentPosition(); 228 playlist.name = CURRENT_PLAYLIST_KEY; 229 230 // Go to Base64 to ensure that we can actually store the string in a sharedpref. This is 231 // slightly wasteful because of the fact that base64 expands the size a bit but it's a 232 // lot less riskier than abusing the java string to directly store bytes coming out of 233 // proto encoding. 234 String serialized = Base64.getEncoder().encodeToString(MessageNano.toByteArray(playlist)); 235 SharedPreferences.Editor editor = mSharedPrefs.edit(); 236 editor.putString(CURRENT_PLAYLIST_KEY, serialized); 237 editor.commit(); 238 } 239 setMissingPermissionError()240 private void setMissingPermissionError() { 241 Intent prefsIntent = new Intent(); 242 prefsIntent.setClass(mContext, PermissionsActivity.class); 243 prefsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 244 PendingIntent pendingIntent = 245 PendingIntent.getActivity(mContext, 0, prefsIntent, PendingIntent.FLAG_IMMUTABLE); 246 247 Bundle extras = new Bundle(); 248 extras.putString(Utils.ERROR_RESOLUTION_ACTION_LABEL, 249 mContext.getString(R.string.permission_error_resolve)); 250 extras.putParcelable(Utils.ERROR_RESOLUTION_ACTION_INTENT, pendingIntent); 251 252 PlaybackState state = new PlaybackState.Builder() 253 .setState(PlaybackState.STATE_ERROR, 0, 0) 254 .setErrorMessage(mContext.getString(R.string.permission_error)) 255 .setExtras(extras) 256 .build(); 257 mSession.setPlaybackState(state); 258 } 259 maybeRebuildQueue(Playlist playlist)260 private boolean maybeRebuildQueue(Playlist playlist) { 261 List<QueueItem> queue = new ArrayList<>(); 262 int foundIdx = 0; 263 // You need to check if the playlist actually is still valid because the user could have 264 // deleted files or taken out the sd card between runs so we might as well check this ahead 265 // of time before we load up the playlist. 266 for (Song song : playlist.songs) { 267 File tmp = new File(song.path); 268 if (!tmp.exists()) { 269 continue; 270 } 271 272 if (playlist.currentQueueId == song.queueId) { 273 foundIdx = queue.size(); 274 } 275 276 Bundle bundle = new Bundle(); 277 bundle.putString(DataModel.PATH_KEY, song.path); 278 MediaDescription description = new MediaDescription.Builder() 279 .setMediaId(song.mediaId) 280 .setTitle(song.title) 281 .setSubtitle(song.subtitle) 282 .setExtras(bundle) 283 .build(); 284 queue.add(new QueueItem(description, song.queueId)); 285 } 286 287 if (queue.isEmpty()) { 288 return false; 289 } 290 291 mQueue = queue; 292 mCurrentQueueIdx = foundIdx; // Resumes from beginning if last playing song was not found. 293 294 return true; 295 } 296 maybeRestoreState()297 public boolean maybeRestoreState() { 298 if (!Utils.hasRequiredPermissions(mContext)) { 299 setMissingPermissionError(); 300 return false; 301 } 302 String serialized = mSharedPrefs.getString(CURRENT_PLAYLIST_KEY, null); 303 if (serialized == null) { 304 return false; 305 } 306 307 try { 308 Playlist playlist = Playlist.parseFrom(Base64.getDecoder().decode(serialized)); 309 if (!maybeRebuildQueue(playlist)) { 310 return false; 311 } 312 updateSessionQueueState(); 313 314 requestAudioFocus(() -> { 315 try { 316 playCurrentQueueIndex(); 317 mMediaPlayer.seekTo(playlist.currentSongPosition); 318 updatePlaybackStatePlaying(); 319 } catch (IOException e) { 320 Log.e(TAG, "Restored queue, but couldn't resume playback."); 321 } 322 }); 323 } catch (IllegalArgumentException | InvalidProtocolBufferNanoException e) { 324 // Couldn't restore the playlist. Not the end of the world. 325 return false; 326 } 327 328 return true; 329 } 330 updateSessionQueueState()331 private void updateSessionQueueState() { 332 mSession.setQueueTitle(mContext.getString(R.string.playlist)); 333 mSession.setQueue(mQueue); 334 } 335 startPlayback(String key)336 private void startPlayback(String key) { 337 if (Log.isLoggable(TAG, Log.DEBUG)) { 338 Log.d(TAG, "startPlayback()"); 339 } 340 341 List<QueueItem> queue = mDataModel.getQueue(); 342 int idx = 0; 343 int foundIdx = -1; 344 for (QueueItem item : queue) { 345 if (item.getDescription().getMediaId().equals(key)) { 346 foundIdx = idx; 347 break; 348 } 349 idx++; 350 } 351 352 if (foundIdx == -1) { 353 mSession.setPlaybackState(mErrorState); 354 return; 355 } 356 357 mQueue = new ArrayList<>(queue); 358 mCurrentQueueIdx = foundIdx; 359 QueueItem current = mQueue.get(mCurrentQueueIdx); 360 String path = current.getDescription().getExtras().getString(DataModel.PATH_KEY); 361 MediaMetadata metadata = mDataModel.getMetadata(current.getDescription().getMediaId()); 362 updateSessionQueueState(); 363 364 try { 365 play(path, metadata); 366 } catch (IOException e) { 367 Log.e(TAG, "Playback failed.", e); 368 mSession.setPlaybackState(mErrorState); 369 } 370 } 371 resumePlayback()372 private void resumePlayback() { 373 if (Log.isLoggable(TAG, Log.DEBUG)) { 374 Log.d(TAG, "resumePlayback()"); 375 } 376 377 updatePlaybackStatePlaying(); 378 379 if (!mMediaPlayer.isPlaying()) { 380 mMediaPlayer.start(); 381 } 382 } 383 postMediaNotification(Notification.Builder builder)384 private void postMediaNotification(Notification.Builder builder) { 385 if (mQueue == null) { 386 return; 387 } 388 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 389 builder.setChannelId(CHANNEL_ID); 390 } 391 MediaDescription current = mQueue.get(mCurrentQueueIdx).getDescription(); 392 Notification notification = builder 393 .setStyle(new Notification.MediaStyle().setMediaSession(mSession.getSessionToken())) 394 .setContentTitle(current.getTitle()) 395 .setContentText(current.getSubtitle()) 396 .setShowWhen(false) 397 .build(); 398 notification.flags |= Notification.FLAG_NO_CLEAR; 399 mNotificationManager.notify(NOTIFICATION_ID, notification); 400 } 401 updatePlaybackStatePlaying()402 private void updatePlaybackStatePlaying() { 403 if (!mSession.isActive()) { 404 mSession.setActive(true); 405 } 406 407 // Update the state in the media session. 408 PlaybackState state = new PlaybackState.Builder() 409 .setState(PlaybackState.STATE_PLAYING, 410 mMediaPlayer.getCurrentPosition(), PLAYBACK_SPEED) 411 .setActions(PLAYING_ACTIONS) 412 .addCustomAction(mShuffle) 413 .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId()) 414 .build(); 415 mSession.setPlaybackState(state); 416 417 // Update the media styled notification. 418 postMediaNotification(mPlayingNotificationBuilder); 419 } 420 pausePlayback()421 private void pausePlayback() { 422 if (Log.isLoggable(TAG, Log.DEBUG)) { 423 Log.d(TAG, "pausePlayback()"); 424 } 425 426 long currentPosition = 0; 427 if (mMediaPlayer.isPlaying()) { 428 currentPosition = mMediaPlayer.getCurrentPosition(); 429 mMediaPlayer.pause(); 430 } 431 432 PlaybackState state = new PlaybackState.Builder() 433 .setState(PlaybackState.STATE_PAUSED, currentPosition, PLAYBACK_SPEED_STOPPED) 434 .setActions(PAUSED_ACTIONS) 435 .addCustomAction(mShuffle) 436 .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId()) 437 .build(); 438 mSession.setPlaybackState(state); 439 440 // Update the media styled notification. 441 postMediaNotification(mPausedNotificationBuilder); 442 } 443 stopPlayback()444 private void stopPlayback() { 445 if (Log.isLoggable(TAG, Log.DEBUG)) { 446 Log.d(TAG, "stopPlayback()"); 447 } 448 449 if (mMediaPlayer.isPlaying()) { 450 mMediaPlayer.stop(); 451 } 452 453 PlaybackState state = new PlaybackState.Builder() 454 .setState(PlaybackState.STATE_STOPPED, PLAYBACK_POSITION_STOPPED, 455 PLAYBACK_SPEED_STOPPED) 456 .setActions(STOPPED_ACTIONS) 457 .build(); 458 mSession.setPlaybackState(state); 459 } 460 advance()461 private void advance() throws IOException { 462 if (Log.isLoggable(TAG, Log.DEBUG)) { 463 Log.d(TAG, "advance()"); 464 } 465 // Go to the next song if one exists. Note that if you were to support gapless 466 // playback, you would have to change this code such that you had a currently 467 // playing and a loading MediaPlayer and juggled between them while also calling 468 // setNextMediaPlayer. 469 470 if (mQueue != null && !mQueue.isEmpty()) { 471 // Keep looping around when we run off the end of our current queue. 472 mCurrentQueueIdx = (mCurrentQueueIdx + 1) % mQueue.size(); 473 playCurrentQueueIndex(); 474 } else { 475 stopPlayback(); 476 } 477 } 478 retreat()479 private void retreat() throws IOException { 480 if (Log.isLoggable(TAG, Log.DEBUG)) { 481 Log.d(TAG, "retreat()"); 482 } 483 // Go to the next song if one exists. Note that if you were to support gapless 484 // playback, you would have to change this code such that you had a currently 485 // playing and a loading MediaPlayer and juggled between them while also calling 486 // setNextMediaPlayer. 487 if (mQueue != null) { 488 // Keep looping around when we run off the end of our current queue. 489 mCurrentQueueIdx--; 490 if (mCurrentQueueIdx < 0) { 491 mCurrentQueueIdx = mQueue.size() - 1; 492 } 493 playCurrentQueueIndex(); 494 } else { 495 stopPlayback(); 496 } 497 } 498 playCurrentQueueIndex()499 private void playCurrentQueueIndex() throws IOException { 500 MediaDescription next = mQueue.get(mCurrentQueueIdx).getDescription(); 501 String path = next.getExtras().getString(DataModel.PATH_KEY); 502 MediaMetadata metadata = mDataModel.getMetadata(next.getMediaId()); 503 504 play(path, metadata); 505 } 506 play(String path, MediaMetadata metadata)507 private void play(String path, MediaMetadata metadata) throws IOException { 508 if (Log.isLoggable(TAG, Log.DEBUG)) { 509 Log.d(TAG, "play path=" + path + " metadata=" + metadata); 510 } 511 512 mMediaPlayer.reset(); 513 mMediaPlayer.setDataSource(path); 514 mMediaPlayer.prepare(); 515 516 if (metadata != null) { 517 mSession.setMetadata(metadata); 518 } 519 boolean wasGrantedAudio = requestAudioFocus(() -> { 520 mMediaPlayer.start(); 521 updatePlaybackStatePlaying(); 522 }); 523 if (!wasGrantedAudio) { 524 // player.pause() isn't needed since it should not actually be playing, the 525 // other steps like, updating the notification and play state are needed, thus we 526 // call the pause method. 527 pausePlayback(); 528 } 529 } 530 safeAdvance()531 private void safeAdvance() { 532 try { 533 advance(); 534 } catch (IOException e) { 535 Log.e(TAG, "Failed to advance.", e); 536 mSession.setPlaybackState(mErrorState); 537 } 538 } 539 safeRetreat()540 private void safeRetreat() { 541 try { 542 retreat(); 543 } catch (IOException e) { 544 Log.e(TAG, "Failed to advance.", e); 545 mSession.setPlaybackState(mErrorState); 546 } 547 } 548 549 /** 550 * This is a naive implementation of shuffle, previously played songs may repeat after the 551 * shuffle operation. Only call this from the main thread. 552 */ shuffle()553 private void shuffle() { 554 if (Log.isLoggable(TAG, Log.DEBUG)) { 555 Log.d(TAG, "Shuffling"); 556 } 557 558 // rebuild the the queue in a shuffled form. 559 if (mQueue != null && mQueue.size() > 2) { 560 QueueItem current = mQueue.remove(mCurrentQueueIdx); 561 Collections.shuffle(mQueue); 562 mQueue.add(0, current); 563 // A QueueItem contains a queue id that's used as the key for when the user selects 564 // the current play list. This means the QueueItems must be rebuilt to have their new 565 // id's set. 566 for (int i = 0; i < mQueue.size(); i++) { 567 mQueue.set(i, new QueueItem(mQueue.get(i).getDescription(), i)); 568 } 569 mCurrentQueueIdx = 0; 570 updateSessionQueueState(); 571 } 572 } 573 574 @Override onPlayFromMediaId(String mediaId, Bundle extras)575 public void onPlayFromMediaId(String mediaId, Bundle extras) { 576 super.onPlayFromMediaId(mediaId, extras); 577 if (Log.isLoggable(TAG, Log.DEBUG)) { 578 Log.d(TAG, "onPlayFromMediaId mediaId" + mediaId + " extras=" + extras); 579 } 580 581 requestAudioFocus(() -> startPlayback(mediaId)); 582 } 583 584 @Override onSkipToNext()585 public void onSkipToNext() { 586 if (Log.isLoggable(TAG, Log.DEBUG)) { 587 Log.d(TAG, "onSkipToNext()"); 588 } 589 safeAdvance(); 590 } 591 592 @Override onSkipToPrevious()593 public void onSkipToPrevious() { 594 if (Log.isLoggable(TAG, Log.DEBUG)) { 595 Log.d(TAG, "onSkipToPrevious()"); 596 } 597 safeRetreat(); 598 } 599 600 @Override onSkipToQueueItem(long id)601 public void onSkipToQueueItem(long id) { 602 try { 603 mCurrentQueueIdx = (int) id; 604 playCurrentQueueIndex(); 605 } catch (IOException e) { 606 Log.e(TAG, "Failed to play.", e); 607 mSession.setPlaybackState(mErrorState); 608 } 609 } 610 611 @Override onCustomAction(String action, Bundle extras)612 public void onCustomAction(String action, Bundle extras) { 613 switch (action) { 614 case SHUFFLE: 615 shuffle(); 616 break; 617 default: 618 Log.e(TAG, "Unhandled custom action: " + action); 619 } 620 } 621 622 private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() { 623 @Override 624 public void onAudioFocusChange(int focus) { 625 switch (focus) { 626 case AudioManager.AUDIOFOCUS_GAIN: 627 resumePlayback(); 628 break; 629 case AudioManager.AUDIOFOCUS_LOSS: 630 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 631 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 632 pausePlayback(); 633 break; 634 default: 635 Log.e(TAG, "Unhandled audio focus type: " + focus); 636 } 637 } 638 }; 639 640 private OnCompletionListener mOnCompletionListener = new OnCompletionListener() { 641 @Override 642 public void onCompletion(MediaPlayer mediaPlayer) { 643 if (Log.isLoggable(TAG, Log.DEBUG)) { 644 Log.d(TAG, "onCompletion()"); 645 } 646 safeAdvance(); 647 } 648 }; 649 createNotificationChannel()650 private void createNotificationChannel() { 651 // Create the NotificationChannel, but only on API 26+ because 652 // the NotificationChannel class is new and not in the support library 653 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 654 CharSequence name = mContext.getString(R.string.notification_channel_name); 655 int importance = NotificationManager.IMPORTANCE_DEFAULT; 656 NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); 657 mNotificationManager.createNotificationChannel(channel); 658 } 659 } 660 } 661