Home | History | Annotate | Download | only in music
      1 /*
      2  * Copyright (C) 2007 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.android.music;
     18 
     19 import android.app.PendingIntent;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.graphics.Bitmap;
     23 import android.media.MediaDescription;
     24 import android.media.MediaMetadata;
     25 import android.media.browse.MediaBrowser.MediaItem;
     26 import android.media.session.MediaSession;
     27 import android.media.session.PlaybackState;
     28 import android.os.Bundle;
     29 import android.os.Handler;
     30 import android.os.Message;
     31 import android.os.SystemClock;
     32 import android.service.media.MediaBrowserService;
     33 import android.text.TextUtils;
     34 import android.util.Log;
     35 import com.android.music.utils.*;
     36 
     37 import java.lang.ref.WeakReference;
     38 import java.util.*;
     39 
     40 import static com.android.music.utils.MediaIDHelper.*;
     41 
     42 /**
     43  * Provides "background" audio playback capabilities, allowing the
     44  * user to switch between activities without stopping playback.
     45  */
     46 public class MediaPlaybackService extends MediaBrowserService implements Playback.Callback {
     47     private static final String TAG = LogHelper.makeLogTag(MediaPlaybackService.class);
     48 
     49     // Delay stopSelf by using a handler.
     50     private static final int STOP_DELAY = 30000;
     51 
     52     public static final String ACTION_CMD = "com.android.music.ACTION_CMD";
     53     public static final String CMD_NAME = "CMD_NAME";
     54     public static final String CMD_PAUSE = "CMD_PAUSE";
     55     public static final String CMD_REPEAT = "CMD_PAUSE";
     56     public static final String REPEAT_MODE = "REPEAT_MODE";
     57 
     58     public enum RepeatMode { REPEAT_NONE, REPEAT_ALL, REPEAT_CURRENT }
     59 
     60     // Music catalog manager
     61     private MusicProvider mMusicProvider;
     62     private MediaSession mSession;
     63     // "Now playing" queue:
     64     private List<MediaSession.QueueItem> mPlayingQueue = null;
     65     private int mCurrentIndexOnQueue = -1;
     66     private MediaNotificationManager mMediaNotificationManager;
     67     // Indicates whether the service was started.
     68     private boolean mServiceStarted;
     69     private DelayedStopHandler mDelayedStopHandler = new DelayedStopHandler(this);
     70     private Playback mPlayback;
     71     // Default mode is repeat none
     72     private RepeatMode mRepeatMode = RepeatMode.REPEAT_NONE;
     73     // Extra information for this session
     74     private Bundle mExtras;
     75 
     76     public MediaPlaybackService() {}
     77 
     78     @Override
     79     public void onCreate() {
     80         LogHelper.d(TAG, "onCreate()");
     81         super.onCreate();
     82         LogHelper.d(TAG, "Create MusicProvider");
     83         mPlayingQueue = new ArrayList<>();
     84         mMusicProvider = new MusicProvider(this);
     85 
     86         LogHelper.d(TAG, "Create MediaSession");
     87         // Start a new MediaSession
     88         mSession = new MediaSession(this, "MediaPlaybackService");
     89         // Set extra information
     90         mExtras = new Bundle();
     91         mExtras.putInt(REPEAT_MODE, mRepeatMode.ordinal());
     92         mSession.setExtras(mExtras);
     93         // Enable callbacks from MediaButtons and TransportControls
     94         mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
     95                 | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
     96         // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
     97         PlaybackState.Builder stateBuilder = new PlaybackState.Builder().setActions(
     98                 PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_PAUSE);
     99         mSession.setPlaybackState(stateBuilder.build());
    100         // MediaSessionCallback() has methods that handle callbacks from a media controller
    101         mSession.setCallback(new MediaSessionCallback());
    102         // Set the session's token so that client activities can communicate with it.
    103         setSessionToken(mSession.getSessionToken());
    104 
    105         mPlayback = new Playback(this, mMusicProvider);
    106         mPlayback.setState(PlaybackState.STATE_NONE);
    107         mPlayback.setCallback(this);
    108         mPlayback.start();
    109 
    110         Context context = getApplicationContext();
    111         Intent intent = new Intent(context, MusicBrowserActivity.class);
    112         PendingIntent pi = PendingIntent.getActivity(
    113                 context, 99 /*request code*/, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    114         mSession.setSessionActivity(pi);
    115 
    116         updatePlaybackState(null);
    117 
    118         mMediaNotificationManager = new MediaNotificationManager(this);
    119     }
    120 
    121     @Override
    122     public int onStartCommand(Intent startIntent, int flags, int startId) {
    123         if (startIntent != null) {
    124             String action = startIntent.getAction();
    125             String command = startIntent.getStringExtra(CMD_NAME);
    126             if (ACTION_CMD.equals(action)) {
    127                 if (CMD_PAUSE.equals(command)) {
    128                     if (mPlayback != null && mPlayback.isPlaying()) {
    129                         handlePauseRequest();
    130                     }
    131                 }
    132             }
    133         }
    134         return START_STICKY;
    135     }
    136 
    137     @Override
    138     public void onDestroy() {
    139         Log.d(TAG, "onDestroy");
    140         // Service is being killed, so make sure we release our resources
    141         handleStopRequest(null);
    142 
    143         mDelayedStopHandler.removeCallbacksAndMessages(null);
    144         // Always release the MediaSession to clean up resources
    145         // and notify associated MediaController(s).
    146         mSession.release();
    147     }
    148 
    149     @Override
    150     public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
    151         Log.d(TAG,
    152                 "OnGetRoot: clientPackageName=" + clientPackageName + "; clientUid=" + clientUid
    153                         + " ; rootHints=" + rootHints);
    154         // Allow everyone to browse
    155         return new BrowserRoot(MEDIA_ID_ROOT, null);
    156     }
    157 
    158     @Override
    159     public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
    160         Log.d(TAG, "OnLoadChildren: parentMediaId=" + parentMediaId);
    161         //  Browsing not allowed
    162         if (parentMediaId == null) {
    163             result.sendResult(null);
    164             return;
    165         }
    166         if (!mMusicProvider.isInitialized()) {
    167             // Use result.detach to allow calling result.sendResult from another thread:
    168             result.detach();
    169 
    170             mMusicProvider.retrieveMediaAsync(new MusicProvider.MusicProviderCallback() {
    171                 @Override
    172                 public void onMusicCatalogReady(boolean success) {
    173                     Log.d(TAG, "Received catalog result, success:  " + String.valueOf(success));
    174                     if (success) {
    175                         onLoadChildren(parentMediaId, result);
    176                     } else {
    177                         result.sendResult(Collections.emptyList());
    178                     }
    179                 }
    180             });
    181 
    182         } else {
    183             // If our music catalog is already loaded/cached, load them into result immediately
    184             List<MediaItem> mediaItems = new ArrayList<>();
    185 
    186             switch (parentMediaId) {
    187                 case MEDIA_ID_ROOT:
    188                     Log.d(TAG, "OnLoadChildren.ROOT");
    189                     mediaItems.add(new MediaItem(new MediaDescription.Builder()
    190                                                          .setMediaId(MEDIA_ID_MUSICS_BY_ARTIST)
    191                                                          .setTitle("Artists")
    192                                                          .build(),
    193                             MediaItem.FLAG_BROWSABLE));
    194                     mediaItems.add(new MediaItem(new MediaDescription.Builder()
    195                                                          .setMediaId(MEDIA_ID_MUSICS_BY_ALBUM)
    196                                                          .setTitle("Albums")
    197                                                          .build(),
    198                             MediaItem.FLAG_BROWSABLE));
    199                     mediaItems.add(new MediaItem(new MediaDescription.Builder()
    200                                                          .setMediaId(MEDIA_ID_MUSICS_BY_SONG)
    201                                                          .setTitle("Songs")
    202                                                          .build(),
    203                             MediaItem.FLAG_BROWSABLE));
    204                     mediaItems.add(new MediaItem(new MediaDescription.Builder()
    205                                                          .setMediaId(MEDIA_ID_MUSICS_BY_PLAYLIST)
    206                                                          .setTitle("Playlists")
    207                                                          .build(),
    208                             MediaItem.FLAG_BROWSABLE));
    209                     break;
    210                 case MEDIA_ID_MUSICS_BY_ARTIST:
    211                     Log.d(TAG, "OnLoadChildren.ARTIST");
    212                     for (String artist : mMusicProvider.getArtists()) {
    213                         MediaItem item = new MediaItem(
    214                                 new MediaDescription.Builder()
    215                                         .setMediaId(MediaIDHelper.createBrowseCategoryMediaID(
    216                                                 MEDIA_ID_MUSICS_BY_ARTIST, artist))
    217                                         .setTitle(artist)
    218                                         .build(),
    219                                 MediaItem.FLAG_BROWSABLE);
    220                         mediaItems.add(item);
    221                     }
    222                     break;
    223                 case MEDIA_ID_MUSICS_BY_PLAYLIST:
    224                     LogHelper.d(TAG, "OnLoadChildren.PLAYLIST");
    225                     for (String playlist : mMusicProvider.getPlaylists()) {
    226                         MediaItem item = new MediaItem(
    227                                 new MediaDescription.Builder()
    228                                         .setMediaId(MediaIDHelper.createBrowseCategoryMediaID(
    229                                                 MEDIA_ID_MUSICS_BY_PLAYLIST, playlist))
    230                                         .setTitle(playlist)
    231                                         .build(),
    232                                 MediaItem.FLAG_BROWSABLE);
    233                         mediaItems.add(item);
    234                     }
    235                     break;
    236                 case MEDIA_ID_MUSICS_BY_ALBUM:
    237                     Log.d(TAG, "OnLoadChildren.ALBUM");
    238                     loadAlbum(mMusicProvider.getAlbums(), mediaItems);
    239                     break;
    240                 case MEDIA_ID_MUSICS_BY_SONG:
    241                     Log.d(TAG, "OnLoadChildren.SONG");
    242                     String hierarchyAwareMediaID = MediaIDHelper.createBrowseCategoryMediaID(
    243                             parentMediaId, MEDIA_ID_MUSICS_BY_SONG);
    244                     loadSong(mMusicProvider.getMusicList(), mediaItems, hierarchyAwareMediaID);
    245                     break;
    246                 default:
    247                     if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_ARTIST)) {
    248                         String artist = MediaIDHelper.getHierarchy(parentMediaId)[1];
    249                         Log.d(TAG, "OnLoadChildren.SONGS_BY_ARTIST  artist=" + artist);
    250                         loadAlbum(mMusicProvider.getAlbumByArtist(artist), mediaItems);
    251                     } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_ALBUM)) {
    252                         String album = MediaIDHelper.getHierarchy(parentMediaId)[1];
    253                         Log.d(TAG, "OnLoadChildren.SONGS_BY_ALBUM  album=" + album);
    254                         loadSong(mMusicProvider.getMusicsByAlbum(album), mediaItems, parentMediaId);
    255                     } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_PLAYLIST)) {
    256                         String playlist = MediaIDHelper.getHierarchy(parentMediaId)[1];
    257                         LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_PLAYLIST playlist=", playlist);
    258                         if (playlist.equals(MEDIA_ID_NOW_PLAYING) && mPlayingQueue != null
    259                                 && mPlayingQueue.size() > 0) {
    260                             loadPlayingQueue(mediaItems, parentMediaId);
    261                         } else {
    262                             loadSong(mMusicProvider.getMusicsByPlaylist(playlist), mediaItems,
    263                                     parentMediaId);
    264                         }
    265                     } else {
    266                         Log.w(TAG, "Skipping unmatched parentMediaId: " + parentMediaId);
    267                     }
    268                     break;
    269             }
    270             Log.d(TAG,
    271                     "OnLoadChildren sending " + mediaItems.size() + " results for "
    272                             + parentMediaId);
    273             result.sendResult(mediaItems);
    274         }
    275     }
    276 
    277     private void loadPlayingQueue(List<MediaItem> mediaItems, String parentId) {
    278         for (MediaSession.QueueItem queueItem : mPlayingQueue) {
    279             MediaItem mediaItem =
    280                     new MediaItem(queueItem.getDescription(), MediaItem.FLAG_PLAYABLE);
    281             mediaItems.add(mediaItem);
    282         }
    283     }
    284 
    285     private void loadSong(
    286             Iterable<MediaMetadata> songList, List<MediaItem> mediaItems, String parentId) {
    287         for (MediaMetadata metadata : songList) {
    288             String hierarchyAwareMediaID =
    289                     MediaIDHelper.createMediaID(metadata.getDescription().getMediaId(), parentId);
    290             Bundle songExtra = new Bundle();
    291             songExtra.putLong(MediaMetadata.METADATA_KEY_DURATION,
    292                     metadata.getLong(MediaMetadata.METADATA_KEY_DURATION));
    293             String title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE);
    294             String artistName = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
    295             MediaItem item = new MediaItem(new MediaDescription.Builder()
    296                                                    .setMediaId(hierarchyAwareMediaID)
    297                                                    .setTitle(title)
    298                                                    .setSubtitle(artistName)
    299                                                    .setExtras(songExtra)
    300                                                    .build(),
    301                     MediaItem.FLAG_PLAYABLE);
    302             mediaItems.add(item);
    303         }
    304     }
    305 
    306     private void loadAlbum(Iterable<MediaMetadata> albumList, List<MediaItem> mediaItems) {
    307         for (MediaMetadata albumMetadata : albumList) {
    308             String albumName = albumMetadata.getString(MediaMetadata.METADATA_KEY_ALBUM);
    309             String artistName = albumMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
    310             Bundle albumExtra = new Bundle();
    311             albumExtra.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS,
    312                     albumMetadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
    313             MediaItem item = new MediaItem(
    314                     new MediaDescription.Builder()
    315                             .setMediaId(MediaIDHelper.createBrowseCategoryMediaID(
    316                                     MEDIA_ID_MUSICS_BY_ALBUM, albumName))
    317                             .setTitle(albumName)
    318                             .setSubtitle(artistName)
    319                             .setIconBitmap(
    320                                     albumMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART))
    321                             .setExtras(albumExtra)
    322                             .build(),
    323                     MediaItem.FLAG_BROWSABLE);
    324             mediaItems.add(item);
    325         }
    326     }
    327 
    328     private final class MediaSessionCallback extends MediaSession.Callback {
    329         @Override
    330         public void onPlay() {
    331             Log.d(TAG, "play");
    332 
    333             if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
    334                 mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
    335                 mSession.setQueue(mPlayingQueue);
    336                 mSession.setQueueTitle(getString(R.string.random_queue_title));
    337                 // start playing from the beginning of the queue
    338                 mCurrentIndexOnQueue = 0;
    339             }
    340 
    341             if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
    342                 handlePlayRequest();
    343             }
    344         }
    345 
    346         @Override
    347         public void onSkipToQueueItem(long queueId) {
    348             LogHelper.d(TAG, "OnSkipToQueueItem:", queueId);
    349 
    350             if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
    351                 // set the current index on queue from the music Id:
    352                 mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId);
    353                 // play the music
    354                 handlePlayRequest();
    355             }
    356         }
    357 
    358         @Override
    359         public void onSeekTo(long position) {
    360             Log.d(TAG, "onSeekTo:" + position);
    361             mPlayback.seekTo((int) position);
    362         }
    363 
    364         @Override
    365         public void onPlayFromMediaId(String mediaId, Bundle extras) {
    366             LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, "  extras=", extras);
    367 
    368             // The mediaId used here is not the unique musicId. This one comes from the
    369             // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of
    370             // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary
    371             // so we can build the correct playing queue, based on where the track was
    372             // selected from.
    373             mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider);
    374             mSession.setQueue(mPlayingQueue);
    375             String queueTitle = getString(R.string.browse_musics_by_genre_subtitle,
    376                     MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId));
    377             mSession.setQueueTitle(queueTitle);
    378 
    379             if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
    380                 // set the current index on queue from the media Id:
    381                 mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, mediaId);
    382 
    383                 if (mCurrentIndexOnQueue < 0) {
    384                     LogHelper.e(TAG, "playFromMediaId: media ID ", mediaId,
    385                             " could not be found on queue. Ignoring.");
    386                 } else {
    387                     // play the music
    388                     handlePlayRequest();
    389                 }
    390             }
    391         }
    392 
    393         @Override
    394         public void onPause() {
    395             LogHelper.d(TAG, "pause. current state=" + mPlayback.getState());
    396             handlePauseRequest();
    397         }
    398 
    399         @Override
    400         public void onStop() {
    401             LogHelper.d(TAG, "stop. current state=" + mPlayback.getState());
    402             handleStopRequest(null);
    403         }
    404 
    405         @Override
    406         public void onSkipToNext() {
    407             LogHelper.d(TAG, "skipToNext");
    408             mCurrentIndexOnQueue++;
    409             if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) {
    410                 // This sample's behavior: skipping to next when in last song returns to the
    411                 // first song.
    412                 mCurrentIndexOnQueue = 0;
    413             }
    414             if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
    415                 handlePlayRequest();
    416             } else {
    417                 LogHelper.e(TAG,
    418                         "skipToNext: cannot skip to next. next Index=" + mCurrentIndexOnQueue
    419                                 + " queue length="
    420                                 + (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
    421                 handleStopRequest("Cannot skip");
    422             }
    423         }
    424 
    425         @Override
    426         public void onSkipToPrevious() {
    427             LogHelper.d(TAG, "skipToPrevious");
    428             mCurrentIndexOnQueue--;
    429             if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) {
    430                 // This sample's behavior: skipping to previous when in first song restarts the
    431                 // first song.
    432                 mCurrentIndexOnQueue = 0;
    433             }
    434             if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
    435                 handlePlayRequest();
    436             } else {
    437                 LogHelper.e(TAG,
    438                         "skipToPrevious: cannot skip to previous. previous Index="
    439                                 + mCurrentIndexOnQueue + " queue length="
    440                                 + (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
    441                 handleStopRequest("Cannot skip");
    442             }
    443         }
    444 
    445         @Override
    446         public void onPlayFromSearch(String query, Bundle extras) {
    447             LogHelper.d(TAG, "playFromSearch  query=", query);
    448 
    449             if (TextUtils.isEmpty(query)) {
    450                 // A generic search like "Play music" sends an empty query
    451                 // and it's expected that we start playing something. What will be played depends
    452                 // on the app: favorite playlist, "I'm feeling lucky", most recent, etc.
    453                 mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
    454             } else {
    455                 mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider);
    456             }
    457 
    458             LogHelper.d(TAG, "playFromSearch  playqueue.length=" + mPlayingQueue.size());
    459             mSession.setQueue(mPlayingQueue);
    460 
    461             if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
    462                 // immediately start playing from the beginning of the search results
    463                 mCurrentIndexOnQueue = 0;
    464 
    465                 handlePlayRequest();
    466             } else {
    467                 // if nothing was found, we need to warn the user and stop playing
    468                 handleStopRequest(getString(R.string.no_search_results));
    469             }
    470         }
    471 
    472         @Override
    473         public void onCustomAction(String action, Bundle extras) {
    474             LogHelper.d(TAG, "onCustomAction action=", action, ", extras=", extras);
    475             switch (action) {
    476                 case CMD_REPEAT:
    477                     mRepeatMode = RepeatMode.values()[extras.getInt(REPEAT_MODE)];
    478                     mExtras.putInt(REPEAT_MODE, mRepeatMode.ordinal());
    479                     mSession.setExtras(mExtras);
    480                     LogHelper.d(TAG, "modified repeatMode=", mRepeatMode);
    481                     break;
    482                 default:
    483                     LogHelper.d(TAG, "Unkown action=", action);
    484                     break;
    485             }
    486         }
    487     }
    488 
    489     /**
    490      * Handle a request to play music
    491      */
    492     private void handlePlayRequest() {
    493         LogHelper.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState());
    494 
    495         mDelayedStopHandler.removeCallbacksAndMessages(null);
    496         if (!mServiceStarted) {
    497             LogHelper.v(TAG, "Starting service");
    498             // The MusicService needs to keep running even after the calling MediaBrowser
    499             // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
    500             // need to play media.
    501             startService(new Intent(getApplicationContext(), MediaPlaybackService.class));
    502             mServiceStarted = true;
    503         }
    504 
    505         if (!mSession.isActive()) {
    506             mSession.setActive(true);
    507         }
    508 
    509         if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
    510             updateMetadata();
    511             mPlayback.play(mPlayingQueue.get(mCurrentIndexOnQueue));
    512         }
    513     }
    514 
    515     /**
    516      * Handle a request to pause music
    517      */
    518     private void handlePauseRequest() {
    519         LogHelper.d(TAG, "handlePauseRequest: mState=" + mPlayback.getState());
    520         mPlayback.pause();
    521         // reset the delayed stop handler.
    522         mDelayedStopHandler.removeCallbacksAndMessages(null);
    523         mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
    524     }
    525 
    526     /**
    527      * Handle a request to stop music
    528      */
    529     private void handleStopRequest(String withError) {
    530         LogHelper.d(
    531                 TAG, "handleStopRequest: mState=" + mPlayback.getState() + " error=", withError);
    532         mPlayback.stop(true);
    533         // reset the delayed stop handler.
    534         mDelayedStopHandler.removeCallbacksAndMessages(null);
    535         mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
    536 
    537         updatePlaybackState(withError);
    538 
    539         // service is no longer necessary. Will be started again if needed.
    540         stopSelf();
    541         mServiceStarted = false;
    542     }
    543 
    544     private void updateMetadata() {
    545         if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
    546             LogHelper.e(TAG, "Can't retrieve current metadata.");
    547             updatePlaybackState(getResources().getString(R.string.error_no_metadata));
    548             return;
    549         }
    550         MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
    551         String musicId =
    552                 MediaIDHelper.extractMusicIDFromMediaID(queueItem.getDescription().getMediaId());
    553         MediaMetadata track = mMusicProvider.getMusicByMediaId(musicId).getMetadata();
    554         final String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
    555         if (!musicId.equals(trackId)) {
    556             IllegalStateException e = new IllegalStateException("track ID should match musicId.");
    557             LogHelper.e(TAG, "track ID should match musicId.", " musicId=", musicId,
    558                     " trackId=", trackId,
    559                     " mediaId from queueItem=", queueItem.getDescription().getMediaId(),
    560                     " title from queueItem=", queueItem.getDescription().getTitle(),
    561                     " mediaId from track=", track.getDescription().getMediaId(),
    562                     " title from track=", track.getDescription().getTitle(),
    563                     " source.hashcode from track=",
    564                     track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE).hashCode(), e);
    565             throw e;
    566         }
    567         LogHelper.d(TAG, "Updating metadata for MusicID= " + musicId);
    568         mSession.setMetadata(track);
    569 
    570         // Set the proper album artwork on the media session, so it can be shown in the
    571         // locked screen and in other places.
    572         if (track.getDescription().getIconBitmap() == null
    573                 && track.getDescription().getIconUri() != null) {
    574             String albumUri = track.getDescription().getIconUri().toString();
    575             AlbumArtCache.getInstance().fetch(albumUri, new AlbumArtCache.FetchListener() {
    576                 @Override
    577                 public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) {
    578                     MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
    579                     MediaMetadata track = mMusicProvider.getMusicByMediaId(trackId).getMetadata();
    580                     track = new MediaMetadata
    581                                     .Builder(track)
    582 
    583                                     // set high resolution bitmap in METADATA_KEY_ALBUM_ART. This is
    584                                     // used, for
    585                                     // example, on the lockscreen background when the media session
    586                                     // is active.
    587                                     .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap)
    588 
    589                                     // set small version of the album art in the DISPLAY_ICON. This
    590                                     // is used on
    591                                     // the MediaDescription and thus it should be small to be
    592                                     // serialized if
    593                                     // necessary..
    594                                     .putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, icon)
    595 
    596                                     .build();
    597 
    598                     mMusicProvider.updateMusic(trackId, track);
    599 
    600                     // If we are still playing the same music
    601                     String currentPlayingId = MediaIDHelper.extractMusicIDFromMediaID(
    602                             queueItem.getDescription().getMediaId());
    603                     if (trackId.equals(currentPlayingId)) {
    604                         mSession.setMetadata(track);
    605                     }
    606                 }
    607             });
    608         }
    609     }
    610 
    611     /**
    612      * Update the current media player state, optionally showing an error message.
    613      *
    614      * @param error if not null, error message to present to the user.
    615      */
    616     private void updatePlaybackState(String error) {
    617         LogHelper.d(TAG, "updatePlaybackState, playback state=" + mPlayback.getState());
    618         long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
    619         if (mPlayback != null && mPlayback.isConnected()) {
    620             position = mPlayback.getCurrentStreamPosition();
    621         }
    622 
    623         PlaybackState.Builder stateBuilder =
    624                 new PlaybackState.Builder().setActions(getAvailableActions());
    625 
    626         int state = mPlayback.getState();
    627 
    628         // If there is an error message, send it to the playback state:
    629         if (error != null) {
    630             // Error states are really only supposed to be used for errors that cause playback to
    631             // stop unexpectedly and persist until the user takes action to fix it.
    632             stateBuilder.setErrorMessage(error);
    633             state = PlaybackState.STATE_ERROR;
    634         }
    635         stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime());
    636 
    637         // Set the activeQueueItemId if the current index is valid.
    638         if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
    639             MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
    640             stateBuilder.setActiveQueueItemId(item.getQueueId());
    641         }
    642 
    643         mSession.setPlaybackState(stateBuilder.build());
    644 
    645         if (state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_PAUSED) {
    646             mMediaNotificationManager.startNotification();
    647         }
    648     }
    649 
    650     private long getAvailableActions() {
    651         long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID
    652                 | PlaybackState.ACTION_PLAY_FROM_SEARCH;
    653         if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
    654             return actions;
    655         }
    656         if (mPlayback.isPlaying()) {
    657             actions |= PlaybackState.ACTION_PAUSE;
    658         }
    659         if (mCurrentIndexOnQueue > 0) {
    660             actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
    661         }
    662         if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) {
    663             actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
    664         }
    665         return actions;
    666     }
    667 
    668     private MediaMetadata getCurrentPlayingMusic() {
    669         if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
    670             MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
    671             if (item != null) {
    672                 LogHelper.d(TAG,
    673                         "getCurrentPlayingMusic for musicId=", item.getDescription().getMediaId());
    674                 return mMusicProvider
    675                         .getMusicByMediaId(MediaIDHelper.extractMusicIDFromMediaID(
    676                                 item.getDescription().getMediaId()))
    677                         .getMetadata();
    678             }
    679         }
    680         return null;
    681     }
    682 
    683     /**
    684      * Implementation of the Playback.Callback interface
    685      */
    686     @Override
    687     public void onCompletion() {
    688         // The media player finished playing the current song, so we go ahead
    689         // and start the next.
    690         if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
    691             switch (mRepeatMode) {
    692                 case REPEAT_ALL:
    693                     // Increase the index
    694                     mCurrentIndexOnQueue++;
    695                     // Restart queue when reaching the end
    696                     if (mCurrentIndexOnQueue >= mPlayingQueue.size()) {
    697                         mCurrentIndexOnQueue = 0;
    698                     }
    699                     break;
    700                 case REPEAT_CURRENT:
    701                     // Do not change the index
    702                     break;
    703                 case REPEAT_NONE:
    704                 default:
    705                     // Increase the index
    706                     mCurrentIndexOnQueue++;
    707                     // Stop the queue when reaching the end
    708                     if (mCurrentIndexOnQueue >= mPlayingQueue.size()) {
    709                         handleStopRequest(null);
    710                         return;
    711                     }
    712                     break;
    713             }
    714             handlePlayRequest();
    715         } else {
    716             // If there is nothing to play, we stop and release the resources:
    717             handleStopRequest(null);
    718         }
    719     }
    720 
    721     @Override
    722     public void onPlaybackStatusChanged(int state) {
    723         updatePlaybackState(null);
    724     }
    725 
    726     @Override
    727     public void onError(String error) {
    728         updatePlaybackState(error);
    729     }
    730 
    731     /**
    732      * A simple handler that stops the service if playback is not active (playing)
    733      */
    734     private static class DelayedStopHandler extends Handler {
    735         private final WeakReference<MediaPlaybackService> mWeakReference;
    736 
    737         private DelayedStopHandler(MediaPlaybackService service) {
    738             mWeakReference = new WeakReference<>(service);
    739         }
    740 
    741         @Override
    742         public void handleMessage(Message msg) {
    743             MediaPlaybackService service = mWeakReference.get();
    744             if (service != null && service.mPlayback != null) {
    745                 if (service.mPlayback.isPlaying()) {
    746                     Log.d(TAG, "Ignoring delayed stop since the media player is in use.");
    747                     return;
    748                 }
    749                 Log.d(TAG, "Stopping service with delay handler.");
    750                 service.stopSelf();
    751                 service.mServiceStarted = false;
    752             }
    753         }
    754     }
    755 }
    756