blob: 6af2d412237ddbd576c7068999a1a6fe3764d7c1 [file] [log] [blame]
/*
* 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.media.AudioManager;
import android.media.MediaPlayer;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import android.util.Log;
import com.example.android.mediabrowserservice.model.MusicProvider;
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;
/**
* 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 = Playback.class.getSimpleName();
/* package */ 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);
}
// 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 MusicProvider mMusicProvider;
private final WifiManager.WifiLock mWifiLock;
private int mState = PlaybackStateCompat.STATE_NONE;
private boolean mPlayOnFocusGain;
private Callback mCallback;
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;
public Playback(MusicService service, MusicProvider musicProvider) {
Context context = service.getApplicationContext();
this.mService = service;
this.mMusicProvider = musicProvider;
this.mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
// Create the Wifi lock (this does not acquire the lock, this just creates it).
this.mWifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "sample_lock");
}
public void stop() {
mState = PlaybackStateCompat.STATE_STOPPED;
if (mCallback != null) {
mCallback.onPlaybackStatusChanged(mState);
}
mCurrentPosition = getCurrentStreamPosition();
// Give up Audio focus
giveUpAudioFocus();
// Relax all resources
relaxResources(true);
if (mWifiLock.isHeld()) {
mWifiLock.release();
}
}
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(MediaSessionCompat.QueueItem item) {
mPlayOnFocusGain = true;
tryToGetAudioFocus();
String mediaId = item.getDescription().getMediaId();
boolean mediaHasChanged = !TextUtils.equals(mediaId, mCurrentMediaId);
if (mediaHasChanged) {
mCurrentPosition = 0;
mCurrentMediaId = mediaId;
}
if (mState == PlaybackStateCompat.STATE_PAUSED
&& !mediaHasChanged && mMediaPlayer != null) {
configMediaPlayerState();
} else {
mState = PlaybackStateCompat.STATE_STOPPED;
relaxResources(false); // release everything except MediaPlayer
MediaMetadataCompat track = mMusicProvider.getMusic(item.getDescription().getMediaId());
String source = track.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI);
try {
createMediaPlayerIfNeeded();
mState = PlaybackStateCompat.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 ioException) {
Log.e(TAG, "Exception playing song", ioException);
if (mCallback != null) {
mCallback.onError(ioException.getMessage());
}
}
}
}
public void pause() {
if (mState == PlaybackStateCompat.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);
}
mState = PlaybackStateCompat.STATE_PAUSED;
if (mCallback != null) {
mCallback.onPlaybackStatusChanged(mState);
}
}
public void seekTo(int position) {
Log.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 = PlaybackStateCompat.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() {
Log.d(TAG, "tryToGetAudioFocus");
int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
mAudioFocus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
? AUDIO_FOCUSED : AUDIO_NO_FOCUS_NO_DUCK;
}
/**
* Give up the audio focus.
*/
private void giveUpAudioFocus() {
Log.d(TAG, "giveUpAudioFocus");
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() {
Log.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 == PlaybackStateCompat.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()) {
Log.d(TAG, "configMediaPlayerState startMediaPlayer. seeking to "
+ mCurrentPosition);
if (mCurrentPosition == mMediaPlayer.getCurrentPosition()) {
mMediaPlayer.start();
mState = PlaybackStateCompat.STATE_PLAYING;
} else {
mMediaPlayer.seekTo(mCurrentPosition);
mState = PlaybackStateCompat.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) {
Log.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 == PlaybackStateCompat.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 {
Log.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 player) {
Log.d(TAG, "onSeekComplete from MediaPlayer:" + player.getCurrentPosition());
mCurrentPosition = player.getCurrentPosition();
if (mState == PlaybackStateCompat.STATE_BUFFERING) {
mMediaPlayer.start();
mState = PlaybackStateCompat.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) {
Log.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) {
Log.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 player, int what, int extra) {
Log.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() {
Log.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) {
Log.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();
}
}
}