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