• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
MediaPlaybackService()76     public MediaPlaybackService() {}
77 
78     @Override
onCreate()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
onStartCommand(Intent startIntent, int flags, int startId)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
onDestroy()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
onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)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
onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result)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 
loadPlayingQueue(List<MediaItem> mediaItems, String parentId)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 
loadSong( Iterable<MediaMetadata> songList, List<MediaItem> mediaItems, String parentId)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 
loadAlbum(Iterable<MediaMetadata> albumList, List<MediaItem> mediaItems)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
onPlay()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
onSkipToQueueItem(long queueId)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
onSeekTo(long position)359         public void onSeekTo(long position) {
360             Log.d(TAG, "onSeekTo:" + position);
361             mPlayback.seekTo((int) position);
362         }
363 
364         @Override
onPlayFromMediaId(String mediaId, Bundle extras)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
onPause()394         public void onPause() {
395             LogHelper.d(TAG, "pause. current state=" + mPlayback.getState());
396             handlePauseRequest();
397         }
398 
399         @Override
onStop()400         public void onStop() {
401             LogHelper.d(TAG, "stop. current state=" + mPlayback.getState());
402             handleStopRequest(null);
403         }
404 
405         @Override
onSkipToNext()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
onSkipToPrevious()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
onPlayFromSearch(String query, Bundle extras)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
onCustomAction(String action, Bundle extras)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      */
handlePlayRequest()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      */
handlePauseRequest()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      */
handleStopRequest(String withError)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 
updateMetadata()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      */
updatePlaybackState(String error)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 
getAvailableActions()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 
getCurrentPlayingMusic()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
onCompletion()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
onPlaybackStatusChanged(int state)722     public void onPlaybackStatusChanged(int state) {
723         updatePlaybackState(null);
724     }
725 
726     @Override
onError(String error)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 
DelayedStopHandler(MediaPlaybackService service)737         private DelayedStopHandler(MediaPlaybackService service) {
738             mWeakReference = new WeakReference<>(service);
739         }
740 
741         @Override
handleMessage(Message msg)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