• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 &lt;automotiveApp&gt; root element. For a media app, this must include
81  *      an &lt;uses name="media"/&gt; element as a child.
82  *      For example, in AndroidManifest.xml:
83  *          &lt;meta-data android:name="com.google.android.gms.car.application"
84  *              android:resource="@xml/automotive_app_desc"/&gt;
85  *      And in res/values/automotive_app_desc.xml:
86  *          &lt;automotiveApp&gt;
87  *              &lt;uses name="media"/&gt;
88  *          &lt;/automotiveApp&gt;
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