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