1 /* 2 * Copyright (C) 2017 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 17 package com.example.android.leanback; 18 19 import android.app.Service; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.graphics.Bitmap; 23 import android.graphics.BitmapFactory; 24 import android.media.AudioManager; 25 import android.media.MediaPlayer; 26 import android.os.Binder; 27 import android.os.Handler; 28 import android.os.IBinder; 29 import android.os.Message; 30 import android.os.SystemClock; 31 import android.support.v4.media.MediaMetadataCompat; 32 import android.support.v4.media.session.MediaSessionCompat; 33 import android.support.v4.media.session.PlaybackStateCompat; 34 import android.util.Log; 35 36 import androidx.annotation.Nullable; 37 38 import java.io.IOException; 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.Random; 42 43 /** 44 * The service to play music. It also contains the media session. 45 */ 46 public class MediaSessionService extends Service { 47 48 49 public static final String CANNOT_SET_DATA_SOURCE = "Cannot set data source"; 50 private static final float NORMAL_SPEED = 1.0f; 51 52 /** 53 * When media player is prepared, our service can send notification to UI side through this 54 * callback. So UI will have chance to prepare/ pre-processing the UI status. 55 */ 56 interface MediaPlayerListener { onPrepared()57 void onPrepared(); 58 } 59 60 /** 61 * This LocalBinder class contains the getService() method which will return the service object. 62 */ 63 public class LocalBinder extends Binder { getService()64 MediaSessionService getService() { 65 return MediaSessionService.this; 66 } 67 } 68 69 /** 70 * Constant used in this class. 71 */ 72 private static final String MUSIC_PLAYER_SESSION_TOKEN = "MusicPlayer Session token"; 73 private static final int MEDIA_ACTION_NO_REPEAT = 0; 74 private static final int MEDIA_ACTION_REPEAT_ONE = 1; 75 private static final int MEDIA_ACTION_REPEAT_ALL = 2; 76 public static final String MEDIA_PLAYER_ERROR_MESSAGE = "Media player error message"; 77 public static final String PLAYER_NOT_INITIALIZED = "Media player not initialized"; 78 public static final String PLAYER_IS_PLAYING = "Media player is playing"; 79 public static final String PLAYER_SET_DATA_SOURCE_ERROR = 80 "Media player set new data source error"; 81 private static final boolean DEBUG = false; 82 private static final String TAG = "MusicPlaybackService"; 83 private static final int FOCUS_CHANGE = 0; 84 85 // This handler can control media player through audio's status. 86 private class MediaPlayerAudioHandler extends Handler { 87 @Override handleMessage(Message msg)88 public void handleMessage(Message msg) { 89 switch (msg.what) { 90 case FOCUS_CHANGE: 91 switch (msg.arg1) { 92 // pause media item when audio focus is lost 93 case AudioManager.AUDIOFOCUS_LOSS: 94 if (isPlaying()) { 95 audioFocusLossHandler(); 96 } 97 break; 98 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 99 if (isPlaying()) { 100 audioLossFocusTransientHandler(); 101 } 102 break; 103 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 104 if (isPlaying()) { 105 audioLossFocusTransientCanDuckHanlder(); 106 } 107 break; 108 case AudioManager.AUDIOFOCUS_GAIN: 109 if (!isPlaying()) { 110 audioFocusGainHandler(); 111 } 112 break; 113 } 114 } 115 } 116 } 117 118 // The callbacks' collection which can be notified by this service. 119 private List<MediaPlayerListener> mCallbacks = new ArrayList<>(); 120 121 // audio manager obtained from system to gain audio focus 122 private AudioManager mAudioManager; 123 124 // record user defined repeat mode. 125 private int mRepeatState = MEDIA_ACTION_NO_REPEAT; 126 127 // record user defined shuffle mode. 128 private int mShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE; 129 130 private MediaPlayer mPlayer; 131 private MediaSessionCompat mMediaSession; 132 133 // set -1 as invalid media item for playing. 134 private int mCurrentIndex = -1; 135 // media item in media playlist. 136 private MusicItem mCurrentMediaItem; 137 // media player's current progress. 138 private int mCurrentPosition; 139 // Buffered Position which will be updated inside of OnBufferingUpdateListener 140 private long mBufferedProgress; 141 List<MusicItem> mMediaItemList = new ArrayList<>(); 142 private boolean mInitialized; 143 144 // fast forward/ rewind speed factors and indexes 145 private float[] mFastForwardSpeedFactors; 146 private float[] mRewindSpeedFactors; 147 private int mFastForwardSpeedFactorIndex = 0; 148 private int mRewindSpeedFactorIndex = 0; 149 150 // Flags to indicate if current state is fast forwarding/ rewinding. 151 private boolean mIsFastForwarding; 152 private boolean mIsRewinding; 153 154 // handle audio related event. 155 private Handler mMediaPlayerHandler = new MediaPlayerAudioHandler(); 156 157 // The volume we set the media player to when we lose audio focus, but are 158 // allowed to reduce the volume and continue playing. 159 private static final float REDUCED_VOLUME = 0.1f; 160 // The volume we set the media player when we have audio focus. 161 private static final float FULL_VOLUME = 1.0f; 162 163 // Record position when current rewind action begins. 164 private long mRewindStartPosition; 165 // Record the time stamp when current rewind action is ended. 166 private long mRewindEndTime; 167 // Record the time stamp when current rewind action is started. 168 private long mRewindStartTime; 169 // Flag to represent the beginning of rewind operation. 170 private boolean mIsRewindBegin; 171 172 // A runnable object which will delay the execution of mPlayer.stop() 173 private Runnable mDelayedStopRunnable = new Runnable() { 174 @Override 175 public void run() { 176 mPlayer.stop(); 177 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 178 PlaybackStateCompat.STATE_STOPPED).build()); 179 } 180 }; 181 182 // Listener for audio focus. 183 private AudioManager.OnAudioFocusChangeListener mOnAudioFocusChangeListener = new 184 AudioManager.OnAudioFocusChangeListener() { 185 @Override 186 public void onAudioFocusChange(int focusChange) { 187 if (DEBUG) { 188 Log.d(TAG, "onAudioFocusChange. focusChange=" + focusChange); 189 } 190 mMediaPlayerHandler.obtainMessage(FOCUS_CHANGE, focusChange, 0).sendToTarget(); 191 } 192 }; 193 194 private final IBinder mBinder = new LocalBinder(); 195 196 /** 197 * The public API to gain media session instance from service. 198 * 199 * @return Media Session Instance. 200 */ getMediaSession()201 public MediaSessionCompat getMediaSession() { 202 return mMediaSession; 203 } 204 205 @Nullable 206 @Override onBind(Intent intent)207 public IBinder onBind(Intent intent) { 208 return mBinder; 209 } 210 211 @Override onCreate()212 public void onCreate() { 213 super.onCreate(); 214 215 // This service can be created for multiple times, the objects will only be created when 216 // it is null 217 if (mMediaSession == null) { 218 mMediaSession = new MediaSessionCompat(this, MUSIC_PLAYER_SESSION_TOKEN); 219 mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS 220 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); 221 mMediaSession.setCallback(new MediaSessionCallback()); 222 } 223 224 if (mAudioManager == null) { 225 // Create audio manager through system service 226 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 227 } 228 229 // initialize the player (including activate media session, request audio focus and 230 // set up the listener to listen to player's state) 231 initializePlayer(); 232 } 233 234 @Override onDestroy()235 public void onDestroy() { 236 super.onDestroy(); 237 stopForeground(true); 238 mAudioManager.abandonAudioFocus(mOnAudioFocusChangeListener); 239 mMediaPlayerHandler.removeCallbacksAndMessages(null); 240 if (mPlayer != null) { 241 // stop and release the media player since it's no longer in use 242 mPlayer.reset(); 243 mPlayer.release(); 244 mPlayer = null; 245 } 246 if (mMediaSession != null) { 247 mMediaSession.release(); 248 mMediaSession = null; 249 } 250 } 251 252 /** 253 * After binding to this service, other component can set Media Item List and prepare 254 * the first item in the list through this function. 255 * 256 * @param mediaItemList A list of media item to play. 257 * @param isQueue When this parameter is true, that meas new items should be appended to 258 * original media item list. 259 * If this parameter is false, the original playlist will be cleared and 260 * replaced with a new media item list. 261 */ setMediaList(List<MusicItem> mediaItemList, boolean isQueue)262 public void setMediaList(List<MusicItem> mediaItemList, boolean isQueue) { 263 if (!isQueue) { 264 mMediaItemList.clear(); 265 } 266 mMediaItemList.addAll(mediaItemList); 267 268 /** 269 * Points to the first media item in play list. 270 */ 271 mCurrentIndex = 0; 272 mCurrentMediaItem = mMediaItemList.get(0); 273 274 try { 275 mPlayer.setDataSource(this.getApplicationContext(), 276 mCurrentMediaItem.getMediaSourceUri(getApplicationContext())); 277 // Prepare the player asynchronously, use onPrepared listener as signal. 278 mPlayer.prepareAsync(); 279 } catch (IOException e) { 280 PlaybackStateCompat.Builder ret = createPlaybackStateBuilder( 281 PlaybackStateCompat.STATE_ERROR); 282 ret.setErrorMessage(PlaybackStateCompat.ERROR_CODE_APP_ERROR, 283 PLAYER_SET_DATA_SOURCE_ERROR); 284 } 285 } 286 287 /** 288 * Set Fast Forward Speeds for this media session service. 289 * 290 * @param fastForwardSpeeds The array contains all fast forward speeds. 291 */ setFastForwardSpeedFactors(int[] fastForwardSpeeds)292 public void setFastForwardSpeedFactors(int[] fastForwardSpeeds) { 293 mFastForwardSpeedFactors = new float[fastForwardSpeeds.length + 1]; 294 295 // Put normal speed factor at the beginning of the array 296 mFastForwardSpeedFactors[0] = 1.0f; 297 298 for (int index = 1; index < mFastForwardSpeedFactors.length; ++index) { 299 mFastForwardSpeedFactors[index] = fastForwardSpeeds[index - 1]; 300 } 301 } 302 303 /** 304 * Set Rewind Speeds for this media session service. 305 * 306 * @param rewindSpeeds The array contains all rewind speeds. 307 */ setRewindSpeedFactors(int[] rewindSpeeds)308 public void setRewindSpeedFactors(int[] rewindSpeeds) { 309 mRewindSpeedFactors = new float[rewindSpeeds.length]; 310 for (int index = 0; index < mRewindSpeedFactors.length; ++index) { 311 mRewindSpeedFactors[index] = -rewindSpeeds[index]; 312 } 313 } 314 315 /** 316 * Prepare the first item in the list. And setup the listener for media player. 317 */ initializePlayer()318 private void initializePlayer() { 319 // This service can be created for multiple times, the objects will only be created when 320 // it is null 321 if (mPlayer != null) { 322 return; 323 } 324 mPlayer = new MediaPlayer(); 325 326 // Set playback state to none to create a valid playback state. So controls row can get 327 // information about the supported actions. 328 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 329 PlaybackStateCompat.STATE_NONE).build()); 330 // Activate media session 331 if (!mMediaSession.isActive()) { 332 mMediaSession.setActive(true); 333 } 334 335 // Set up listener and audio stream type for underlying music player. 336 mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 337 338 // set up listener when the player is prepared. 339 mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 340 @Override 341 public void onPrepared(MediaPlayer mp) { 342 mInitialized = true; 343 // Every time when the player is prepared (when new data source is set), 344 // all listeners will be notified to toggle the UI to "pause" status. 345 notifyUiWhenPlayerIsPrepared(); 346 347 // When media player is prepared, the callback functions will be executed to update 348 // the meta data and playback state. 349 onMediaSessionMetaDataChanged(); 350 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 351 PlaybackStateCompat.STATE_PAUSED).build()); 352 } 353 }); 354 355 // set up listener for player's error. 356 mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { 357 @Override 358 public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { 359 if (DEBUG) { 360 PlaybackStateCompat.Builder builder = createPlaybackStateBuilder( 361 PlaybackStateCompat.STATE_ERROR); 362 builder.setErrorMessage(PlaybackStateCompat.ERROR_CODE_APP_ERROR, 363 MEDIA_PLAYER_ERROR_MESSAGE); 364 mMediaSession.setPlaybackState(builder.build()); 365 } 366 return true; 367 } 368 }); 369 370 // set up listener to respond the event when current music item is finished 371 mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 372 373 /** 374 * Expected Interaction Behavior: 375 * 1. If current media item's playing speed not equal to normal speed. 376 * 377 * A. MEDIA_ACTION_REPEAT_ALL 378 * a. If current media item is the last one. The first music item in the list will 379 * be prepared, but it won't play until user press play button. 380 * 381 * When user press the play button, the speed will be reset to normal (1.0f) 382 * no matter what the previous media item's playing speed is. 383 * 384 * b. If current media item isn't the last one, next media item will be prepared, 385 * but it won't play. 386 * 387 * When user press the play button, the speed will be reset to normal (1.0f) 388 * no matter what the previous media item's playing speed is. 389 * 390 * B. MEDIA_ACTION_REPEAT_ONE 391 * Different with previous scenario, current item will go back to the start point 392 * again and play automatically. (The reason to enable auto play here is for 393 * testing purpose and to make sure our designed API is flexible enough to support 394 * different situations.) 395 * 396 * No matter what the previous media item's playing speed is, in this situation 397 * current media item will be replayed in normal speed. 398 * 399 * C. MEDIA_ACTION_REPEAT_NONE 400 * a. If current media is the last one. The service will be closed, no music item 401 * will be prepared to play. From the UI perspective, the progress bar will not 402 * be reset to the starting point. 403 * 404 * b. If current media item isn't the last one, next media item will be prepared, 405 * but it won't play. 406 * 407 * When user press the play button, the speed will be reset to normal (1.0f) 408 * no matter what the previous media item's playing speed is. 409 * 410 * @param mp Object of MediaPlayer。 411 */ 412 @Override 413 public void onCompletion(MediaPlayer mp) { 414 415 // When current media item finishes playing, always reset rewind/ fastforward state 416 mFastForwardSpeedFactorIndex = 0; 417 mRewindSpeedFactorIndex = 0; 418 // Set player's playback speed back to normal 419 mPlayer.setPlaybackParams(mPlayer.getPlaybackParams().setSpeed( 420 mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex])); 421 // Pause the player, and update the status accordingly. 422 mPlayer.pause(); 423 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 424 PlaybackStateCompat.STATE_PAUSED).build()); 425 426 if (mRepeatState == MEDIA_ACTION_REPEAT_ALL 427 && mCurrentIndex == mMediaItemList.size() - 1) { 428 // if the repeat mode is enabled but the shuffle mode is not enabled, 429 // will go back to the first music item to play 430 if (mShuffleMode == PlaybackStateCompat.SHUFFLE_MODE_NONE) { 431 mCurrentIndex = 0; 432 } else { 433 // Or will choose a music item from playing list randomly. 434 mCurrentIndex = generateMediaItemIndex(); 435 } 436 mCurrentMediaItem = mMediaItemList.get(mCurrentIndex); 437 // The ui will also be changed from playing state to pause state through 438 // setDataSource() operation 439 setDataSource(); 440 } else if (mRepeatState == MEDIA_ACTION_REPEAT_ONE) { 441 // Play current music item again. 442 // The ui will stay to be "playing" status for the reason that there is no 443 // setDataSource() function call. 444 mPlayer.start(); 445 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 446 PlaybackStateCompat.STATE_PLAYING).build()); 447 } else if (mCurrentIndex < mMediaItemList.size() - 1) { 448 if (mShuffleMode == PlaybackStateCompat.SHUFFLE_MODE_NONE) { 449 mCurrentIndex++; 450 } else { 451 mCurrentIndex = generateMediaItemIndex(); 452 } 453 mCurrentMediaItem = mMediaItemList.get(mCurrentIndex); 454 // The ui will also be changed from playing state to pause state through 455 // setDataSource() operation 456 setDataSource(); 457 } else { 458 // close the service when the playlist is finished 459 // The PlaybackState will be updated to STATE_STOPPED. And onPlayComplete 460 // callback will be called by attached glue. 461 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 462 PlaybackStateCompat.STATE_STOPPED).build()); 463 stopSelf(); 464 } 465 } 466 }); 467 468 final MediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener = 469 new MediaPlayer.OnBufferingUpdateListener() { 470 @Override 471 public void onBufferingUpdate(MediaPlayer mp, int percent) { 472 mBufferedProgress = getDuration() * percent / 100; 473 PlaybackStateCompat.Builder builder = createPlaybackStateBuilder( 474 PlaybackStateCompat.STATE_BUFFERING); 475 builder.setBufferedPosition(mBufferedProgress); 476 mMediaSession.setPlaybackState(builder.build()); 477 } 478 }; 479 mPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener); 480 } 481 482 483 /** 484 * Public API to register listener for this service. 485 * 486 * @param listener The listener which will keep tracking current service's status 487 */ registerCallback(MediaPlayerListener listener)488 public void registerCallback(MediaPlayerListener listener) { 489 mCallbacks.add(listener); 490 } 491 492 /** 493 * Instead of shuffling the who music list, we will generate a media item index randomly 494 * and return it as the index for next media item to play. 495 * 496 * @return The index of next media item to play. 497 */ generateMediaItemIndex()498 private int generateMediaItemIndex() { 499 return new Random().nextInt(mMediaItemList.size()); 500 } 501 502 /** 503 * When player is prepared, service will send notification to UI through calling the callback's 504 * method 505 */ notifyUiWhenPlayerIsPrepared()506 private void notifyUiWhenPlayerIsPrepared() { 507 for (MediaPlayerListener callback : mCallbacks) { 508 callback.onPrepared(); 509 } 510 } 511 512 /** 513 * Set up media session callback to associate with player's operation. 514 */ 515 private class MediaSessionCallback extends MediaSessionCompat.Callback { 516 @Override onPlay()517 public void onPlay() { 518 play(); 519 } 520 521 @Override onPause()522 public void onPause() { 523 pause(); 524 } 525 526 @Override onSkipToNext()527 public void onSkipToNext() { 528 next(); 529 } 530 531 @Override onSkipToPrevious()532 public void onSkipToPrevious() { 533 previous(); 534 } 535 536 @Override onStop()537 public void onStop() { 538 stop(); 539 } 540 541 @Override onSeekTo(long pos)542 public void onSeekTo(long pos) { 543 // media player's seekTo method can only take integer as the parameter 544 // so the data type need to be casted as int 545 seekTo((int) pos); 546 } 547 548 @Override onFastForward()549 public void onFastForward() { 550 fastForward(); 551 } 552 553 @Override onRewind()554 public void onRewind() { 555 rewind(); 556 } 557 558 @Override onSetRepeatMode(int repeatMode)559 public void onSetRepeatMode(int repeatMode) { 560 setRepeatState(repeatMode); 561 } 562 563 @Override onSetShuffleMode(int shuffleMode)564 public void onSetShuffleMode(int shuffleMode) { 565 setShuffleMode(shuffleMode); 566 } 567 } 568 569 /** 570 * Set new data source and prepare the music player asynchronously. 571 */ setDataSource()572 private void setDataSource() { 573 reset(); 574 try { 575 mPlayer.setDataSource(this.getApplicationContext(), 576 mCurrentMediaItem.getMediaSourceUri(getApplicationContext())); 577 mPlayer.prepareAsync(); 578 } catch (IOException e) { 579 PlaybackStateCompat.Builder builder = createPlaybackStateBuilder( 580 PlaybackStateCompat.STATE_ERROR); 581 builder.setErrorMessage(PlaybackStateCompat.ERROR_CODE_APP_ERROR, 582 CANNOT_SET_DATA_SOURCE); 583 mMediaSession.setPlaybackState(builder.build()); 584 } 585 } 586 587 /** 588 * This function will return a playback state builder based on playbackState and current 589 * media position. 590 * 591 * @param playState current playback state. 592 * @return Object of PlaybackStateBuilder. 593 */ createPlaybackStateBuilder(int playState)594 private PlaybackStateCompat.Builder createPlaybackStateBuilder(int playState) { 595 PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder(); 596 long currentPosition = getCurrentPosition(); 597 float playbackSpeed = NORMAL_SPEED; 598 if (mIsFastForwarding) { 599 playbackSpeed = mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex]; 600 // After setting the playback speed, reset mIsFastForwarding flag. 601 mIsFastForwarding = false; 602 } else if (mIsRewinding) { 603 playbackSpeed = mRewindSpeedFactors[mRewindSpeedFactorIndex]; 604 // After setting the playback speed, reset mIsRewinding flag. 605 mIsRewinding = false; 606 } 607 playbackStateBuilder.setState(playState, currentPosition, playbackSpeed 608 ).setActions( 609 getPlaybackStateActions() 610 ); 611 return playbackStateBuilder; 612 } 613 614 /** 615 * Return supported actions related to current playback state. 616 * Currently the return value from this function is a constant. 617 * For demonstration purpose, the customized fast forward action and customized rewind action 618 * are supported in our case. 619 * 620 * @return playback state actions. 621 */ getPlaybackStateActions()622 private long getPlaybackStateActions() { 623 long res = PlaybackStateCompat.ACTION_PLAY 624 | PlaybackStateCompat.ACTION_PAUSE 625 | PlaybackStateCompat.ACTION_PLAY_PAUSE 626 | PlaybackStateCompat.ACTION_SKIP_TO_NEXT 627 | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS 628 | PlaybackStateCompat.ACTION_FAST_FORWARD 629 | PlaybackStateCompat.ACTION_REWIND 630 | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE 631 | PlaybackStateCompat.ACTION_SET_REPEAT_MODE; 632 return res; 633 } 634 635 /** 636 * Callback function when media session's meta data is changed. 637 * When this function is returned, the callback function onMetaDataChanged will be 638 * executed to address the new playback state. 639 */ onMediaSessionMetaDataChanged()640 private void onMediaSessionMetaDataChanged() { 641 if (mCurrentMediaItem == null) { 642 throw new IllegalArgumentException( 643 "mCurrentMediaItem is null in onMediaSessionMetaDataChanged!"); 644 } 645 MediaMetadataCompat.Builder metaDataBuilder = new MediaMetadataCompat.Builder(); 646 647 if (mCurrentMediaItem.getMediaTitle() != null) { 648 metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, 649 mCurrentMediaItem.getMediaTitle()); 650 } 651 652 if (mCurrentMediaItem.getMediaDescription() != null) { 653 metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, 654 mCurrentMediaItem.getMediaDescription()); 655 } 656 657 if (mCurrentMediaItem.getMediaAlbumArtResId(getApplicationContext()) != 0) { 658 Bitmap albumArtBitmap = BitmapFactory.decodeResource(getResources(), 659 mCurrentMediaItem.getMediaAlbumArtResId(getApplicationContext())); 660 metaDataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArtBitmap); 661 } 662 663 // duration information will be fetched from player. 664 metaDataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, getDuration()); 665 666 mMediaSession.setMetadata(metaDataBuilder.build()); 667 } 668 669 // Reset player. will be executed when new data source is assigned. reset()670 private void reset() { 671 if (mPlayer != null) { 672 mPlayer.reset(); 673 mInitialized = false; 674 } 675 } 676 677 // Control the player to play the music item. play()678 private void play() { 679 // Only when player is not null (meaning the player has been created), the player is 680 // prepared (using the mInitialized as the flag to represent it, 681 // this boolean variable will only be assigned to true inside of the onPrepared callback) 682 // and the media item is not currently playing (!isPlaying()), then the player can be 683 // started. 684 685 // If the player has not been prepared, but this function is fired, it is an error state 686 // from the app side 687 if (!mInitialized) { 688 PlaybackStateCompat.Builder builder = createPlaybackStateBuilder( 689 PlaybackStateCompat.STATE_ERROR); 690 builder.setErrorMessage(PlaybackStateCompat.ERROR_CODE_APP_ERROR, 691 PLAYER_NOT_INITIALIZED); 692 mMediaSession.setPlaybackState(builder.build()); 693 694 // If the player has is playing, and this function is fired again, it is an error state 695 // from the app side 696 } else { 697 // Request audio focus only when needed 698 if (mAudioManager.requestAudioFocus(mOnAudioFocusChangeListener, 699 AudioManager.STREAM_MUSIC, 700 AudioManager.AUDIOFOCUS_GAIN) != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 701 return; 702 } 703 704 if (mPlayer.getPlaybackParams().getSpeed() != NORMAL_SPEED) { 705 // Reset to normal speed and play 706 resetSpeedAndPlay(); 707 } else { 708 // Continue play. 709 mPlayer.start(); 710 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 711 PlaybackStateCompat.STATE_PLAYING).build()); 712 } 713 } 714 715 } 716 717 // Control the player to pause current music item. pause()718 private void pause() { 719 if (mPlayer != null && mPlayer.isPlaying()) { 720 // abandon audio focus immediately when the music item is paused. 721 mAudioManager.abandonAudioFocus(mOnAudioFocusChangeListener); 722 723 mPlayer.pause(); 724 // Update playbackState. 725 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 726 PlaybackStateCompat.STATE_PAUSED).build()); 727 } 728 } 729 730 // Control the player to stop. stop()731 private void stop() { 732 if (mPlayer != null) { 733 mPlayer.stop(); 734 // Update playbackState. 735 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 736 PlaybackStateCompat.STATE_STOPPED).build()); 737 } 738 } 739 740 741 /** 742 * Control the player to play next music item. 743 * Expected Interaction Behavior: 744 * No matter current media item is playing or not, when use hit next button, next item will be 745 * prepared but won't play unless user hit play button 746 * 747 * Also no matter current media item is fast forwarding or rewinding. Next music item will 748 * be played in normal speed. 749 */ next()750 private void next() { 751 if (mMediaItemList.isEmpty()) { 752 return; 753 } 754 mCurrentIndex = (mCurrentIndex + 1) % mMediaItemList.size(); 755 mCurrentMediaItem = mMediaItemList.get(mCurrentIndex); 756 757 // Reset FastForward/ Rewind state to normal state 758 mFastForwardSpeedFactorIndex = 0; 759 mRewindSpeedFactorIndex = 0; 760 // Set player's playback speed back to normal 761 mPlayer.setPlaybackParams(mPlayer.getPlaybackParams().setSpeed( 762 mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex])); 763 // Pause the player and update the play state. 764 // The ui will also be changed from "playing" state to "pause" state. 765 mPlayer.pause(); 766 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 767 PlaybackStateCompat.STATE_PAUSED).build()); 768 // set new data source to play based on mCurrentIndex and prepare the player. 769 // The ui will also be changed from "playing" state to "pause" state through setDataSource() 770 // operation 771 setDataSource(); 772 } 773 774 /** 775 * Control the player to play next music item. 776 * Expected Interaction Behavior: 777 * No matter current media item is playing or not, when use hit previous button, previous item 778 * will be prepared but won't play unless user hit play button 779 * 780 * Also no matter current media item is fast forwarding or rewinding. Previous music item will 781 * be played in normal speed. 782 */ previous()783 private void previous() { 784 if (mMediaItemList.isEmpty()) { 785 return; 786 } 787 mCurrentIndex = (mCurrentIndex - 1 + mMediaItemList.size()) % mMediaItemList.size(); 788 mCurrentMediaItem = mMediaItemList.get(mCurrentIndex); 789 790 // Reset FastForward/ Rewind state to normal state 791 mFastForwardSpeedFactorIndex = 0; 792 mRewindSpeedFactorIndex = 0; 793 // Set player's playback speed back to normal 794 mPlayer.setPlaybackParams(mPlayer.getPlaybackParams().setSpeed( 795 mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex])); 796 // Pause the player and update the play state. 797 // The ui will also be changed from "playing" state to "pause" state. 798 mPlayer.pause(); 799 // Update playbackState. 800 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 801 PlaybackStateCompat.STATE_PAUSED).build()); 802 // set new data source to play based on mCurrentIndex and prepare the player. 803 // The ui will also be changed from "playing" state to "pause" state through setDataSource() 804 // operation 805 setDataSource(); 806 } 807 808 // Get is playing information from underlying player. isPlaying()809 private boolean isPlaying() { 810 return mPlayer != null && mPlayer.isPlaying(); 811 } 812 813 // Play media item in a fast forward speed. fastForward()814 private void fastForward() { 815 // To support fast forward action, the mRewindSpeedFactors must be provided through 816 // setFastForwardSpeedFactors() method; 817 if (mFastForwardSpeedFactors == null) { 818 if (DEBUG) { 819 Log.d(TAG, "FastForwardSpeedFactors are not set"); 820 } 821 return; 822 } 823 824 // Toggle the flag to indicate fast forward status. 825 mIsFastForwarding = true; 826 827 // The first element in mFastForwardSpeedFactors is used to represent the normal speed. 828 // Will always be incremented by 1 firstly before setting the speed. 829 mFastForwardSpeedFactorIndex += 1; 830 if (mFastForwardSpeedFactorIndex > mFastForwardSpeedFactors.length - 1) { 831 mFastForwardSpeedFactorIndex = mFastForwardSpeedFactors.length - 1; 832 } 833 834 // In our customized fast forward operation, the media player will not be paused, 835 // But the player's speed will be changed accordingly. 836 mPlayer.setPlaybackParams(mPlayer.getPlaybackParams().setSpeed( 837 mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex])); 838 // Update playback state, mIsFastForwarding will be reset to false inside of it. 839 mMediaSession.setPlaybackState( 840 createPlaybackStateBuilder(PlaybackStateCompat.STATE_FAST_FORWARDING).build()); 841 } 842 843 844 // Play media item in a rewind speed. 845 // Android media player doesn't support negative speed. So for customized rewind operation, 846 // the player will be paused internally, but the pause state will not be published. So from 847 // the UI perspective, the player is still in playing status. 848 // Every time when the rewind speed is changed, the position will be computed through previous 849 // rewind speed then media player will seek to that position for seamless playing. rewind()850 private void rewind() { 851 // To support rewind action, the mRewindSpeedFactors must be provided through 852 // setRewindSpeedFactors() method; 853 if (mRewindSpeedFactors == null) { 854 if (DEBUG) { 855 Log.d(TAG, "RewindSpeedFactors are not set"); 856 } 857 return; 858 } 859 860 // Perform rewind operation using different speed. 861 if (mIsRewindBegin) { 862 // record end time stamp for previous rewind operation. 863 mRewindEndTime = SystemClock.elapsedRealtime(); 864 long position = mRewindStartPosition 865 + (long) mRewindSpeedFactors[mRewindSpeedFactorIndex - 1] * ( 866 mRewindEndTime - mRewindStartTime); 867 if (DEBUG) { 868 Log.e(TAG, "Last Rewind Operation Position" + position); 869 } 870 mPlayer.seekTo((int) position); 871 872 // Set new start status 873 mRewindStartPosition = position; 874 mRewindStartTime = mRewindEndTime; 875 // It is still in rewind state, so mIsRewindBegin remains to be true. 876 } 877 878 // Perform rewind operation using the first speed set. 879 if (!mIsRewindBegin) { 880 mRewindStartPosition = getCurrentPosition(); 881 Log.e("REWIND_BEGIN", "REWIND BEGIN PLACE " + mRewindStartPosition); 882 mIsRewindBegin = true; 883 mRewindStartTime = SystemClock.elapsedRealtime(); 884 } 885 886 // Toggle the flag to indicate rewind status. 887 mIsRewinding = true; 888 889 // Pause the player but won't update the UI status. 890 mPlayer.pause(); 891 892 // Update playback state, mIsRewinding will be reset to false inside of it. 893 mMediaSession.setPlaybackState( 894 createPlaybackStateBuilder(PlaybackStateCompat.STATE_REWINDING).build()); 895 896 mRewindSpeedFactorIndex += 1; 897 if (mRewindSpeedFactorIndex > mRewindSpeedFactors.length - 1) { 898 mRewindSpeedFactorIndex = mRewindSpeedFactors.length - 1; 899 } 900 } 901 902 // Reset the playing speed to normal. 903 // From PlaybackBannerGlue's key dispatching mechanism. If the player is currently in rewinding 904 // or fast forwarding status, moving from the rewinding/ FastForwarindg button will trigger 905 // the fastForwarding/ rewinding ending event. 906 // When customized fast forwarding or rewinding actions are supported, this function will be 907 // called. 908 // If we are in rewind mode, this function will compute the new position through rewinding 909 // speed and compare the start/ end rewinding time stamp. resetSpeedAndPlay()910 private void resetSpeedAndPlay() { 911 912 if (mIsRewindBegin) { 913 mIsRewindBegin = false; 914 mRewindEndTime = SystemClock.elapsedRealtime(); 915 916 long position = mRewindStartPosition 917 + (long) mRewindSpeedFactors[mRewindSpeedFactorIndex ] * ( 918 mRewindEndTime - mRewindStartTime); 919 920 // Seek to the computed position for seamless playing. 921 mPlayer.seekTo((int) position); 922 } 923 924 // Reset the state to normal state. 925 mFastForwardSpeedFactorIndex = 0; 926 mRewindSpeedFactorIndex = 0; 927 mPlayer.setPlaybackParams(mPlayer.getPlaybackParams().setSpeed( 928 mFastForwardSpeedFactors[mFastForwardSpeedFactorIndex])); 929 930 // Update the playback status from rewinding/ fast forwardindg to STATE_PLAYING. 931 // Which indicates current media item is played in the normal speed. 932 mMediaSession.setPlaybackState( 933 createPlaybackStateBuilder(PlaybackStateCompat.STATE_PLAYING).build()); 934 } 935 936 // Get current playing progress from media player. getCurrentPosition()937 private int getCurrentPosition() { 938 if (mInitialized && mPlayer != null) { 939 // Always record current position for seekTo operation. 940 mCurrentPosition = mPlayer.getCurrentPosition(); 941 return mPlayer.getCurrentPosition(); 942 } 943 return 0; 944 } 945 946 // get music duration from underlying music player getDuration()947 private int getDuration() { 948 return (mInitialized && mPlayer != null) ? mPlayer.getDuration() : 0; 949 } 950 951 // seek to specific position through underlying music player. seekTo(int newPosition)952 private void seekTo(int newPosition) { 953 if (mPlayer != null) { 954 mPlayer.seekTo(newPosition); 955 } 956 } 957 958 // set shuffle mode through passed parameter. setShuffleMode(int shuffleMode)959 private void setShuffleMode(int shuffleMode) { 960 mShuffleMode = shuffleMode; 961 } 962 963 // set shuffle mode through passed parameter. setRepeatState(int repeatState)964 public void setRepeatState(int repeatState) { 965 mRepeatState = repeatState; 966 } 967 audioFocusLossHandler()968 private void audioFocusLossHandler() { 969 // Permanent loss of audio focus 970 // Pause playback immediately 971 mPlayer.pause(); 972 // Wait 30 seconds before stopping playback 973 mMediaPlayerHandler.postDelayed(mDelayedStopRunnable, 30); 974 // Update playback state. 975 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 976 PlaybackStateCompat.STATE_PAUSED).build()); 977 // Will record current player progress when losing the audio focus. 978 mCurrentPosition = getCurrentPosition(); 979 } 980 audioLossFocusTransientHandler()981 private void audioLossFocusTransientHandler() { 982 // In this case, we already have lost the audio focus, and we cannot duck. 983 // So the player will be paused immediately, but different with the previous state, there is 984 // no need to stop the player. 985 mPlayer.pause(); 986 // update playback state 987 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 988 PlaybackStateCompat.STATE_PAUSED).build()); 989 // Will record current player progress when lossing the audio focus. 990 mCurrentPosition = getCurrentPosition(); 991 } 992 audioLossFocusTransientCanDuckHanlder()993 private void audioLossFocusTransientCanDuckHanlder() { 994 // In this case, we have lots the audio focus, but since we can duck 995 // the music item can continue to play but the volume will be reduced 996 mPlayer.setVolume(REDUCED_VOLUME, REDUCED_VOLUME); 997 } 998 audioFocusGainHandler()999 private void audioFocusGainHandler() { 1000 // In this case the app has been granted audio focus again 1001 // Firstly, raise volume to normal 1002 mPlayer.setVolume(FULL_VOLUME, FULL_VOLUME); 1003 1004 // If the recorded position is the same as current position 1005 // Start the player directly 1006 if (mCurrentPosition == mPlayer.getCurrentPosition()) { 1007 mPlayer.start(); 1008 mMediaSession.setPlaybackState(createPlaybackStateBuilder( 1009 PlaybackStateCompat.STATE_PLAYING).build()); 1010 // If the recorded position is not equal to current position 1011 // The player will seek to the last recorded position firstly to continue playing the 1012 // last music item 1013 } else { 1014 mPlayer.seekTo(mCurrentPosition); 1015 PlaybackStateCompat.Builder builder = createPlaybackStateBuilder( 1016 PlaybackStateCompat.STATE_BUFFERING); 1017 builder.setBufferedPosition(mBufferedProgress); 1018 mMediaSession.setPlaybackState(builder.build()); 1019 } 1020 } 1021 } 1022