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