| /* |
| * 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.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.media.AudioManager; |
| import android.media.MediaMetadata; |
| import android.media.MediaPlayer; |
| import android.media.session.PlaybackState; |
| import android.net.wifi.WifiManager; |
| import android.os.PowerManager; |
| import android.text.TextUtils; |
| |
| import com.example.android.mediabrowserservice.model.MusicProvider; |
| import com.example.android.mediabrowserservice.utils.LogHelper; |
| import com.example.android.mediabrowserservice.utils.MediaIDHelper; |
| |
| import java.io.IOException; |
| |
| import static android.media.MediaPlayer.OnCompletionListener; |
| import static android.media.MediaPlayer.OnErrorListener; |
| import static android.media.MediaPlayer.OnPreparedListener; |
| import static android.media.MediaPlayer.OnSeekCompleteListener; |
| import static android.media.session.MediaSession.QueueItem; |
| |
| /** |
| * A class that implements local media playback using {@link android.media.MediaPlayer} |
| */ |
| public class Playback implements AudioManager.OnAudioFocusChangeListener, |
| OnCompletionListener, OnErrorListener, OnPreparedListener, OnSeekCompleteListener { |
| |
| private static final String TAG = LogHelper.makeLogTag(Playback.class); |
| |
| // 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; |
| |
| // we don't have audio focus, and can't duck (play at a low volume) |
| private static final int AUDIO_NO_FOCUS_NO_DUCK = 0; |
| // we don't have focus, but can duck (play at a low volume) |
| private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1; |
| // we have full audio focus |
| private static final int AUDIO_FOCUSED = 2; |
| |
| private final MusicService mService; |
| private final WifiManager.WifiLock mWifiLock; |
| private int mState; |
| private boolean mPlayOnFocusGain; |
| private Callback mCallback; |
| private MusicProvider mMusicProvider; |
| private volatile boolean mAudioNoisyReceiverRegistered; |
| private volatile int mCurrentPosition; |
| private volatile String mCurrentMediaId; |
| |
| // Type of audio focus we have: |
| private int mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK; |
| private AudioManager mAudioManager; |
| private MediaPlayer mMediaPlayer; |
| |
| private IntentFilter mAudioNoisyIntentFilter = |
| new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); |
| |
| private BroadcastReceiver mAudioNoisyReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { |
| LogHelper.d(TAG, "Headphones disconnected."); |
| if (isPlaying()) { |
| Intent i = new Intent(context, MusicService.class); |
| i.setAction(MusicService.ACTION_CMD); |
| i.putExtra(MusicService.CMD_NAME, MusicService.CMD_PAUSE); |
| mService.startService(i); |
| } |
| } |
| } |
| }; |
| |
| public Playback(MusicService service, MusicProvider musicProvider) { |
| this.mService = service; |
| this.mMusicProvider = musicProvider; |
| this.mAudioManager = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE); |
| // Create the Wifi lock (this does not acquire the lock, this just creates it) |
| this.mWifiLock = ((WifiManager) service.getSystemService(Context.WIFI_SERVICE)) |
| .createWifiLock(WifiManager.WIFI_MODE_FULL, "sample_lock"); |
| } |
| |
| public void start() { |
| } |
| |
| public void stop(boolean notifyListeners) { |
| mState = PlaybackState.STATE_STOPPED; |
| if (notifyListeners && mCallback != null) { |
| mCallback.onPlaybackStatusChanged(mState); |
| } |
| mCurrentPosition = getCurrentStreamPosition(); |
| // Give up Audio focus |
| giveUpAudioFocus(); |
| unregisterAudioNoisyReceiver(); |
| // Relax all resources |
| relaxResources(true); |
| if (mWifiLock.isHeld()) { |
| mWifiLock.release(); |
| } |
| } |
| |
| public void setState(int state) { |
| this.mState = state; |
| } |
| |
| public int getState() { |
| return mState; |
| } |
| |
| public boolean isConnected() { |
| return true; |
| } |
| |
| public boolean isPlaying() { |
| return mPlayOnFocusGain || (mMediaPlayer != null && mMediaPlayer.isPlaying()); |
| } |
| |
| public int getCurrentStreamPosition() { |
| return mMediaPlayer != null ? |
| mMediaPlayer.getCurrentPosition() : mCurrentPosition; |
| } |
| |
| public void play(QueueItem item) { |
| mPlayOnFocusGain = true; |
| tryToGetAudioFocus(); |
| registerAudioNoisyReceiver(); |
| String mediaId = item.getDescription().getMediaId(); |
| boolean mediaHasChanged = !TextUtils.equals(mediaId, mCurrentMediaId); |
| if (mediaHasChanged) { |
| mCurrentPosition = 0; |
| mCurrentMediaId = mediaId; |
| } |
| |
| if (mState == PlaybackState.STATE_PAUSED && !mediaHasChanged && mMediaPlayer != null) { |
| configMediaPlayerState(); |
| } else { |
| mState = PlaybackState.STATE_STOPPED; |
| relaxResources(false); // release everything except MediaPlayer |
| MediaMetadata track = mMusicProvider.getMusic( |
| MediaIDHelper.extractMusicIDFromMediaID(item.getDescription().getMediaId())); |
| |
| String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE); |
| |
| 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(); |
| |
| if (mCallback != null) { |
| mCallback.onPlaybackStatusChanged(mState); |
| } |
| |
| } catch (IOException ex) { |
| LogHelper.e(TAG, ex, "Exception playing song"); |
| if (mCallback != null) { |
| mCallback.onError(ex.getMessage()); |
| } |
| } |
| } |
| } |
| |
| public void pause() { |
| if (mState == PlaybackState.STATE_PLAYING) { |
| // Pause media player and cancel the 'foreground service' state. |
| if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { |
| mMediaPlayer.pause(); |
| mCurrentPosition = mMediaPlayer.getCurrentPosition(); |
| } |
| // while paused, retain the MediaPlayer but give up audio focus |
| relaxResources(false); |
| giveUpAudioFocus(); |
| } |
| mState = PlaybackState.STATE_PAUSED; |
| if (mCallback != null) { |
| mCallback.onPlaybackStatusChanged(mState); |
| } |
| unregisterAudioNoisyReceiver(); |
| } |
| |
| public void seekTo(int position) { |
| LogHelper.d(TAG, "seekTo called with ", position); |
| |
| if (mMediaPlayer == null) { |
| // If we do not have a current media player, simply update the current position |
| mCurrentPosition = position; |
| } else { |
| if (mMediaPlayer.isPlaying()) { |
| mState = PlaybackState.STATE_BUFFERING; |
| } |
| mMediaPlayer.seekTo(position); |
| if (mCallback != null) { |
| mCallback.onPlaybackStatusChanged(mState); |
| } |
| } |
| } |
| |
| public void setCallback(Callback callback) { |
| this.mCallback = callback; |
| } |
| |
| /** |
| * Try to get the system audio focus. |
| */ |
| private void tryToGetAudioFocus() { |
| LogHelper.d(TAG, "tryToGetAudioFocus"); |
| if (mAudioFocus != AUDIO_FOCUSED) { |
| int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, |
| AudioManager.AUDIOFOCUS_GAIN); |
| if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| mAudioFocus = AUDIO_FOCUSED; |
| } |
| } |
| } |
| |
| /** |
| * Give up the audio focus. |
| */ |
| private void giveUpAudioFocus() { |
| LogHelper.d(TAG, "giveUpAudioFocus"); |
| if (mAudioFocus == AUDIO_FOCUSED) { |
| if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK; |
| } |
| } |
| } |
| |
| /** |
| * 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, "configMediaPlayerState. mAudioFocus=", mAudioFocus); |
| if (mAudioFocus == AUDIO_NO_FOCUS_NO_DUCK) { |
| // If we don't have audio focus and can't duck, we have to pause, |
| if (mState == PlaybackState.STATE_PLAYING) { |
| pause(); |
| } |
| } else { // we have audio focus: |
| if (mAudioFocus == AUDIO_NO_FOCUS_CAN_DUCK) { |
| mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet |
| } else { |
| if (mMediaPlayer != null) { |
| mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again |
| } // else do something for remote client. |
| } |
| // If we were playing when we lost focus, we need to resume playing. |
| if (mPlayOnFocusGain) { |
| if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) { |
| LogHelper.d(TAG,"configMediaPlayerState startMediaPlayer. seeking to ", |
| mCurrentPosition); |
| if (mCurrentPosition == mMediaPlayer.getCurrentPosition()) { |
| mMediaPlayer.start(); |
| mState = PlaybackState.STATE_PLAYING; |
| } else { |
| mMediaPlayer.seekTo(mCurrentPosition); |
| mState = PlaybackState.STATE_BUFFERING; |
| } |
| } |
| mPlayOnFocusGain = false; |
| } |
| } |
| if (mCallback != null) { |
| mCallback.onPlaybackStatusChanged(mState); |
| } |
| } |
| |
| /** |
| * Called by AudioManager on audio focus changes. |
| * Implementation of {@link android.media.AudioManager.OnAudioFocusChangeListener} |
| */ |
| @Override |
| public void onAudioFocusChange(int focusChange) { |
| LogHelper.d(TAG, "onAudioFocusChange. focusChange=", focusChange); |
| if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { |
| // We have gained focus: |
| mAudioFocus = AUDIO_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 ? AUDIO_NO_FOCUS_CAN_DUCK : AUDIO_NO_FOCUS_NO_DUCK; |
| |
| // 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(); |
| } |
| |
| /** |
| * Called when MediaPlayer has completed a seek |
| * |
| * @see android.media.MediaPlayer.OnSeekCompleteListener |
| */ |
| @Override |
| public void onSeekComplete(MediaPlayer mp) { |
| LogHelper.d(TAG, "onSeekComplete from MediaPlayer:", mp.getCurrentPosition()); |
| mCurrentPosition = mp.getCurrentPosition(); |
| if (mState == PlaybackState.STATE_BUFFERING) { |
| mMediaPlayer.start(); |
| mState = PlaybackState.STATE_PLAYING; |
| } |
| if (mCallback != null) { |
| mCallback.onPlaybackStatusChanged(mState); |
| } |
| } |
| |
| /** |
| * 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 (mCallback != null) { |
| mCallback.onCompletion(); |
| } |
| } |
| |
| /** |
| * 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); |
| if (mCallback != null) { |
| mCallback.onError("MediaPlayer error " + what + " (" + extra + ")"); |
| } |
| return true; // true indicates we handled the error |
| } |
| |
| /** |
| * 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(mService.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); |
| mMediaPlayer.setOnSeekCompleteListener(this); |
| } else { |
| mMediaPlayer.reset(); |
| } |
| } |
| |
| /** |
| * 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); |
| |
| mService.stopForeground(true); |
| |
| // 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(); |
| } |
| } |
| |
| private void registerAudioNoisyReceiver() { |
| if (!mAudioNoisyReceiverRegistered) { |
| mService.registerReceiver(mAudioNoisyReceiver, mAudioNoisyIntentFilter); |
| mAudioNoisyReceiverRegistered = true; |
| } |
| } |
| |
| private void unregisterAudioNoisyReceiver() { |
| if (mAudioNoisyReceiverRegistered) { |
| mService.unregisterReceiver(mAudioNoisyReceiver); |
| mAudioNoisyReceiverRegistered = false; |
| } |
| } |
| |
| interface Callback { |
| /** |
| * On current music completed. |
| */ |
| void onCompletion(); |
| /** |
| * on Playback status changed |
| * Implementations can use this callback to update |
| * playback state on the media sessions. |
| */ |
| void onPlaybackStatusChanged(int state); |
| |
| /** |
| * @param error to be added to the PlaybackState |
| */ |
| void onError(String error); |
| |
| } |
| |
| } |