• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 &lt;automotiveApp&gt; root element. For a media app, this must include
98  *      an &lt;uses name="media"/&gt; element as a child.
99  *      For example, in AndroidManifest.xml:
100  *          &lt;meta-data android:name="com.google.android.gms.car.application"
101  *              android:resource="@xml/automotive_app_desc"/&gt;
102  *      And in res/values/automotive_app_desc.xml:
103  *          &lt;automotiveApp&gt;
104  *              &lt;uses name="media"/&gt;
105  *          &lt;/automotiveApp&gt;
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 = "MusicService";
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 MediaNotification mMediaNotification;
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
onCreate()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         mMediaNotification = new MediaNotification(this);
235     }
236 
237     /*
238      * (non-Javadoc)
239      * @see android.app.Service#onDestroy()
240      */
241     @Override
onDestroy()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
onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)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
onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result)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      */
loadChildrenImpl(final String parentMediaId, final Result<List<MediaBrowser.MediaItem>> result)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
onPlay()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
onSkipToQueueItem(long queueId)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
onPlayFromMediaId(String mediaId, Bundle extras)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
onPause()428         public void onPause() {
429             LogHelper.d(TAG, "pause. current state=" + mState);
430             handlePauseRequest();
431         }
432 
433         @Override
onStop()434         public void onStop() {
435             LogHelper.d(TAG, "stop. current state=" + mState);
436             handleStopRequest(null);
437         }
438 
439         @Override
onSkipToNext()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
onSkipToPrevious()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
onCustomAction(String action, Bundle extras)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
onPlayFromSearch(String query, Bundle extras)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
onCompletion(MediaPlayer player)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
onPrepared(MediaPlayer player)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
onError(MediaPlayer mp, int what, int extra)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
onAudioFocusChange(int focusChange)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      */
handlePlayRequest()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      */
handlePauseRequest()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      */
handleStopRequest(String withError)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         mMediaNotification.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      */
relaxResources(boolean releaseMediaPlayer)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      */
configMediaPlayerState()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      */
createMediaPlayerIfNeeded()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      */
playCurrentSong()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 
updateMetadata()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      */
updatePlaybackState(String error)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             mMediaNotification.startNotification();
873         }
874     }
875 
setCustomAction(PlaybackState.Builder stateBuilder)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 
getAvailableActions()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 
getCurrentPlayingMusic()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      */
tryToGetAudioFocus()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      */
giveUpAudioFocus()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