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