Home | History | Annotate | Download | only in leanback
      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 {
     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