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 <automotiveApp> root element. For a media app, this must include 93 * an <uses name="media"/> element as a child. 94 * For example, in AndroidManifest.xml: 95 * <meta-data android:name="com.google.android.gms.car.application" 96 * android:resource="@xml/automotive_app_desc"/> 97 * And in res/values/automotive_app_desc.xml: 98 * <automotiveApp> 99 * <uses name="media"/> 100 * </automotiveApp> 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