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.v4.media.MediaMetadataCompat;
32import android.support.v4.media.session.MediaSessionCompat;
33import android.support.v4.media.session.PlaybackStateCompat;
34import android.util.Log;
35
36import androidx.annotation.Nullable;
37
38import java.io.IOException;
39import java.util.ArrayList;
40import java.util.List;
41import java.util.Random;
42
43/**
44 * The service to play music. It also contains the media session.
45 */
46public 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 {
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 {
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
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     */
201    public MediaSessionCompat getMediaSession() {
202        return mMediaSession;
203    }
204
205    @Nullable
206    @Override
207    public IBinder onBind(Intent intent) {
208        return mBinder;
209    }
210
211    @Override
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
517        public void onPlay() {
518            play();
519        }
520
521        @Override
522        public void onPause() {
523            pause();
524        }
525
526        @Override
527        public void onSkipToNext() {
528            next();
529        }
530
531        @Override
532        public void onSkipToPrevious() {
533            previous();
534        }
535
536        @Override
537        public void onStop() {
538            stop();
539        }
540
541        @Override
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
549        public void onFastForward() {
550            fastForward();
551        }
552
553        @Override
554        public void onRewind() {
555            rewind();
556        }
557
558        @Override
559        public void onSetRepeatMode(int repeatMode) {
560            setRepeatState(repeatMode);
561        }
562
563        @Override
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     */
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     */
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     */
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     */
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.
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.
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.
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.
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     */
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     */
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.
809    private boolean isPlaying() {
810        return mPlayer != null && mPlayer.isPlaying();
811    }
812
813    // Play media item in a fast forward speed.
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.
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.
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.
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
947    private int getDuration() {
948        return (mInitialized && mPlayer != null) ? mPlayer.getDuration() : 0;
949    }
950
951    // seek to specific position through underlying music player.
952    private void seekTo(int newPosition) {
953        if (mPlayer != null) {
954            mPlayer.seekTo(newPosition);
955        }
956    }
957
958    // set shuffle mode through passed parameter.
959    private void setShuffleMode(int shuffleMode) {
960        mShuffleMode = shuffleMode;
961    }
962
963    // set shuffle mode through passed parameter.
964    public void setRepeatState(int repeatState) {
965        mRepeatState = repeatState;
966    }
967
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
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
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
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