1  /*
2  * Copyright (C) 2014 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.mediabrowserservice;
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.net.Uri;
29  import android.os.Bundle;
30  import android.os.Handler;
31  import android.os.Message;
32  import android.os.SystemClock;
33  import android.service.media.MediaBrowserService;
34  import android.text.TextUtils;
35 
36  import com.example.android.mediabrowserservice.model.MusicProvider;
37  import com.example.android.mediabrowserservice.utils.CarHelper;
38  import com.example.android.mediabrowserservice.utils.LogHelper;
39  import com.example.android.mediabrowserservice.utils.MediaIDHelper;
40  import com.example.android.mediabrowserservice.utils.QueueHelper;
41 
42  import java.lang.ref.WeakReference;
43  import java.util.ArrayList;
44  import java.util.Collections;
45  import java.util.List;
46 
47  import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
48  import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_ROOT;
49  import static com.example.android.mediabrowserservice.utils.MediaIDHelper.createBrowseCategoryMediaID;
50 
51  /**
52   * This class provides a MediaBrowser through a service. It exposes the media library to a browsing
53   * client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and
54   * exposes it through its MediaSession.Token, which allows the client to create a MediaController
55   * that connects to and send control commands to the MediaSession remotely. This is useful for
56   * user interfaces that need to interact with your media session, like Android Auto. You can
57   * (should) also use the same service from your app's UI, which gives a seamless playback
58   * experience to the user.
59   *
60   * To implement a MediaBrowserService, you need to:
61   *
62   * <ul>
63   *
64   * <li> Extend {@link android.service.media.MediaBrowserService}, implementing the media browsing
65   *      related methods {@link android.service.media.MediaBrowserService#onGetRoot} and
66   *      {@link android.service.media.MediaBrowserService#onLoadChildren};
67   * <li> In onCreate, start a new {@link android.media.session.MediaSession} and notify its parent
68   *      with the session's token {@link android.service.media.MediaBrowserService#setSessionToken};
69   *
70   * <li> Set a callback on the
71   *      {@link android.media.session.MediaSession#setCallback(android.media.session.MediaSession.Callback)}.
72   *      The callback will receive all the user's actions, like play, pause, etc;
73   *
74   * <li> Handle all the actual music playing using any method your app prefers (for example,
75   *      {@link android.media.MediaPlayer})
76   *
77   * <li> Update playbackState, "now playing" metadata and queue, using MediaSession proper methods
78   *      {@link android.media.session.MediaSession#setPlaybackState(android.media.session.PlaybackState)}
79   *      {@link android.media.session.MediaSession#setMetadata(android.media.MediaMetadata)} and
80   *      {@link android.media.session.MediaSession#setQueue(java.util.List)})
81   *
82   * <li> Declare and export the service in AndroidManifest with an intent receiver for the action
83   *      android.media.browse.MediaBrowserService
84   *
85   * </ul>
86   *
87   * To make your app compatible with Android Auto, you also need to:
88   *
89   * <ul>
90   *
91   * <li> Declare a meta-data tag in AndroidManifest.xml linking to a xml resource
92   *      with a &lt;automotiveApp&gt; root element. For a media app, this must include
93   *      an &lt;uses name="media"/&gt; element as a child.
94   *      For example, in AndroidManifest.xml:
95   *          &lt;meta-data android:name="com.google.android.gms.car.application"
96   *              android:resource="@xml/automotive_app_desc"/&gt;
97   *      And in res/values/automotive_app_desc.xml:
98   *          &lt;automotiveApp&gt;
99   *              &lt;uses name="media"/&gt;
100   *          &lt;/automotiveApp&gt;
101   *
102   * </ul>
103 
104   * @see <a href="README.md">README.md</a> for more details.
105   *
106   */
107 
108  public class MusicService extends MediaBrowserService implements Playback.Callback {
109 
110      // The action of the incoming Intent indicating that it contains a command
111      // to be executed (see {@link #onStartCommand})
112      public static final String ACTION_CMD = "com.example.android.mediabrowserservice.ACTION_CMD";
113      // The key in the extras of the incoming Intent indicating the command that
114      // should be executed (see {@link #onStartCommand})
115      public static final String CMD_NAME = "CMD_NAME";
116      // A value of a CMD_NAME key in the extras of the incoming Intent that
117      // indicates that the music playback should be paused (see {@link #onStartCommand})
118      public static final String CMD_PAUSE = "CMD_PAUSE";
119 
120      private static final String TAG = LogHelper.makeLogTag(MusicService.class);
121      // Action to thumbs up a media item
122      private static final String CUSTOM_ACTION_THUMBS_UP =
123          "com.example.android.mediabrowserservice.THUMBS_UP";
124      // Delay stopSelf by using a handler.
125      private static final int STOP_DELAY = 30000;
126 
127      // Music catalog manager
128      private MusicProvider mMusicProvider;
129      private MediaSession mSession;
130      // "Now playing" queue:
131      private List<MediaSession.QueueItem> mPlayingQueue;
132      private int mCurrentIndexOnQueue;
133      private MediaNotificationManager mMediaNotificationManager;
134      // Indicates whether the service was started.
135      private boolean mServiceStarted;
136      private DelayedStopHandler mDelayedStopHandler = new DelayedStopHandler(this);
137      private Playback mPlayback;
138      private PackageValidator mPackageValidator;
139 
140      /*
141       * (non-Javadoc)
142       * @see android.app.Service#onCreate()
143       */
144      @Override
onCreate()145      public void onCreate() {
146          super.onCreate();
147          LogHelper.d(TAG, "onCreate");
148 
149          mPlayingQueue = new ArrayList<>();
150          mMusicProvider = new MusicProvider();
151          mPackageValidator = new PackageValidator(this);
152 
153          // Start a new MediaSession
154          mSession = new MediaSession(this, "MusicService");
155          setSessionToken(mSession.getSessionToken());
156          mSession.setCallback(new MediaSessionCallback());
157          mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
158              MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
159 
160          mPlayback = new Playback(this, mMusicProvider);
161          mPlayback.setState(PlaybackState.STATE_NONE);
162          mPlayback.setCallback(this);
163          mPlayback.start();
164 
165          Context context = getApplicationContext();
166          Intent intent = new Intent(context, MusicPlayerActivity.class);
167          PendingIntent pi = PendingIntent.getActivity(context, 99 /*request code*/,
168                  intent, PendingIntent.FLAG_UPDATE_CURRENT);
169          mSession.setSessionActivity(pi);
170 
171          Bundle extras = new Bundle();
172          CarHelper.setSlotReservationFlags(extras, true, true, true);
173          mSession.setExtras(extras);
174 
175          updatePlaybackState(null);
176 
177          mMediaNotificationManager = new MediaNotificationManager(this);
178      }
179 
180      /**
181       * (non-Javadoc)
182       * @see android.app.Service#onStartCommand(android.content.Intent, int, int)
183       */
184      @Override
onStartCommand(Intent startIntent, int flags, int startId)185      public int onStartCommand(Intent startIntent, int flags, int startId) {
186          if (startIntent != null) {
187              String action = startIntent.getAction();
188              String command = startIntent.getStringExtra(CMD_NAME);
189              if (ACTION_CMD.equals(action)) {
190                  if (CMD_PAUSE.equals(command)) {
191                      if (mPlayback != null && mPlayback.isPlaying()) {
192                          handlePauseRequest();
193                      }
194                  }
195              }
196          }
197          return START_STICKY;
198      }
199 
200      /**
201       * (non-Javadoc)
202       * @see android.app.Service#onDestroy()
203       */
204      @Override
onDestroy()205      public void onDestroy() {
206          LogHelper.d(TAG, "onDestroy");
207          // Service is being killed, so make sure we release our resources
208          handleStopRequest(null);
209 
210          mDelayedStopHandler.removeCallbacksAndMessages(null);
211          // Always release the MediaSession to clean up resources
212          // and notify associated MediaController(s).
213          mSession.release();
214      }
215 
216      @Override
onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)217      public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
218          LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName,
219                  "; clientUid=" + clientUid + " ; rootHints=", rootHints);
220          // To ensure you are not allowing any arbitrary app to browse your app's contents, you
221          // need to check the origin:
222          if (!mPackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) {
223              // If the request comes from an untrusted package, return null. No further calls will
224              // be made to other media browsing methods.
225              LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package "
226                      + clientPackageName);
227              return null;
228          }
229          //noinspection StatementWithEmptyBody
230          if (CarHelper.isValidCarPackage(clientPackageName)) {
231              // Optional: if your app needs to adapt ads, music library or anything else that
232              // needs to run differently when connected to the car, this is where you should handle
233              // it.
234          }
235          return new BrowserRoot(MEDIA_ID_ROOT, null);
236      }
237 
238      @Override
onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result)239      public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
240          if (!mMusicProvider.isInitialized()) {
241              // Use result.detach to allow calling result.sendResult from another thread:
242              result.detach();
243 
244              mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() {
245                  @Override
246                  public void onMusicCatalogReady(boolean success) {
247                      if (success) {
248                          loadChildrenImpl(parentMediaId, result);
249                      } else {
250                          updatePlaybackState(getString(R.string.error_no_metadata));
251                          result.sendResult(Collections.<MediaItem>emptyList());
252                      }
253                  }
254              });
255 
256          } else {
257              // If our music catalog is already loaded/cached, load them into result immediately
258              loadChildrenImpl(parentMediaId, result);
259          }
260      }
261 
262      /**
263       * Actual implementation of onLoadChildren that assumes that MusicProvider is already
264       * initialized.
265       */
loadChildrenImpl(final String parentMediaId, final Result<List<MediaItem>> result)266      private void loadChildrenImpl(final String parentMediaId,
267                                    final Result<List<MediaItem>> result) {
268          LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId);
269 
270          List<MediaItem> mediaItems = new ArrayList<>();
271 
272          if (MEDIA_ID_ROOT.equals(parentMediaId)) {
273              LogHelper.d(TAG, "OnLoadChildren.ROOT");
274              mediaItems.add(new MediaItem(
275                      new MediaDescription.Builder()
276                          .setMediaId(MEDIA_ID_MUSICS_BY_GENRE)
277                          .setTitle(getString(R.string.browse_genres))
278                          .setIconUri(Uri.parse("android.resource://" +
279                              "com.example.android.mediabrowserservice/drawable/ic_by_genre"))
280                          .setSubtitle(getString(R.string.browse_genre_subtitle))
281                          .build(), MediaItem.FLAG_BROWSABLE
282              ));
283 
284          } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) {
285              LogHelper.d(TAG, "OnLoadChildren.GENRES");
286              for (String genre : mMusicProvider.getGenres()) {
287                  MediaItem item = new MediaItem(
288                      new MediaDescription.Builder()
289                          .setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE, genre))
290                          .setTitle(genre)
291                          .setSubtitle(getString(R.string.browse_musics_by_genre_subtitle, genre))
292                          .build(), MediaItem.FLAG_BROWSABLE
293                  );
294                  mediaItems.add(item);
295              }
296 
297          } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) {
298              String genre = MediaIDHelper.getHierarchy(parentMediaId)[1];
299              LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_GENRE  genre=", genre);
300              for (MediaMetadata track : mMusicProvider.getMusicsByGenre(genre)) {
301                  // Since mediaMetadata fields are immutable, we need to create a copy, so we
302                  // can set a hierarchy-aware mediaID. We will need to know the media hierarchy
303                  // when we get a onPlayFromMusicID call, so we can create the proper queue based
304                  // on where the music was selected from (by artist, by genre, random, etc)
305                  String hierarchyAwareMediaID = MediaIDHelper.createMediaID(
306                          track.getDescription().getMediaId(), MEDIA_ID_MUSICS_BY_GENRE, genre);
307                  MediaMetadata trackCopy = new MediaMetadata.Builder(track)
308                          .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID)
309                          .build();
310                  MediaItem bItem = new MediaItem(
311                          trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE);
312                  mediaItems.add(bItem);
313              }
314          } else {
315              LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId);
316          }
317          LogHelper.d(TAG, "OnLoadChildren sending ", mediaItems.size(),
318                  " results for ", parentMediaId);
319          result.sendResult(mediaItems);
320      }
321 
322      private final class MediaSessionCallback extends MediaSession.Callback {
323          @Override
onPlay()324          public void onPlay() {
325              LogHelper.d(TAG, "play");
326 
327              if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
328                  mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
329                  mSession.setQueue(mPlayingQueue);
330                  mSession.setQueueTitle(getString(R.string.random_queue_title));
331                  // start playing from the beginning of the queue
332                  mCurrentIndexOnQueue = 0;
333              }
334 
335              if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
336                  handlePlayRequest();
337              }
338          }
339 
340          @Override
onSkipToQueueItem(long queueId)341          public void onSkipToQueueItem(long queueId) {
342              LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId);
343 
344              if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
345                  // set the current index on queue from the music Id:
346                  mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId);
347                  // play the music
348                  handlePlayRequest();
349              }
350          }
351 
352          @Override
onSeekTo(long position)353          public void onSeekTo(long position) {
354              LogHelper.d(TAG, "onSeekTo:", position);
355              mPlayback.seekTo((int) position);
356          }
357 
358          @Override
onPlayFromMediaId(String mediaId, Bundle extras)359          public void onPlayFromMediaId(String mediaId, Bundle extras) {
360              LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, "  extras=", extras);
361 
362              // The mediaId used here is not the unique musicId. This one comes from the
363              // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of
364              // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary
365              // so we can build the correct playing queue, based on where the track was
366              // selected from.
367              mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider);
368              mSession.setQueue(mPlayingQueue);
369              String queueTitle = getString(R.string.browse_musics_by_genre_subtitle,
370                      MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId));
371              mSession.setQueueTitle(queueTitle);
372 
373              if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
374                  // set the current index on queue from the media Id:
375                  mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, mediaId);
376 
377                  if (mCurrentIndexOnQueue < 0) {
378                      LogHelper.e(TAG, "playFromMediaId: media ID ", mediaId,
379                              " could not be found on queue. Ignoring.");
380                  } else {
381                      // play the music
382                      handlePlayRequest();
383                  }
384              }
385          }
386 
387          @Override
onPause()388          public void onPause() {
389              LogHelper.d(TAG, "pause. current state=" + mPlayback.getState());
390              handlePauseRequest();
391          }
392 
393          @Override
onStop()394          public void onStop() {
395              LogHelper.d(TAG, "stop. current state=" + mPlayback.getState());
396              handleStopRequest(null);
397          }
398 
399          @Override
onSkipToNext()400          public void onSkipToNext() {
401              LogHelper.d(TAG, "skipToNext");
402              mCurrentIndexOnQueue++;
403              if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) {
404                  // This sample's behavior: skipping to next when in last song returns to the
405                  // first song.
406                  mCurrentIndexOnQueue = 0;
407              }
408              if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
409                  handlePlayRequest();
410              } else {
411                  LogHelper.e(TAG, "skipToNext: cannot skip to next. next Index=" +
412                          mCurrentIndexOnQueue + " queue length=" +
413                          (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
414                  handleStopRequest("Cannot skip");
415              }
416          }
417 
418          @Override
onSkipToPrevious()419          public void onSkipToPrevious() {
420              LogHelper.d(TAG, "skipToPrevious");
421              mCurrentIndexOnQueue--;
422              if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) {
423                  // This sample's behavior: skipping to previous when in first song restarts the
424                  // first song.
425                  mCurrentIndexOnQueue = 0;
426              }
427              if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
428                  handlePlayRequest();
429              } else {
430                  LogHelper.e(TAG, "skipToPrevious: cannot skip to previous. previous Index=" +
431                          mCurrentIndexOnQueue + " queue length=" +
432                          (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
433                  handleStopRequest("Cannot skip");
434              }
435          }
436 
437          @Override
onCustomAction(String action, Bundle extras)438          public void onCustomAction(String action, Bundle extras) {
439              if (CUSTOM_ACTION_THUMBS_UP.equals(action)) {
440                  LogHelper.i(TAG, "onCustomAction: favorite for current track");
441                  MediaMetadata track = getCurrentPlayingMusic();
442                  if (track != null) {
443                      String musicId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
444                      mMusicProvider.setFavorite(musicId, !mMusicProvider.isFavorite(musicId));
445                  }
446                  // playback state needs to be updated because the "Favorite" icon on the
447                  // custom action will change to reflect the new favorite state.
448                  updatePlaybackState(null);
449              } else {
450                  LogHelper.e(TAG, "Unsupported action: ", action);
451              }
452          }
453 
454          @Override
onPlayFromSearch(String query, Bundle extras)455          public void onPlayFromSearch(String query, Bundle extras) {
456              LogHelper.d(TAG, "playFromSearch  query=", query);
457 
458              if (TextUtils.isEmpty(query)) {
459                  // A generic search like "Play music" sends an empty query
460                  // and it's expected that we start playing something. What will be played depends
461                  // on the app: favorite playlist, "I'm feeling lucky", most recent, etc.
462                  mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
463              } else {
464                  mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider);
465              }
466 
467              LogHelper.d(TAG, "playFromSearch  playqueue.length=" + mPlayingQueue.size());
468              mSession.setQueue(mPlayingQueue);
469 
470              if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
471                  // immediately start playing from the beginning of the search results
472                  mCurrentIndexOnQueue = 0;
473 
474                  handlePlayRequest();
475              } else {
476                  // if nothing was found, we need to warn the user and stop playing
477                  handleStopRequest(getString(R.string.no_search_results));
478              }
479          }
480      }
481 
482      /**
483       * Handle a request to play music
484       */
handlePlayRequest()485      private void handlePlayRequest() {
486          LogHelper.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState());
487 
488          mDelayedStopHandler.removeCallbacksAndMessages(null);
489          if (!mServiceStarted) {
490              LogHelper.v(TAG, "Starting service");
491              // The MusicService needs to keep running even after the calling MediaBrowser
492              // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
493              // need to play media.
494              startService(new Intent(getApplicationContext(), MusicService.class));
495              mServiceStarted = true;
496          }
497 
498          if (!mSession.isActive()) {
499              mSession.setActive(true);
500          }
501 
502          if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
503              updateMetadata();
504              mPlayback.play(mPlayingQueue.get(mCurrentIndexOnQueue));
505          }
506      }
507 
508      /**
509       * Handle a request to pause music
510       */
handlePauseRequest()511      private void handlePauseRequest() {
512          LogHelper.d(TAG, "handlePauseRequest: mState=" + mPlayback.getState());
513          mPlayback.pause();
514          // reset the delayed stop handler.
515          mDelayedStopHandler.removeCallbacksAndMessages(null);
516          mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
517      }
518 
519      /**
520       * Handle a request to stop music
521       */
handleStopRequest(String withError)522      private void handleStopRequest(String withError) {
523          LogHelper.d(TAG, "handleStopRequest: mState=" + mPlayback.getState() + " error=", withError);
524          mPlayback.stop(true);
525          // reset the delayed stop handler.
526          mDelayedStopHandler.removeCallbacksAndMessages(null);
527          mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
528 
529          updatePlaybackState(withError);
530 
531          // service is no longer necessary. Will be started again if needed.
532          stopSelf();
533          mServiceStarted = false;
534      }
535 
updateMetadata()536      private void updateMetadata() {
537          if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
538              LogHelper.e(TAG, "Can't retrieve current metadata.");
539              updatePlaybackState(getResources().getString(R.string.error_no_metadata));
540              return;
541          }
542          MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
543          String musicId = MediaIDHelper.extractMusicIDFromMediaID(
544                  queueItem.getDescription().getMediaId());
545          MediaMetadata track = mMusicProvider.getMusic(musicId);
546          final String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
547          if (!musicId.equals(trackId)) {
548              IllegalStateException e = new IllegalStateException("track ID should match musicId.");
549              LogHelper.e(TAG, "track ID should match musicId.",
550                  " musicId=", musicId, " trackId=", trackId,
551                  " mediaId from queueItem=", queueItem.getDescription().getMediaId(),
552                  " title from queueItem=", queueItem.getDescription().getTitle(),
553                  " mediaId from track=", track.getDescription().getMediaId(),
554                  " title from track=", track.getDescription().getTitle(),
555                  " source.hashcode from track=", track.getString(
556                      MusicProvider.CUSTOM_METADATA_TRACK_SOURCE).hashCode(),
557                  e);
558              throw e;
559          }
560          LogHelper.d(TAG, "Updating metadata for MusicID= " + musicId);
561          mSession.setMetadata(track);
562 
563          // Set the proper album artwork on the media session, so it can be shown in the
564          // locked screen and in other places.
565          if (track.getDescription().getIconBitmap() == null &&
566                  track.getDescription().getIconUri() != null) {
567              String albumUri = track.getDescription().getIconUri().toString();
568              AlbumArtCache.getInstance().fetch(albumUri, new AlbumArtCache.FetchListener() {
569                  @Override
570                  public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) {
571                      MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
572                      MediaMetadata track = mMusicProvider.getMusic(trackId);
573                      track = new MediaMetadata.Builder(track)
574 
575                          // set high resolution bitmap in METADATA_KEY_ALBUM_ART. This is used, for
576                          // example, on the lockscreen background when the media session is active.
577                          .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap)
578 
579                          // set small version of the album art in the DISPLAY_ICON. This is used on
580                          // the MediaDescription and thus it should be small to be serialized if
581                          // necessary..
582                          .putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, icon)
583 
584                          .build();
585 
586                      mMusicProvider.updateMusic(trackId, track);
587 
588                      // If we are still playing the same music
589                      String currentPlayingId = MediaIDHelper.extractMusicIDFromMediaID(
590                          queueItem.getDescription().getMediaId());
591                      if (trackId.equals(currentPlayingId)) {
592                          mSession.setMetadata(track);
593                      }
594                  }
595              });
596          }
597      }
598 
599      /**
600       * Update the current media player state, optionally showing an error message.
601       *
602       * @param error if not null, error message to present to the user.
603       */
updatePlaybackState(String error)604      private void updatePlaybackState(String error) {
605          LogHelper.d(TAG, "updatePlaybackState, playback state=" + mPlayback.getState());
606          long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
607          if (mPlayback != null && mPlayback.isConnected()) {
608              position = mPlayback.getCurrentStreamPosition();
609          }
610 
611          PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
612                  .setActions(getAvailableActions());
613 
614          setCustomAction(stateBuilder);
615          int state = mPlayback.getState();
616 
617          // If there is an error message, send it to the playback state:
618          if (error != null) {
619              // Error states are really only supposed to be used for errors that cause playback to
620              // stop unexpectedly and persist until the user takes action to fix it.
621              stateBuilder.setErrorMessage(error);
622              state = PlaybackState.STATE_ERROR;
623          }
624          stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime());
625 
626          // Set the activeQueueItemId if the current index is valid.
627          if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
628              MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
629              stateBuilder.setActiveQueueItemId(item.getQueueId());
630          }
631 
632          mSession.setPlaybackState(stateBuilder.build());
633 
634          if (state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_PAUSED) {
635              mMediaNotificationManager.startNotification();
636          }
637      }
638 
setCustomAction(PlaybackState.Builder stateBuilder)639      private void setCustomAction(PlaybackState.Builder stateBuilder) {
640          MediaMetadata currentMusic = getCurrentPlayingMusic();
641          if (currentMusic != null) {
642              // Set appropriate "Favorite" icon on Custom action:
643              String musicId = currentMusic.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
644              int favoriteIcon = R.drawable.ic_star_off;
645              if (mMusicProvider.isFavorite(musicId)) {
646                  favoriteIcon = R.drawable.ic_star_on;
647              }
648              LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ",
649                      musicId, " current favorite=", mMusicProvider.isFavorite(musicId));
650              stateBuilder.addCustomAction(CUSTOM_ACTION_THUMBS_UP, getString(R.string.favorite),
651                  favoriteIcon);
652          }
653      }
654 
getAvailableActions()655      private long getAvailableActions() {
656          long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
657                  PlaybackState.ACTION_PLAY_FROM_SEARCH;
658          if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
659              return actions;
660          }
661          if (mPlayback.isPlaying()) {
662              actions |= PlaybackState.ACTION_PAUSE;
663          }
664          if (mCurrentIndexOnQueue > 0) {
665              actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
666          }
667          if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) {
668              actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
669          }
670          return actions;
671      }
672 
getCurrentPlayingMusic()673      private MediaMetadata getCurrentPlayingMusic() {
674          if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
675              MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
676              if (item != null) {
677                  LogHelper.d(TAG, "getCurrentPlayingMusic for musicId=",
678                          item.getDescription().getMediaId());
679                  return mMusicProvider.getMusic(
680                          MediaIDHelper.extractMusicIDFromMediaID(item.getDescription().getMediaId()));
681              }
682          }
683          return null;
684      }
685 
686      /**
687       * Implementation of the Playback.Callback interface
688       */
689      @Override
onCompletion()690      public void onCompletion() {
691          // The media player finished playing the current song, so we go ahead
692          // and start the next.
693          if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
694              // In this sample, we restart the playing queue when it gets to the end:
695              mCurrentIndexOnQueue++;
696              if (mCurrentIndexOnQueue >= mPlayingQueue.size()) {
697                  mCurrentIndexOnQueue = 0;
698              }
699              handlePlayRequest();
700          } else {
701              // If there is nothing to play, we stop and release the resources:
702              handleStopRequest(null);
703          }
704      }
705 
706      @Override
onPlaybackStatusChanged(int state)707      public void onPlaybackStatusChanged(int state) {
708          updatePlaybackState(null);
709      }
710 
711      @Override
onError(String error)712      public void onError(String error) {
713          updatePlaybackState(error);
714      }
715 
716      /**
717       * A simple handler that stops the service if playback is not active (playing)
718       */
719      private static class DelayedStopHandler extends Handler {
720          private final WeakReference<MusicService> mWeakReference;
721 
DelayedStopHandler(MusicService service)722          private DelayedStopHandler(MusicService service) {
723              mWeakReference = new WeakReference<>(service);
724          }
725 
726          @Override
handleMessage(Message msg)727          public void handleMessage(Message msg) {
728              MusicService service = mWeakReference.get();
729              if (service != null && service.mPlayback != null) {
730                  if (service.mPlayback.isPlaying()) {
731                      LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use.");
732                      return;
733                  }
734                  LogHelper.d(TAG, "Stopping service with delay handler.");
735                  service.stopSelf();
736                  service.mServiceStarted = false;
737              }
738          }
739      }
740  }
741