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