| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.example.android.mediabrowserservice; |
| |
| import android.content.Context; |
| import android.content.Intent; |
| import android.media.AudioManager; |
| import android.media.MediaDescription; |
| import android.media.MediaMetadata; |
| import android.media.MediaPlayer; |
| import android.media.MediaPlayer.OnCompletionListener; |
| import android.media.MediaPlayer.OnErrorListener; |
| import android.media.MediaPlayer.OnPreparedListener; |
| import android.media.browse.MediaBrowser; |
| import android.media.browse.MediaBrowser.MediaItem; |
| import android.media.session.MediaSession; |
| import android.media.session.PlaybackState; |
| import android.net.Uri; |
| import android.net.wifi.WifiManager; |
| import android.net.wifi.WifiManager.WifiLock; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.os.PowerManager; |
| import android.os.SystemClock; |
| import android.service.media.MediaBrowserService; |
| |
| import com.example.android.mediabrowserservice.model.MusicProvider; |
| import com.example.android.mediabrowserservice.utils.LogHelper; |
| import com.example.android.mediabrowserservice.utils.MediaIDHelper; |
| import com.example.android.mediabrowserservice.utils.QueueHelper; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE; |
| import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_ROOT; |
| import static com.example.android.mediabrowserservice.utils.MediaIDHelper.createBrowseCategoryMediaID; |
| import static com.example.android.mediabrowserservice.utils.MediaIDHelper.extractBrowseCategoryFromMediaID; |
| |
| /** |
| * This class provides a MediaBrowser through a service. It exposes the media library to a browsing |
| * client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and |
| * exposes it through its MediaSession.Token, which allows the client to create a MediaController |
| * that connects to and send control commands to the MediaSession remotely. This is useful for |
| * user interfaces that need to interact with your media session, like Android Auto. You can |
| * (should) also use the same service from your app's UI, which gives a seamless playback |
| * experience to the user. |
| * |
| * To implement a MediaBrowserService, you need to: |
| * |
| * <ul> |
| * |
| * <li> Extend {@link android.service.media.MediaBrowserService}, implementing the media browsing |
| * related methods {@link android.service.media.MediaBrowserService#onGetRoot} and |
| * {@link android.service.media.MediaBrowserService#onLoadChildren}; |
| * <li> In onCreate, start a new {@link android.media.session.MediaSession} and notify its parent |
| * with the session's token {@link android.service.media.MediaBrowserService#setSessionToken}; |
| * |
| * <li> Set a callback on the |
| * {@link android.media.session.MediaSession#setCallback(android.media.session.MediaSession.Callback)}. |
| * The callback will receive all the user's actions, like play, pause, etc; |
| * |
| * <li> Handle all the actual music playing using any method your app prefers (for example, |
| * {@link android.media.MediaPlayer}) |
| * |
| * <li> Update playbackState, "now playing" metadata and queue, using MediaSession proper methods |
| * {@link android.media.session.MediaSession#setPlaybackState(android.media.session.PlaybackState)} |
| * {@link android.media.session.MediaSession#setMetadata(android.media.MediaMetadata)} and |
| * {@link android.media.session.MediaSession#setQueue(java.util.List)}) |
| * |
| * <li> Declare and export the service in AndroidManifest with an intent receiver for the action |
| * android.media.browse.MediaBrowserService |
| * |
| * </ul> |
| * |
| * To make your app compatible with Android Auto, you also need to: |
| * |
| * <ul> |
| * |
| * <li> Declare a meta-data tag in AndroidManifest.xml linking to a xml resource |
| * with a <automotiveApp> root element. For a media app, this must include |
| * an <uses name="media"/> element as a child. |
| * For example, in AndroidManifest.xml: |
| * <meta-data android:name="com.google.android.gms.car.application" |
| * android:resource="@xml/automotive_app_desc"/> |
| * And in res/values/automotive_app_desc.xml: |
| * <automotiveApp> |
| * <uses name="media"/> |
| * </automotiveApp> |
| * |
| * </ul> |
| |
| * @see <a href="README.md">README.md</a> for more details. |
| * |
| */ |
| |
| public class MusicService extends MediaBrowserService implements OnPreparedListener, |
| OnCompletionListener, OnErrorListener, AudioManager.OnAudioFocusChangeListener { |
| |
| private static final String TAG = "MusicService"; |
| |
| // Action to thumbs up a media item |
| private static final String CUSTOM_ACTION_THUMBS_UP = "thumbs_up"; |
| // Delay stopSelf by using a handler. |
| private static final int STOP_DELAY = 30000; |
| |
| // The volume we set the media player to when we lose audio focus, but are |
| // allowed to reduce the volume instead of stopping playback. |
| public static final float VOLUME_DUCK = 0.2f; |
| |
| // The volume we set the media player when we have audio focus. |
| public static final float VOLUME_NORMAL = 1.0f; |
| public static final String ANDROID_AUTO_PACKAGE_NAME = "com.google.android.projection.gearhead"; |
| public static final String ANDROID_AUTO_SIMULATOR_PACKAGE_NAME = "com.google.android.mediasimulator"; |
| |
| // Music catalog manager |
| private MusicProvider mMusicProvider; |
| |
| private MediaSession mSession; |
| private MediaPlayer mMediaPlayer; |
| |
| // "Now playing" queue: |
| private List<MediaSession.QueueItem> mPlayingQueue; |
| private int mCurrentIndexOnQueue; |
| |
| // Current local media player state |
| private int mState = PlaybackState.STATE_NONE; |
| |
| // Wifi lock that we hold when streaming files from the internet, in order |
| // to prevent the device from shutting off the Wifi radio |
| private WifiLock mWifiLock; |
| |
| private MediaNotification mMediaNotification; |
| |
| // Indicates whether the service was started. |
| private boolean mServiceStarted; |
| |
| enum AudioFocus { |
| NoFocusNoDuck, // we don't have audio focus, and can't duck |
| NoFocusCanDuck, // we don't have focus, but can play at a low volume |
| // ("ducking") |
| Focused // we have full audio focus |
| } |
| |
| // Type of audio focus we have: |
| private AudioFocus mAudioFocus = AudioFocus.NoFocusNoDuck; |
| private AudioManager mAudioManager; |
| |
| // Indicates if we should start playing immediately after we gain focus. |
| private boolean mPlayOnFocusGain; |
| |
| private Handler mDelayedStopHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| if ((mMediaPlayer != null && mMediaPlayer.isPlaying()) || |
| mPlayOnFocusGain) { |
| LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use."); |
| return; |
| } |
| LogHelper.d(TAG, "Stopping service with delay handler."); |
| stopSelf(); |
| mServiceStarted = false; |
| } |
| }; |
| |
| /* |
| * (non-Javadoc) |
| * @see android.app.Service#onCreate() |
| */ |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| LogHelper.d(TAG, "onCreate"); |
| |
| mPlayingQueue = new ArrayList<>(); |
| |
| // Create the Wifi lock (this does not acquire the lock, this just creates it) |
| mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)) |
| .createWifiLock(WifiManager.WIFI_MODE_FULL, "MusicDemo_lock"); |
| |
| |
| // Create the music catalog metadata provider |
| mMusicProvider = new MusicProvider(); |
| mMusicProvider.retrieveMedia(new MusicProvider.Callback() { |
| @Override |
| public void onMusicCatalogReady(boolean success) { |
| mState = success ? PlaybackState.STATE_NONE : PlaybackState.STATE_ERROR; |
| } |
| }); |
| |
| mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); |
| |
| // Start a new MediaSession |
| mSession = new MediaSession(this, "MusicService"); |
| setSessionToken(mSession.getSessionToken()); |
| mSession.setCallback(new MediaSessionCallback()); |
| mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | |
| MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); |
| |
| // Use these extras to reserve space for the corresponding actions, even when they are disabled |
| // in the playbackstate, so the custom actions don't reflow. |
| Bundle extras = new Bundle(); |
| extras.putBoolean( |
| "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT", |
| true); |
| extras.putBoolean( |
| "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS", |
| true); |
| // If you want to reserve the Queue slot when there is no queue |
| // (mSession.setQueue(emptylist)), uncomment the lines below: |
| // extras.putBoolean( |
| // "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE", |
| // true); |
| mSession.setExtras(extras); |
| |
| updatePlaybackState(null); |
| |
| mMediaNotification = new MediaNotification(this); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * @see android.app.Service#onDestroy() |
| */ |
| @Override |
| public void onDestroy() { |
| LogHelper.d(TAG, "onDestroy"); |
| |
| // Service is being killed, so make sure we release our resources |
| handleStopRequest(null); |
| |
| mDelayedStopHandler.removeCallbacksAndMessages(null); |
| // In particular, always release the MediaSession to clean up resources |
| // and notify associated MediaController(s). |
| mSession.release(); |
| } |
| |
| |
| @Override |
| public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { |
| LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName, |
| "; clientUid=" + clientUid + " ; rootHints=", rootHints); |
| // To ensure you are not allowing any arbitrary app to browse your app's contents, you |
| // need to check the origin: |
| if (!PackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) { |
| // If the request comes from an untrusted package, return null. No further calls will |
| // be made to other media browsing methods. |
| LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package " |
| + clientPackageName); |
| return null; |
| } |
| if (ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName)) { |
| // Optional: if your app needs to adapt ads, music library or anything else that |
| // needs to run differently when connected to the car, this is where you should handle |
| // it. |
| } |
| return new BrowserRoot(MEDIA_ID_ROOT, null); |
| } |
| |
| @Override |
| public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) { |
| if (!mMusicProvider.isInitialized()) { |
| // Use result.detach to allow calling result.sendResult from another thread: |
| result.detach(); |
| |
| mMusicProvider.retrieveMedia(new MusicProvider.Callback() { |
| @Override |
| public void onMusicCatalogReady(boolean success) { |
| if (success) { |
| loadChildrenImpl(parentMediaId, result); |
| } else { |
| updatePlaybackState(getString(R.string.error_no_metadata)); |
| result.sendResult(new ArrayList<MediaItem>()); |
| } |
| } |
| }); |
| |
| } else { |
| // If our music catalog is already loaded/cached, load them into result immediately |
| loadChildrenImpl(parentMediaId, result); |
| } |
| } |
| |
| /** |
| * Actual implementation of onLoadChildren that assumes that MusicProvider is already |
| * initialized. |
| */ |
| private void loadChildrenImpl(final String parentMediaId, |
| final Result<List<MediaBrowser.MediaItem>> result) { |
| LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId); |
| |
| List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); |
| |
| if (MEDIA_ID_ROOT.equals(parentMediaId)) { |
| LogHelper.d(TAG, "OnLoadChildren.ROOT"); |
| mediaItems.add(new MediaBrowser.MediaItem( |
| new MediaDescription.Builder() |
| .setMediaId(MEDIA_ID_MUSICS_BY_GENRE) |
| .setTitle(getString(R.string.browse_genres)) |
| .setIconUri(Uri.parse("android.resource://" + |
| "com.example.android.mediabrowserservice/drawable/ic_by_genre")) |
| .setSubtitle(getString(R.string.browse_genre_subtitle)) |
| .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE |
| )); |
| |
| } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) { |
| LogHelper.d(TAG, "OnLoadChildren.GENRES"); |
| for (String genre: mMusicProvider.getGenres()) { |
| MediaBrowser.MediaItem item = new MediaBrowser.MediaItem( |
| new MediaDescription.Builder() |
| .setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE, genre)) |
| .setTitle(genre) |
| .setSubtitle(getString(R.string.browse_musics_by_genre_subtitle, genre)) |
| .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE |
| ); |
| mediaItems.add(item); |
| } |
| |
| } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) { |
| String genre = extractBrowseCategoryFromMediaID(parentMediaId)[1]; |
| LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_GENRE genre=", genre); |
| for (MediaMetadata track: mMusicProvider.getMusicsByGenre(genre)) { |
| // Since mediaMetadata fields are immutable, we need to create a copy, so we |
| // can set a hierarchy-aware mediaID. We will need to know the media hierarchy |
| // when we get a onPlayFromMusicID call, so we can create the proper queue based |
| // on where the music was selected from (by artist, by genre, random, etc) |
| String hierarchyAwareMediaID = MediaIDHelper.createTrackMediaID( |
| MEDIA_ID_MUSICS_BY_GENRE, genre, track); |
| MediaMetadata trackCopy = new MediaMetadata.Builder(track) |
| .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID) |
| .build(); |
| MediaBrowser.MediaItem bItem = new MediaBrowser.MediaItem( |
| trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE); |
| mediaItems.add(bItem); |
| } |
| } else { |
| LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId); |
| } |
| result.sendResult(mediaItems); |
| } |
| |
| |
| |
| private final class MediaSessionCallback extends MediaSession.Callback { |
| @Override |
| public void onPlay() { |
| LogHelper.d(TAG, "play"); |
| |
| if (mPlayingQueue == null || mPlayingQueue.isEmpty()) { |
| mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider); |
| mSession.setQueue(mPlayingQueue); |
| mSession.setQueueTitle(getString(R.string.random_queue_title)); |
| // start playing from the beginning of the queue |
| mCurrentIndexOnQueue = 0; |
| } |
| |
| if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { |
| handlePlayRequest(); |
| } |
| } |
| |
| @Override |
| public void onSkipToQueueItem(long queueId) { |
| LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId); |
| |
| if (mState == PlaybackState.STATE_PAUSED) { |
| mState = PlaybackState.STATE_STOPPED; |
| } |
| |
| if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { |
| |
| // set the current index on queue from the music Id: |
| mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId); |
| |
| // play the music |
| handlePlayRequest(); |
| } |
| } |
| |
| @Override |
| public void onPlayFromMediaId(String mediaId, Bundle extras) { |
| LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, " extras=", extras); |
| |
| if (mState == PlaybackState.STATE_PAUSED) { |
| mState = PlaybackState.STATE_STOPPED; |
| } |
| |
| // The mediaId used here is not the unique musicId. This one comes from the |
| // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of |
| // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary |
| // so we can build the correct playing queue, based on where the track was |
| // selected from. |
| mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider); |
| mSession.setQueue(mPlayingQueue); |
| String queueTitle = getString(R.string.browse_musics_by_genre_subtitle, |
| MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId)); |
| mSession.setQueueTitle(queueTitle); |
| |
| if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { |
| String uniqueMusicID = MediaIDHelper.extractMusicIDFromMediaID(mediaId); |
| |
| // set the current index on queue from the music Id: |
| mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue( |
| mPlayingQueue, uniqueMusicID); |
| |
| // play the music |
| handlePlayRequest(); |
| } |
| } |
| |
| @Override |
| public void onPause() { |
| LogHelper.d(TAG, "pause. current state=" + mState); |
| handlePauseRequest(); |
| } |
| |
| @Override |
| public void onStop() { |
| LogHelper.d(TAG, "stop. current state=" + mState); |
| handleStopRequest(null); |
| } |
| |
| @Override |
| public void onSkipToNext() { |
| LogHelper.d(TAG, "skipToNext"); |
| mCurrentIndexOnQueue++; |
| if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) { |
| mCurrentIndexOnQueue = 0; |
| } |
| if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { |
| mState = PlaybackState.STATE_STOPPED; |
| handlePlayRequest(); |
| } else { |
| LogHelper.e(TAG, "skipToNext: cannot skip to next. next Index=" + |
| mCurrentIndexOnQueue + " queue length=" + |
| (mPlayingQueue == null ? "null" : mPlayingQueue.size())); |
| handleStopRequest("Cannot skip"); |
| } |
| } |
| |
| @Override |
| public void onSkipToPrevious() { |
| LogHelper.d(TAG, "skipToPrevious"); |
| mCurrentIndexOnQueue--; |
| if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) { |
| // This sample's behavior: skipping to previous when in first song restarts the |
| // first song. |
| mCurrentIndexOnQueue = 0; |
| } |
| if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { |
| mState = PlaybackState.STATE_STOPPED; |
| handlePlayRequest(); |
| } else { |
| LogHelper.e(TAG, "skipToPrevious: cannot skip to previous. previous Index=" + |
| mCurrentIndexOnQueue + " queue length=" + |
| (mPlayingQueue == null ? "null" : mPlayingQueue.size())); |
| handleStopRequest("Cannot skip"); |
| } |
| } |
| |
| @Override |
| public void onCustomAction(String action, Bundle extras) { |
| if (CUSTOM_ACTION_THUMBS_UP.equals(action)) { |
| LogHelper.i(TAG, "onCustomAction: favorite for current track"); |
| MediaMetadata track = getCurrentPlayingMusic(); |
| if (track != null) { |
| String mediaId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); |
| mMusicProvider.setFavorite(mediaId, !mMusicProvider.isFavorite(mediaId)); |
| } |
| updatePlaybackState(null); |
| } else { |
| LogHelper.e(TAG, "Unsupported action: ", action); |
| } |
| |
| } |
| |
| @Override |
| public void onPlayFromSearch(String query, Bundle extras) { |
| LogHelper.d(TAG, "playFromSearch query=", query); |
| |
| if (mState == PlaybackState.STATE_PAUSED) { |
| mState = PlaybackState.STATE_STOPPED; |
| } |
| |
| mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider); |
| LogHelper.d(TAG, "playFromSearch playqueue.length=" + mPlayingQueue.size()); |
| mSession.setQueue(mPlayingQueue); |
| |
| if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { |
| // start playing from the beginning of the queue |
| mCurrentIndexOnQueue = 0; |
| |
| handlePlayRequest(); |
| } |
| } |
| } |
| |
| |
| |
| /* |
| * Called when media player is done playing current song. |
| * @see android.media.MediaPlayer.OnCompletionListener |
| */ |
| @Override |
| public void onCompletion(MediaPlayer player) { |
| LogHelper.d(TAG, "onCompletion from MediaPlayer"); |
| // The media player finished playing the current song, so we go ahead |
| // and start the next. |
| if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { |
| // In this sample, we restart the playing queue when it gets to the end: |
| mCurrentIndexOnQueue++; |
| if (mCurrentIndexOnQueue >= mPlayingQueue.size()) { |
| mCurrentIndexOnQueue = 0; |
| } |
| handlePlayRequest(); |
| } else { |
| // If there is nothing to play, we stop and release the resources: |
| handleStopRequest(null); |
| } |
| } |
| |
| /* |
| * Called when media player is done preparing. |
| * @see android.media.MediaPlayer.OnPreparedListener |
| */ |
| @Override |
| public void onPrepared(MediaPlayer player) { |
| LogHelper.d(TAG, "onPrepared from MediaPlayer"); |
| // The media player is done preparing. That means we can start playing if we |
| // have audio focus. |
| configMediaPlayerState(); |
| } |
| |
| /** |
| * Called when there's an error playing media. When this happens, the media |
| * player goes to the Error state. We warn the user about the error and |
| * reset the media player. |
| * |
| * @see android.media.MediaPlayer.OnErrorListener |
| */ |
| @Override |
| public boolean onError(MediaPlayer mp, int what, int extra) { |
| LogHelper.e(TAG, "Media player error: what=" + what + ", extra=" + extra); |
| handleStopRequest("MediaPlayer error " + what + " (" + extra + ")"); |
| return true; // true indicates we handled the error |
| } |
| |
| |
| |
| |
| /** |
| * Called by AudioManager on audio focus changes. |
| */ |
| @Override |
| public void onAudioFocusChange(int focusChange) { |
| LogHelper.d(TAG, "onAudioFocusChange. focusChange=" + focusChange); |
| if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { |
| // We have gained focus: |
| mAudioFocus = AudioFocus.Focused; |
| |
| } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS || |
| focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || |
| focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { |
| // We have lost focus. If we can duck (low playback volume), we can keep playing. |
| // Otherwise, we need to pause the playback. |
| boolean canDuck = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK; |
| mAudioFocus = canDuck ? AudioFocus.NoFocusCanDuck : AudioFocus.NoFocusNoDuck; |
| |
| // If we are playing, we need to reset media player by calling configMediaPlayerState |
| // with mAudioFocus properly set. |
| if (mState == PlaybackState.STATE_PLAYING && !canDuck) { |
| // If we don't have audio focus and can't duck, we save the information that |
| // we were playing, so that we can resume playback once we get the focus back. |
| mPlayOnFocusGain = true; |
| } |
| } else { |
| LogHelper.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange); |
| } |
| |
| configMediaPlayerState(); |
| } |
| |
| |
| |
| /** |
| * Handle a request to play music |
| */ |
| private void handlePlayRequest() { |
| LogHelper.d(TAG, "handlePlayRequest: mState=" + mState); |
| |
| mDelayedStopHandler.removeCallbacksAndMessages(null); |
| if (!mServiceStarted) { |
| LogHelper.v(TAG, "Starting service"); |
| // The MusicService needs to keep running even after the calling MediaBrowser |
| // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer |
| // need to play media. |
| startService(new Intent(getApplicationContext(), MusicService.class)); |
| mServiceStarted = true; |
| } |
| |
| mPlayOnFocusGain = true; |
| tryToGetAudioFocus(); |
| |
| if (!mSession.isActive()) { |
| mSession.setActive(true); |
| } |
| |
| // actually play the song |
| if (mState == PlaybackState.STATE_PAUSED) { |
| // If we're paused, just continue playback and restore the |
| // 'foreground service' state. |
| configMediaPlayerState(); |
| } else { |
| // If we're stopped or playing a song, |
| // just go ahead to the new song and (re)start playing |
| playCurrentSong(); |
| } |
| } |
| |
| |
| /** |
| * Handle a request to pause music |
| */ |
| private void handlePauseRequest() { |
| LogHelper.d(TAG, "handlePauseRequest: mState=" + mState); |
| |
| if (mState == PlaybackState.STATE_PLAYING) { |
| // Pause media player and cancel the 'foreground service' state. |
| mState = PlaybackState.STATE_PAUSED; |
| if (mMediaPlayer.isPlaying()) { |
| mMediaPlayer.pause(); |
| } |
| // while paused, retain the MediaPlayer but give up audio focus |
| relaxResources(false); |
| giveUpAudioFocus(); |
| } |
| updatePlaybackState(null); |
| } |
| |
| /** |
| * Handle a request to stop music |
| */ |
| private void handleStopRequest(String withError) { |
| LogHelper.d(TAG, "handleStopRequest: mState=" + mState + " error=", withError); |
| mState = PlaybackState.STATE_STOPPED; |
| |
| // let go of all resources... |
| relaxResources(true); |
| giveUpAudioFocus(); |
| updatePlaybackState(withError); |
| |
| mMediaNotification.stopNotification(); |
| |
| // service is no longer necessary. Will be started again if needed. |
| stopSelf(); |
| mServiceStarted = false; |
| } |
| |
| /** |
| * Releases resources used by the service for playback. This includes the |
| * "foreground service" status, the wake locks and possibly the MediaPlayer. |
| * |
| * @param releaseMediaPlayer Indicates whether the Media Player should also |
| * be released or not |
| */ |
| private void relaxResources(boolean releaseMediaPlayer) { |
| LogHelper.d(TAG, "relaxResources. releaseMediaPlayer=" + releaseMediaPlayer); |
| // stop being a foreground service |
| stopForeground(true); |
| |
| // reset the delayed stop handler. |
| mDelayedStopHandler.removeCallbacksAndMessages(null); |
| mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY); |
| |
| // stop and release the Media Player, if it's available |
| if (releaseMediaPlayer && mMediaPlayer != null) { |
| mMediaPlayer.reset(); |
| mMediaPlayer.release(); |
| mMediaPlayer = null; |
| } |
| |
| // we can also release the Wifi lock, if we're holding it |
| if (mWifiLock.isHeld()) { |
| mWifiLock.release(); |
| } |
| } |
| |
| /** |
| * Reconfigures MediaPlayer according to audio focus settings and |
| * starts/restarts it. This method starts/restarts the MediaPlayer |
| * respecting the current audio focus state. So if we have focus, it will |
| * play normally; if we don't have focus, it will either leave the |
| * MediaPlayer paused or set it to a low volume, depending on what is |
| * allowed by the current focus settings. This method assumes mPlayer != |
| * null, so if you are calling it, you have to do so from a context where |
| * you are sure this is the case. |
| */ |
| private void configMediaPlayerState() { |
| LogHelper.d(TAG, "configAndStartMediaPlayer. mAudioFocus=" + mAudioFocus); |
| if (mAudioFocus == AudioFocus.NoFocusNoDuck) { |
| // If we don't have audio focus and can't duck, we have to pause, |
| if (mState == PlaybackState.STATE_PLAYING) { |
| handlePauseRequest(); |
| } |
| } else { // we have audio focus: |
| if (mAudioFocus == AudioFocus.NoFocusCanDuck) { |
| mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet |
| } else { |
| mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again |
| } |
| // If we were playing when we lost focus, we need to resume playing. |
| if (mPlayOnFocusGain) { |
| if (!mMediaPlayer.isPlaying()) { |
| LogHelper.d(TAG, "configAndStartMediaPlayer startMediaPlayer."); |
| mMediaPlayer.start(); |
| } |
| mPlayOnFocusGain = false; |
| mState = PlaybackState.STATE_PLAYING; |
| } |
| } |
| updatePlaybackState(null); |
| } |
| |
| /** |
| * Makes sure the media player exists and has been reset. This will create |
| * the media player if needed, or reset the existing media player if one |
| * already exists. |
| */ |
| private void createMediaPlayerIfNeeded() { |
| LogHelper.d(TAG, "createMediaPlayerIfNeeded. needed? " + (mMediaPlayer==null)); |
| if (mMediaPlayer == null) { |
| mMediaPlayer = new MediaPlayer(); |
| |
| // Make sure the media player will acquire a wake-lock while |
| // playing. If we don't do that, the CPU might go to sleep while the |
| // song is playing, causing playback to stop. |
| mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); |
| |
| // we want the media player to notify us when it's ready preparing, |
| // and when it's done playing: |
| mMediaPlayer.setOnPreparedListener(this); |
| mMediaPlayer.setOnCompletionListener(this); |
| mMediaPlayer.setOnErrorListener(this); |
| } else { |
| mMediaPlayer.reset(); |
| } |
| } |
| |
| /** |
| * Starts playing the current song in the playing queue. |
| */ |
| void playCurrentSong() { |
| MediaMetadata track = getCurrentPlayingMusic(); |
| if (track == null) { |
| LogHelper.e(TAG, "playSong: ignoring request to play next song, because cannot" + |
| " find it." + |
| " currentIndex=" + mCurrentIndexOnQueue + |
| " playQueue.size=" + (mPlayingQueue==null?"null": mPlayingQueue.size())); |
| return; |
| } |
| String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE); |
| LogHelper.d(TAG, "playSong: current (" + mCurrentIndexOnQueue + ") in playingQueue. " + |
| " musicId=" + track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) + |
| " source=" + source); |
| |
| mState = PlaybackState.STATE_STOPPED; |
| relaxResources(false); // release everything except MediaPlayer |
| |
| try { |
| createMediaPlayerIfNeeded(); |
| |
| mState = PlaybackState.STATE_BUFFERING; |
| |
| mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); |
| mMediaPlayer.setDataSource(source); |
| |
| // Starts preparing the media player in the background. When |
| // it's done, it will call our OnPreparedListener (that is, |
| // the onPrepared() method on this class, since we set the |
| // listener to 'this'). Until the media player is prepared, |
| // we *cannot* call start() on it! |
| mMediaPlayer.prepareAsync(); |
| |
| // If we are streaming from the internet, we want to hold a |
| // Wifi lock, which prevents the Wifi radio from going to |
| // sleep while the song is playing. |
| mWifiLock.acquire(); |
| |
| updatePlaybackState(null); |
| updateMetadata(); |
| |
| } catch (IOException ex) { |
| LogHelper.e(TAG, ex, "IOException playing song"); |
| updatePlaybackState(ex.getMessage()); |
| } |
| } |
| |
| |
| |
| private void updateMetadata() { |
| if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { |
| LogHelper.e(TAG, "Can't retrieve current metadata."); |
| mState = PlaybackState.STATE_ERROR; |
| updatePlaybackState(getResources().getString(R.string.error_no_metadata)); |
| return; |
| } |
| MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue); |
| String mediaId = queueItem.getDescription().getMediaId(); |
| MediaMetadata track = mMusicProvider.getMusic(mediaId); |
| String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); |
| if (!mediaId.equals(trackId)) { |
| throw new IllegalStateException("track ID (" + trackId + ") " + |
| "should match mediaId (" + mediaId + ")"); |
| } |
| LogHelper.d(TAG, "Updating metadata for MusicID= " + mediaId); |
| mSession.setMetadata(track); |
| } |
| |
| |
| /** |
| * Update the current media player state, optionally showing an error message. |
| * |
| * @param error if not null, error message to present to the user. |
| * |
| */ |
| private void updatePlaybackState(String error) { |
| |
| LogHelper.d(TAG, "updatePlaybackState, setting session playback state to " + mState); |
| long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN; |
| if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { |
| position = mMediaPlayer.getCurrentPosition(); |
| } |
| PlaybackState.Builder stateBuilder = new PlaybackState.Builder() |
| .setActions(getAvailableActions()); |
| |
| setCustomAction(stateBuilder); |
| |
| // If there is an error message, send it to the playback state: |
| if (error != null) { |
| // Error states are really only supposed to be used for errors that cause playback to |
| // stop unexpectedly and persist until the user takes action to fix it. |
| stateBuilder.setErrorMessage(error); |
| mState = PlaybackState.STATE_ERROR; |
| } |
| stateBuilder.setState(mState, position, 1.0f, SystemClock.elapsedRealtime()); |
| |
| // Set the activeQueueItemId if the current index is valid. |
| if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { |
| MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue); |
| stateBuilder.setActiveQueueItemId(item.getQueueId()); |
| } |
| |
| mSession.setPlaybackState(stateBuilder.build()); |
| |
| if (mState == PlaybackState.STATE_PLAYING || mState == PlaybackState.STATE_PAUSED) { |
| mMediaNotification.startNotification(); |
| } |
| } |
| |
| private void setCustomAction(PlaybackState.Builder stateBuilder) { |
| MediaMetadata currentMusic = getCurrentPlayingMusic(); |
| if (currentMusic != null) { |
| // Set appropriate "Favorite" icon on Custom action: |
| String mediaId = currentMusic.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); |
| int favoriteIcon = R.drawable.ic_star_off; |
| if (mMusicProvider.isFavorite(mediaId)) { |
| favoriteIcon = R.drawable.ic_star_on; |
| } |
| LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ", |
| mediaId, " current favorite=", mMusicProvider.isFavorite(mediaId)); |
| stateBuilder.addCustomAction(CUSTOM_ACTION_THUMBS_UP, getString(R.string.favorite), |
| favoriteIcon); |
| } |
| } |
| |
| private long getAvailableActions() { |
| long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | |
| PlaybackState.ACTION_PLAY_FROM_SEARCH; |
| if (mPlayingQueue == null || mPlayingQueue.isEmpty()) { |
| return actions; |
| } |
| if (mState == PlaybackState.STATE_PLAYING) { |
| actions |= PlaybackState.ACTION_PAUSE; |
| } |
| if (mCurrentIndexOnQueue > 0) { |
| actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; |
| } |
| if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) { |
| actions |= PlaybackState.ACTION_SKIP_TO_NEXT; |
| } |
| return actions; |
| } |
| |
| private MediaMetadata getCurrentPlayingMusic() { |
| if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { |
| MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue); |
| if (item != null) { |
| LogHelper.d(TAG, "getCurrentPlayingMusic for musicId=", |
| item.getDescription().getMediaId()); |
| return mMusicProvider.getMusic(item.getDescription().getMediaId()); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Try to get the system audio focus. |
| */ |
| void tryToGetAudioFocus() { |
| LogHelper.d(TAG, "tryToGetAudioFocus"); |
| if (mAudioFocus != AudioFocus.Focused) { |
| int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, |
| AudioManager.AUDIOFOCUS_GAIN); |
| if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| mAudioFocus = AudioFocus.Focused; |
| } |
| } |
| } |
| |
| /** |
| * Give up the audio focus. |
| */ |
| void giveUpAudioFocus() { |
| LogHelper.d(TAG, "giveUpAudioFocus"); |
| if (mAudioFocus == AudioFocus.Focused) { |
| if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| mAudioFocus = AudioFocus.NoFocusNoDuck; |
| } |
| } |
| } |
| } |