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