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