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.content.Context; 20 import android.content.Intent; 21 import android.media.AudioManager; 22 import android.media.MediaDescription; 23 import android.media.MediaMetadata; 24 import android.media.MediaPlayer; 25 import android.media.MediaPlayer.OnCompletionListener; 26 import android.media.MediaPlayer.OnErrorListener; 27 import android.media.MediaPlayer.OnPreparedListener; 28 import android.media.browse.MediaBrowser; 29 import android.media.browse.MediaBrowser.MediaItem; 30 import android.media.session.MediaSession; 31 import android.media.session.PlaybackState; 32 import android.net.Uri; 33 import android.net.wifi.WifiManager; 34 import android.net.wifi.WifiManager.WifiLock; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.os.Message; 38 import android.os.PowerManager; 39 import android.os.SystemClock; 40 import android.service.media.MediaBrowserService; 41 42 import com.example.android.mediabrowserservice.model.MusicProvider; 43 import com.example.android.mediabrowserservice.utils.LogHelper; 44 import com.example.android.mediabrowserservice.utils.MediaIDHelper; 45 import com.example.android.mediabrowserservice.utils.QueueHelper; 46 47 import java.io.IOException; 48 import java.util.ArrayList; 49 import java.util.List; 50 51 import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE; 52 import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_ROOT; 53 import static com.example.android.mediabrowserservice.utils.MediaIDHelper.createBrowseCategoryMediaID; 54 import static com.example.android.mediabrowserservice.utils.MediaIDHelper.extractBrowseCategoryFromMediaID; 55 56 /** 57 * This class provides a MediaBrowser through a service. It exposes the media library to a browsing 58 * client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and 59 * exposes it through its MediaSession.Token, which allows the client to create a MediaController 60 * that connects to and send control commands to the MediaSession remotely. This is useful for 61 * user interfaces that need to interact with your media session, like Android Auto. You can 62 * (should) also use the same service from your app's UI, which gives a seamless playback 63 * experience to the user. 64 * 65 * To implement a MediaBrowserService, you need to: 66 * 67 * <ul> 68 * 69 * <li> Extend {@link android.service.media.MediaBrowserService}, implementing the media browsing 70 * related methods {@link android.service.media.MediaBrowserService#onGetRoot} and 71 * {@link android.service.media.MediaBrowserService#onLoadChildren}; 72 * <li> In onCreate, start a new {@link android.media.session.MediaSession} and notify its parent 73 * with the session's token {@link android.service.media.MediaBrowserService#setSessionToken}; 74 * 75 * <li> Set a callback on the 76 * {@link android.media.session.MediaSession#setCallback(android.media.session.MediaSession.Callback)}. 77 * The callback will receive all the user's actions, like play, pause, etc; 78 * 79 * <li> Handle all the actual music playing using any method your app prefers (for example, 80 * {@link android.media.MediaPlayer}) 81 * 82 * <li> Update playbackState, "now playing" metadata and queue, using MediaSession proper methods 83 * {@link android.media.session.MediaSession#setPlaybackState(android.media.session.PlaybackState)} 84 * {@link android.media.session.MediaSession#setMetadata(android.media.MediaMetadata)} and 85 * {@link android.media.session.MediaSession#setQueue(java.util.List)}) 86 * 87 * <li> Declare and export the service in AndroidManifest with an intent receiver for the action 88 * android.media.browse.MediaBrowserService 89 * 90 * </ul> 91 * 92 * To make your app compatible with Android Auto, you also need to: 93 * 94 * <ul> 95 * 96 * <li> Declare a meta-data tag in AndroidManifest.xml linking to a xml resource 97 * with a <automotiveApp> root element. For a media app, this must include 98 * an <uses name="media"/> element as a child. 99 * For example, in AndroidManifest.xml: 100 * <meta-data android:name="com.google.android.gms.car.application" 101 * android:resource="@xml/automotive_app_desc"/> 102 * And in res/values/automotive_app_desc.xml: 103 * <automotiveApp> 104 * <uses name="media"/> 105 * </automotiveApp> 106 * 107 * </ul> 108 109 * @see <a href="README.md">README.md</a> for more details. 110 * 111 */ 112 113 public class MusicService extends MediaBrowserService implements OnPreparedListener, 114 OnCompletionListener, OnErrorListener, AudioManager.OnAudioFocusChangeListener { 115 116 private static final String TAG = LogHelper.makeLogTag(MusicService.class.getSimpleName()); 117 118 // Action to thumbs up a media item 119 private static final String CUSTOM_ACTION_THUMBS_UP = "thumbs_up"; 120 // Delay stopSelf by using a handler. 121 private static final int STOP_DELAY = 30000; 122 123 // The volume we set the media player to when we lose audio focus, but are 124 // allowed to reduce the volume instead of stopping playback. 125 public static final float VOLUME_DUCK = 0.2f; 126 127 // The volume we set the media player when we have audio focus. 128 public static final float VOLUME_NORMAL = 1.0f; 129 public static final String ANDROID_AUTO_PACKAGE_NAME = "com.google.android.projection.gearhead"; 130 public static final String ANDROID_AUTO_SIMULATOR_PACKAGE_NAME = "com.google.android.mediasimulator"; 131 132 // Music catalog manager 133 private MusicProvider mMusicProvider; 134 135 private MediaSession mSession; 136 private MediaPlayer mMediaPlayer; 137 138 // "Now playing" queue: 139 private List<MediaSession.QueueItem> mPlayingQueue; 140 private int mCurrentIndexOnQueue; 141 142 // Current local media player state 143 private int mState = PlaybackState.STATE_NONE; 144 145 // Wifi lock that we hold when streaming files from the internet, in order 146 // to prevent the device from shutting off the Wifi radio 147 private WifiLock mWifiLock; 148 149 private MediaNotificationManager mMediaNotificationManager; 150 151 // Indicates whether the service was started. 152 private boolean mServiceStarted; 153 154 enum AudioFocus { 155 NoFocusNoDuck, // we don't have audio focus, and can't duck 156 NoFocusCanDuck, // we don't have focus, but can play at a low volume 157 // ("ducking") 158 Focused // we have full audio focus 159 } 160 161 // Type of audio focus we have: 162 private AudioFocus mAudioFocus = AudioFocus.NoFocusNoDuck; 163 private AudioManager mAudioManager; 164 165 // Indicates if we should start playing immediately after we gain focus. 166 private boolean mPlayOnFocusGain; 167 168 private Handler mDelayedStopHandler = new Handler() { 169 @Override 170 public void handleMessage(Message msg) { 171 if ((mMediaPlayer != null && mMediaPlayer.isPlaying()) || 172 mPlayOnFocusGain) { 173 LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use."); 174 return; 175 } 176 LogHelper.d(TAG, "Stopping service with delay handler."); 177 stopSelf(); 178 mServiceStarted = false; 179 } 180 }; 181 182 /* 183 * (non-Javadoc) 184 * @see android.app.Service#onCreate() 185 */ 186 @Override 187 public void onCreate() { 188 super.onCreate(); 189 LogHelper.d(TAG, "onCreate"); 190 191 mPlayingQueue = new ArrayList<>(); 192 193 // Create the Wifi lock (this does not acquire the lock, this just creates it) 194 mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)) 195 .createWifiLock(WifiManager.WIFI_MODE_FULL, "MusicDemo_lock"); 196 197 198 // Create the music catalog metadata provider 199 mMusicProvider = new MusicProvider(); 200 mMusicProvider.retrieveMedia(new MusicProvider.Callback() { 201 @Override 202 public void onMusicCatalogReady(boolean success) { 203 mState = success ? PlaybackState.STATE_NONE : PlaybackState.STATE_ERROR; 204 } 205 }); 206 207 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 208 209 // Start a new MediaSession 210 mSession = new MediaSession(this, "MusicService"); 211 setSessionToken(mSession.getSessionToken()); 212 mSession.setCallback(new MediaSessionCallback()); 213 mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | 214 MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); 215 216 // Use these extras to reserve space for the corresponding actions, even when they are disabled 217 // in the playbackstate, so the custom actions don't reflow. 218 Bundle extras = new Bundle(); 219 extras.putBoolean( 220 "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT", 221 true); 222 extras.putBoolean( 223 "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS", 224 true); 225 // If you want to reserve the Queue slot when there is no queue 226 // (mSession.setQueue(emptylist)), uncomment the lines below: 227 // extras.putBoolean( 228 // "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE", 229 // true); 230 mSession.setExtras(extras); 231 232 updatePlaybackState(null); 233 234 mMediaNotificationManager = new MediaNotificationManager(this); 235 } 236 237 /* 238 * (non-Javadoc) 239 * @see android.app.Service#onDestroy() 240 */ 241 @Override 242 public void onDestroy() { 243 LogHelper.d(TAG, "onDestroy"); 244 245 // Service is being killed, so make sure we release our resources 246 handleStopRequest(null); 247 248 mDelayedStopHandler.removeCallbacksAndMessages(null); 249 // In particular, always release the MediaSession to clean up resources 250 // and notify associated MediaController(s). 251 mSession.release(); 252 } 253 254 255 @Override 256 public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { 257 LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName, 258 "; clientUid=" + clientUid + " ; rootHints=", rootHints); 259 // To ensure you are not allowing any arbitrary app to browse your app's contents, you 260 // need to check the origin: 261 if (!PackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) { 262 // If the request comes from an untrusted package, return null. No further calls will 263 // be made to other media browsing methods. 264 LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package " 265 + clientPackageName); 266 return null; 267 } 268 if (ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName)) { 269 // Optional: if your app needs to adapt ads, music library or anything else that 270 // needs to run differently when connected to the car, this is where you should handle 271 // it. 272 } 273 return new BrowserRoot(MEDIA_ID_ROOT, null); 274 } 275 276 @Override 277 public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) { 278 if (!mMusicProvider.isInitialized()) { 279 // Use result.detach to allow calling result.sendResult from another thread: 280 result.detach(); 281 282 mMusicProvider.retrieveMedia(new MusicProvider.Callback() { 283 @Override 284 public void onMusicCatalogReady(boolean success) { 285 if (success) { 286 loadChildrenImpl(parentMediaId, result); 287 } else { 288 updatePlaybackState(getString(R.string.error_no_metadata)); 289 result.sendResult(new ArrayList<MediaItem>()); 290 } 291 } 292 }); 293 294 } else { 295 // If our music catalog is already loaded/cached, load them into result immediately 296 loadChildrenImpl(parentMediaId, result); 297 } 298 } 299 300 /** 301 * Actual implementation of onLoadChildren that assumes that MusicProvider is already 302 * initialized. 303 */ 304 private void loadChildrenImpl(final String parentMediaId, 305 final Result<List<MediaBrowser.MediaItem>> result) { 306 LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId); 307 308 List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); 309 310 if (MEDIA_ID_ROOT.equals(parentMediaId)) { 311 LogHelper.d(TAG, "OnLoadChildren.ROOT"); 312 mediaItems.add(new MediaBrowser.MediaItem( 313 new MediaDescription.Builder() 314 .setMediaId(MEDIA_ID_MUSICS_BY_GENRE) 315 .setTitle(getString(R.string.browse_genres)) 316 .setIconUri(Uri.parse("android.resource://" + 317 "com.example.android.mediabrowserservice/drawable/ic_by_genre")) 318 .setSubtitle(getString(R.string.browse_genre_subtitle)) 319 .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE 320 )); 321 322 } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) { 323 LogHelper.d(TAG, "OnLoadChildren.GENRES"); 324 for (String genre: mMusicProvider.getGenres()) { 325 MediaBrowser.MediaItem item = new MediaBrowser.MediaItem( 326 new MediaDescription.Builder() 327 .setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE, genre)) 328 .setTitle(genre) 329 .setSubtitle(getString(R.string.browse_musics_by_genre_subtitle, genre)) 330 .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE 331 ); 332 mediaItems.add(item); 333 } 334 335 } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) { 336 String genre = extractBrowseCategoryFromMediaID(parentMediaId)[1]; 337 LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_GENRE genre=", genre); 338 for (MediaMetadata track: mMusicProvider.getMusicsByGenre(genre)) { 339 // Since mediaMetadata fields are immutable, we need to create a copy, so we 340 // can set a hierarchy-aware mediaID. We will need to know the media hierarchy 341 // when we get a onPlayFromMusicID call, so we can create the proper queue based 342 // on where the music was selected from (by artist, by genre, random, etc) 343 String hierarchyAwareMediaID = MediaIDHelper.createTrackMediaID( 344 MEDIA_ID_MUSICS_BY_GENRE, genre, track); 345 MediaMetadata trackCopy = new MediaMetadata.Builder(track) 346 .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID) 347 .build(); 348 MediaBrowser.MediaItem bItem = new MediaBrowser.MediaItem( 349 trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE); 350 mediaItems.add(bItem); 351 } 352 } else { 353 LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId); 354 } 355 result.sendResult(mediaItems); 356 } 357 358 359 360 private final class MediaSessionCallback extends MediaSession.Callback { 361 @Override 362 public void onPlay() { 363 LogHelper.d(TAG, "play"); 364 365 if (mPlayingQueue == null || mPlayingQueue.isEmpty()) { 366 mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider); 367 mSession.setQueue(mPlayingQueue); 368 mSession.setQueueTitle(getString(R.string.random_queue_title)); 369 // start playing from the beginning of the queue 370 mCurrentIndexOnQueue = 0; 371 } 372 373 if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { 374 handlePlayRequest(); 375 } 376 } 377 378 @Override 379 public void onSkipToQueueItem(long queueId) { 380 LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId); 381 382 if (mState == PlaybackState.STATE_PAUSED) { 383 mState = PlaybackState.STATE_STOPPED; 384 } 385 386 if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { 387 388 // set the current index on queue from the music Id: 389 mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId); 390 391 // play the music 392 handlePlayRequest(); 393 } 394 } 395 396 @Override 397 public void onPlayFromMediaId(String mediaId, Bundle extras) { 398 LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, " extras=", extras); 399 400 if (mState == PlaybackState.STATE_PAUSED) { 401 mState = PlaybackState.STATE_STOPPED; 402 } 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 String uniqueMusicID = MediaIDHelper.extractMusicIDFromMediaID(mediaId); 417 418 // set the current index on queue from the music Id: 419 mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue( 420 mPlayingQueue, uniqueMusicID); 421 422 // play the music 423 handlePlayRequest(); 424 } 425 } 426 427 @Override 428 public void onPause() { 429 LogHelper.d(TAG, "pause. current state=" + mState); 430 handlePauseRequest(); 431 } 432 433 @Override 434 public void onStop() { 435 LogHelper.d(TAG, "stop. current state=" + mState); 436 handleStopRequest(null); 437 } 438 439 @Override 440 public void onSkipToNext() { 441 LogHelper.d(TAG, "skipToNext"); 442 mCurrentIndexOnQueue++; 443 if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) { 444 mCurrentIndexOnQueue = 0; 445 } 446 if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { 447 mState = PlaybackState.STATE_STOPPED; 448 handlePlayRequest(); 449 } else { 450 LogHelper.e(TAG, "skipToNext: cannot skip to next. next Index=" + 451 mCurrentIndexOnQueue + " queue length=" + 452 (mPlayingQueue == null ? "null" : mPlayingQueue.size())); 453 handleStopRequest("Cannot skip"); 454 } 455 } 456 457 @Override 458 public void onSkipToPrevious() { 459 LogHelper.d(TAG, "skipToPrevious"); 460 mCurrentIndexOnQueue--; 461 if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) { 462 // This sample's behavior: skipping to previous when in first song restarts the 463 // first song. 464 mCurrentIndexOnQueue = 0; 465 } 466 if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { 467 mState = PlaybackState.STATE_STOPPED; 468 handlePlayRequest(); 469 } else { 470 LogHelper.e(TAG, "skipToPrevious: cannot skip to previous. previous Index=" + 471 mCurrentIndexOnQueue + " queue length=" + 472 (mPlayingQueue == null ? "null" : mPlayingQueue.size())); 473 handleStopRequest("Cannot skip"); 474 } 475 } 476 477 @Override 478 public void onCustomAction(String action, Bundle extras) { 479 if (CUSTOM_ACTION_THUMBS_UP.equals(action)) { 480 LogHelper.i(TAG, "onCustomAction: favorite for current track"); 481 MediaMetadata track = getCurrentPlayingMusic(); 482 if (track != null) { 483 String mediaId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); 484 mMusicProvider.setFavorite(mediaId, !mMusicProvider.isFavorite(mediaId)); 485 } 486 updatePlaybackState(null); 487 } else { 488 LogHelper.e(TAG, "Unsupported action: ", action); 489 } 490 491 } 492 493 @Override 494 public void onPlayFromSearch(String query, Bundle extras) { 495 LogHelper.d(TAG, "playFromSearch query=", query); 496 497 if (mState == PlaybackState.STATE_PAUSED) { 498 mState = PlaybackState.STATE_STOPPED; 499 } 500 501 mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider); 502 LogHelper.d(TAG, "playFromSearch playqueue.length=" + mPlayingQueue.size()); 503 mSession.setQueue(mPlayingQueue); 504 505 if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { 506 // start playing from the beginning of the queue 507 mCurrentIndexOnQueue = 0; 508 509 handlePlayRequest(); 510 } 511 } 512 } 513 514 515 516 /* 517 * Called when media player is done playing current song. 518 * @see android.media.MediaPlayer.OnCompletionListener 519 */ 520 @Override 521 public void onCompletion(MediaPlayer player) { 522 LogHelper.d(TAG, "onCompletion from MediaPlayer"); 523 // The media player finished playing the current song, so we go ahead 524 // and start the next. 525 if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { 526 // In this sample, we restart the playing queue when it gets to the end: 527 mCurrentIndexOnQueue++; 528 if (mCurrentIndexOnQueue >= mPlayingQueue.size()) { 529 mCurrentIndexOnQueue = 0; 530 } 531 handlePlayRequest(); 532 } else { 533 // If there is nothing to play, we stop and release the resources: 534 handleStopRequest(null); 535 } 536 } 537 538 /* 539 * Called when media player is done preparing. 540 * @see android.media.MediaPlayer.OnPreparedListener 541 */ 542 @Override 543 public void onPrepared(MediaPlayer player) { 544 LogHelper.d(TAG, "onPrepared from MediaPlayer"); 545 // The media player is done preparing. That means we can start playing if we 546 // have audio focus. 547 configMediaPlayerState(); 548 } 549 550 /** 551 * Called when there's an error playing media. When this happens, the media 552 * player goes to the Error state. We warn the user about the error and 553 * reset the media player. 554 * 555 * @see android.media.MediaPlayer.OnErrorListener 556 */ 557 @Override 558 public boolean onError(MediaPlayer mp, int what, int extra) { 559 LogHelper.e(TAG, "Media player error: what=" + what + ", extra=" + extra); 560 handleStopRequest("MediaPlayer error " + what + " (" + extra + ")"); 561 return true; // true indicates we handled the error 562 } 563 564 565 566 567 /** 568 * Called by AudioManager on audio focus changes. 569 */ 570 @Override 571 public void onAudioFocusChange(int focusChange) { 572 LogHelper.d(TAG, "onAudioFocusChange. focusChange=" + focusChange); 573 if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { 574 // We have gained focus: 575 mAudioFocus = AudioFocus.Focused; 576 577 } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS || 578 focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || 579 focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { 580 // We have lost focus. If we can duck (low playback volume), we can keep playing. 581 // Otherwise, we need to pause the playback. 582 boolean canDuck = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK; 583 mAudioFocus = canDuck ? AudioFocus.NoFocusCanDuck : AudioFocus.NoFocusNoDuck; 584 585 // If we are playing, we need to reset media player by calling configMediaPlayerState 586 // with mAudioFocus properly set. 587 if (mState == PlaybackState.STATE_PLAYING && !canDuck) { 588 // If we don't have audio focus and can't duck, we save the information that 589 // we were playing, so that we can resume playback once we get the focus back. 590 mPlayOnFocusGain = true; 591 } 592 } else { 593 LogHelper.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange); 594 } 595 596 configMediaPlayerState(); 597 } 598 599 600 601 /** 602 * Handle a request to play music 603 */ 604 private void handlePlayRequest() { 605 LogHelper.d(TAG, "handlePlayRequest: mState=" + mState); 606 607 mDelayedStopHandler.removeCallbacksAndMessages(null); 608 if (!mServiceStarted) { 609 LogHelper.v(TAG, "Starting service"); 610 // The MusicService needs to keep running even after the calling MediaBrowser 611 // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer 612 // need to play media. 613 startService(new Intent(getApplicationContext(), MusicService.class)); 614 mServiceStarted = true; 615 } 616 617 mPlayOnFocusGain = true; 618 tryToGetAudioFocus(); 619 620 if (!mSession.isActive()) { 621 mSession.setActive(true); 622 } 623 624 // actually play the song 625 if (mState == PlaybackState.STATE_PAUSED) { 626 // If we're paused, just continue playback and restore the 627 // 'foreground service' state. 628 configMediaPlayerState(); 629 } else { 630 // If we're stopped or playing a song, 631 // just go ahead to the new song and (re)start playing 632 playCurrentSong(); 633 } 634 } 635 636 637 /** 638 * Handle a request to pause music 639 */ 640 private void handlePauseRequest() { 641 LogHelper.d(TAG, "handlePauseRequest: mState=" + mState); 642 643 if (mState == PlaybackState.STATE_PLAYING) { 644 // Pause media player and cancel the 'foreground service' state. 645 mState = PlaybackState.STATE_PAUSED; 646 if (mMediaPlayer.isPlaying()) { 647 mMediaPlayer.pause(); 648 } 649 // while paused, retain the MediaPlayer but give up audio focus 650 relaxResources(false); 651 giveUpAudioFocus(); 652 } 653 updatePlaybackState(null); 654 } 655 656 /** 657 * Handle a request to stop music 658 */ 659 private void handleStopRequest(String withError) { 660 LogHelper.d(TAG, "handleStopRequest: mState=" + mState + " error=", withError); 661 mState = PlaybackState.STATE_STOPPED; 662 663 // let go of all resources... 664 relaxResources(true); 665 giveUpAudioFocus(); 666 updatePlaybackState(withError); 667 668 mMediaNotificationManager.stopNotification(); 669 670 // service is no longer necessary. Will be started again if needed. 671 stopSelf(); 672 mServiceStarted = false; 673 } 674 675 /** 676 * Releases resources used by the service for playback. This includes the 677 * "foreground service" status, the wake locks and possibly the MediaPlayer. 678 * 679 * @param releaseMediaPlayer Indicates whether the Media Player should also 680 * be released or not 681 */ 682 private void relaxResources(boolean releaseMediaPlayer) { 683 LogHelper.d(TAG, "relaxResources. releaseMediaPlayer=" + releaseMediaPlayer); 684 // stop being a foreground service 685 stopForeground(true); 686 687 // reset the delayed stop handler. 688 mDelayedStopHandler.removeCallbacksAndMessages(null); 689 mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY); 690 691 // stop and release the Media Player, if it's available 692 if (releaseMediaPlayer && mMediaPlayer != null) { 693 mMediaPlayer.reset(); 694 mMediaPlayer.release(); 695 mMediaPlayer = null; 696 } 697 698 // we can also release the Wifi lock, if we're holding it 699 if (mWifiLock.isHeld()) { 700 mWifiLock.release(); 701 } 702 } 703 704 /** 705 * Reconfigures MediaPlayer according to audio focus settings and 706 * starts/restarts it. This method starts/restarts the MediaPlayer 707 * respecting the current audio focus state. So if we have focus, it will 708 * play normally; if we don't have focus, it will either leave the 709 * MediaPlayer paused or set it to a low volume, depending on what is 710 * allowed by the current focus settings. This method assumes mPlayer != 711 * null, so if you are calling it, you have to do so from a context where 712 * you are sure this is the case. 713 */ 714 private void configMediaPlayerState() { 715 LogHelper.d(TAG, "configAndStartMediaPlayer. mAudioFocus=" + mAudioFocus); 716 if (mAudioFocus == AudioFocus.NoFocusNoDuck) { 717 // If we don't have audio focus and can't duck, we have to pause, 718 if (mState == PlaybackState.STATE_PLAYING) { 719 handlePauseRequest(); 720 } 721 } else { // we have audio focus: 722 if (mAudioFocus == AudioFocus.NoFocusCanDuck) { 723 mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet 724 } else { 725 mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again 726 } 727 // If we were playing when we lost focus, we need to resume playing. 728 if (mPlayOnFocusGain) { 729 if (!mMediaPlayer.isPlaying()) { 730 LogHelper.d(TAG, "configAndStartMediaPlayer startMediaPlayer."); 731 mMediaPlayer.start(); 732 } 733 mPlayOnFocusGain = false; 734 mState = PlaybackState.STATE_PLAYING; 735 } 736 } 737 updatePlaybackState(null); 738 } 739 740 /** 741 * Makes sure the media player exists and has been reset. This will create 742 * the media player if needed, or reset the existing media player if one 743 * already exists. 744 */ 745 private void createMediaPlayerIfNeeded() { 746 LogHelper.d(TAG, "createMediaPlayerIfNeeded. needed? " + (mMediaPlayer==null)); 747 if (mMediaPlayer == null) { 748 mMediaPlayer = new MediaPlayer(); 749 750 // Make sure the media player will acquire a wake-lock while 751 // playing. If we don't do that, the CPU might go to sleep while the 752 // song is playing, causing playback to stop. 753 mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); 754 755 // we want the media player to notify us when it's ready preparing, 756 // and when it's done playing: 757 mMediaPlayer.setOnPreparedListener(this); 758 mMediaPlayer.setOnCompletionListener(this); 759 mMediaPlayer.setOnErrorListener(this); 760 } else { 761 mMediaPlayer.reset(); 762 } 763 } 764 765 /** 766 * Starts playing the current song in the playing queue. 767 */ 768 void playCurrentSong() { 769 MediaMetadata track = getCurrentPlayingMusic(); 770 if (track == null) { 771 LogHelper.e(TAG, "playSong: ignoring request to play next song, because cannot" + 772 " find it." + 773 " currentIndex=" + mCurrentIndexOnQueue + 774 " playQueue.size=" + (mPlayingQueue==null?"null": mPlayingQueue.size())); 775 return; 776 } 777 String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE); 778 LogHelper.d(TAG, "playSong: current (" + mCurrentIndexOnQueue + ") in playingQueue. " + 779 " musicId=" + track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) + 780 " source=" + source); 781 782 mState = PlaybackState.STATE_STOPPED; 783 relaxResources(false); // release everything except MediaPlayer 784 785 try { 786 createMediaPlayerIfNeeded(); 787 788 mState = PlaybackState.STATE_BUFFERING; 789 790 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 791 mMediaPlayer.setDataSource(source); 792 793 // Starts preparing the media player in the background. When 794 // it's done, it will call our OnPreparedListener (that is, 795 // the onPrepared() method on this class, since we set the 796 // listener to 'this'). Until the media player is prepared, 797 // we *cannot* call start() on it! 798 mMediaPlayer.prepareAsync(); 799 800 // If we are streaming from the internet, we want to hold a 801 // Wifi lock, which prevents the Wifi radio from going to 802 // sleep while the song is playing. 803 mWifiLock.acquire(); 804 805 updatePlaybackState(null); 806 updateMetadata(); 807 808 } catch (IOException ex) { 809 LogHelper.e(TAG, ex, "IOException playing song"); 810 updatePlaybackState(ex.getMessage()); 811 } 812 } 813 814 815 816 private void updateMetadata() { 817 if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { 818 LogHelper.e(TAG, "Can't retrieve current metadata."); 819 mState = PlaybackState.STATE_ERROR; 820 updatePlaybackState(getResources().getString(R.string.error_no_metadata)); 821 return; 822 } 823 MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue); 824 String mediaId = queueItem.getDescription().getMediaId(); 825 MediaMetadata track = mMusicProvider.getMusic(mediaId); 826 String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); 827 if (!mediaId.equals(trackId)) { 828 throw new IllegalStateException("track ID (" + trackId + ") " + 829 "should match mediaId (" + mediaId + ")"); 830 } 831 LogHelper.d(TAG, "Updating metadata for MusicID= " + mediaId); 832 mSession.setMetadata(track); 833 } 834 835 836 /** 837 * Update the current media player state, optionally showing an error message. 838 * 839 * @param error if not null, error message to present to the user. 840 * 841 */ 842 private void updatePlaybackState(String error) { 843 844 LogHelper.d(TAG, "updatePlaybackState, setting session playback state to " + mState); 845 long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN; 846 if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { 847 position = mMediaPlayer.getCurrentPosition(); 848 } 849 PlaybackState.Builder stateBuilder = new PlaybackState.Builder() 850 .setActions(getAvailableActions()); 851 852 setCustomAction(stateBuilder); 853 854 // If there is an error message, send it to the playback state: 855 if (error != null) { 856 // Error states are really only supposed to be used for errors that cause playback to 857 // stop unexpectedly and persist until the user takes action to fix it. 858 stateBuilder.setErrorMessage(error); 859 mState = PlaybackState.STATE_ERROR; 860 } 861 stateBuilder.setState(mState, position, 1.0f, SystemClock.elapsedRealtime()); 862 863 // Set the activeQueueItemId if the current index is valid. 864 if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { 865 MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue); 866 stateBuilder.setActiveQueueItemId(item.getQueueId()); 867 } 868 869 mSession.setPlaybackState(stateBuilder.build()); 870 871 if (mState == PlaybackState.STATE_PLAYING || mState == PlaybackState.STATE_PAUSED) { 872 mMediaNotificationManager.startNotification(); 873 } 874 } 875 876 private void setCustomAction(PlaybackState.Builder stateBuilder) { 877 MediaMetadata currentMusic = getCurrentPlayingMusic(); 878 if (currentMusic != null) { 879 // Set appropriate "Favorite" icon on Custom action: 880 String mediaId = currentMusic.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); 881 int favoriteIcon = R.drawable.ic_star_off; 882 if (mMusicProvider.isFavorite(mediaId)) { 883 favoriteIcon = R.drawable.ic_star_on; 884 } 885 LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ", 886 mediaId, " current favorite=", mMusicProvider.isFavorite(mediaId)); 887 stateBuilder.addCustomAction(CUSTOM_ACTION_THUMBS_UP, getString(R.string.favorite), 888 favoriteIcon); 889 } 890 } 891 892 private long getAvailableActions() { 893 long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | 894 PlaybackState.ACTION_PLAY_FROM_SEARCH; 895 if (mPlayingQueue == null || mPlayingQueue.isEmpty()) { 896 return actions; 897 } 898 if (mState == PlaybackState.STATE_PLAYING) { 899 actions |= PlaybackState.ACTION_PAUSE; 900 } 901 if (mCurrentIndexOnQueue > 0) { 902 actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; 903 } 904 if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) { 905 actions |= PlaybackState.ACTION_SKIP_TO_NEXT; 906 } 907 return actions; 908 } 909 910 private MediaMetadata getCurrentPlayingMusic() { 911 if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { 912 MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue); 913 if (item != null) { 914 LogHelper.d(TAG, "getCurrentPlayingMusic for musicId=", 915 item.getDescription().getMediaId()); 916 return mMusicProvider.getMusic(item.getDescription().getMediaId()); 917 } 918 } 919 return null; 920 } 921 922 /** 923 * Try to get the system audio focus. 924 */ 925 void tryToGetAudioFocus() { 926 LogHelper.d(TAG, "tryToGetAudioFocus"); 927 if (mAudioFocus != AudioFocus.Focused) { 928 int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, 929 AudioManager.AUDIOFOCUS_GAIN); 930 if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 931 mAudioFocus = AudioFocus.Focused; 932 } 933 } 934 } 935 936 /** 937 * Give up the audio focus. 938 */ 939 void giveUpAudioFocus() { 940 LogHelper.d(TAG, "giveUpAudioFocus"); 941 if (mAudioFocus == AudioFocus.Focused) { 942 if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 943 mAudioFocus = AudioFocus.NoFocusNoDuck; 944 } 945 } 946 } 947 } 948