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