/*
* 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);
}
}