/*******************************************************************************
* Copyright 2012 Crazywater
*
* 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 de.knufficast.player;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.media.AudioManager;
import android.os.IBinder;
import de.knufficast.events.EpisodeDownloadStateEvent;
import de.knufficast.events.EventBus;
import de.knufficast.events.Listener;
import de.knufficast.events.PlayerErrorEvent;
import de.knufficast.events.PlayerProgressEvent;
import de.knufficast.events.PlayerStateChangeEvent;
import de.knufficast.events.QueueChangedEvent;
import de.knufficast.logic.db.DBEpisode;
import de.knufficast.logic.db.DBEpisode.DownloadState;
import de.knufficast.logic.db.Queue;
import de.knufficast.player.PlayerService.PlayerBinder;
import de.knufficast.util.Callback;
import de.knufficast.util.Function;
import de.knufficast.util.PollingThread;
/**
* A wrapper around {@link PlayerService} that manages always playing what is on
* top of the queue and removing the top upon completion.
*
* @author crazywater
*
*/
public class QueuePlayer {
private static final long UI_UPDATE_INTERVAL = 1000; // ms
public static final int FORWARD_MS = 30 * 1000;
public static final int REWIND_MS = 30 * 1000;
private final Queue queue;
private final EventBus eventBus;
private final Context context;
private PlayerService player;
private NotificationAreaController notificationArea;
private boolean shouldPlay;
private AudioManager audioManager;
private final RemoteController remoteController = new RemoteController();
private final ServiceConnection playerConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder binder) {
player = ((PlayerBinder) binder).getService();
notificationArea = new NotificationAreaController(player);
player.setOnCompletionCallback(onCompletionCallback);
player.setOnPreparedCallback(onPreparedCallback);
prepareAsync();
}
@Override
public void onServiceDisconnected(ComponentName name) {
player = null;
}
};
private final Function<Void, Integer> getProgress = new Function<Void, Integer>() {
@Override
public Integer call(Void a) {
return player.getCurrentPosition();
}
};
private final Callback<Integer> progressListener = new Callback<Integer>() {
@Override
public void call(Integer progress) {
eventBus.fireEvent(new PlayerProgressEvent(queue.peek(), progress, player
.getDuration()));
}
};
private final Callback<Void> onCompletionCallback = new Callback<Void>() {
@Override
public void call(Void unused) {
eventBus.fireEvent(new PlayerStateChangeEvent(false));
queue.pop();
}
};
private final Callback<Void> onPreparedCallback = new Callback<Void>() {
@Override
public void call(Void unused) {
int progress = player.getCurrentPosition();
int total = player.getDuration();
eventBus
.fireEvent(new PlayerProgressEvent(queue.peek(), progress, total));
if (shouldPlay) {
play();
}
}
};
private final Listener<QueueChangedEvent> topChangedListener = new Listener<QueueChangedEvent>() {
@Override
public void onEvent(QueueChangedEvent event) {
if (event.topOfQueueChanged()) {
topOfQueueChanged();
}
}
};
private final Listener<EpisodeDownloadStateEvent> topDownloadListener = new Listener<EpisodeDownloadStateEvent>() {
@Override
public void onEvent(EpisodeDownloadStateEvent event) {
if (!queue.isEmpty() && event.getIdentifier() == queue.peek().getId()) {
topOfQueueChanged();
}
}
};
private final Listener<PlayerErrorEvent> errorListener = new Listener<PlayerErrorEvent>() {
@Override
public void onEvent(PlayerErrorEvent event) {
pause();
}
};
private final AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
remoteController.release();
pause();
}
}
};
private final BroadcastReceiver audioUnpluggedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
pause();
}
}
};
private static final IntentFilter audioUnpluggedIntent = new IntentFilter(
AudioManager.ACTION_AUDIO_BECOMING_NOISY);
private PollingThread<Integer> progressReporter;
public QueuePlayer(Queue queue, Context context, EventBus eventBus) {
this.queue = queue;
this.eventBus = eventBus;
this.context = context;
eventBus.addListener(QueueChangedEvent.class, topChangedListener);
eventBus.addListener(EpisodeDownloadStateEvent.class, topDownloadListener);
eventBus.addListener(PlayerErrorEvent.class, errorListener);
}
private void topOfQueueChanged() {
if (player == null) {
return;
}
boolean tmp = shouldPlay;
if (shouldPlay) {
pause();
}
shouldPlay = tmp;
prepareAsync();
}
/**
* Prepare the queuePlayer to play whatever is on top of the queue. Fires a
* {@link PlayerProgressEvent} as soon as the length of the current episode is
* known. May fire a {@link PlayerErrorEvent}. If the player is already
* playing, apart from the events, nothing happens.
*/
public void prepareAsync() {
if (player == null) {
context.bindService(new Intent(context, PlayerService.class),
playerConnection, Context.BIND_AUTO_CREATE);
// prepareAsync is called again once the player is not null anymore
} else {
int progress = 0;
int total = 0;
if (!queue.isEmpty()) {
DBEpisode next = queue.peek();
if (next.getDownloadState() == DownloadState.FINISHED) {
player.setEpisode(next);
} else {
player.setEpisode(null);
stopped();
}
if (player.isPrepared()) {
progress = player.getCurrentPosition();
total = player.getDuration();
next.setDuration(total);
}
} else {
stopped();
}
eventBus
.fireEvent(new PlayerProgressEvent(queue.peek(), progress, total));
}
if (audioManager == null) {
audioManager = (AudioManager) context
.getSystemService(Context.AUDIO_SERVICE);
}
}
private void stopped() {
shouldPlay = false;
remoteController.stop();
remoteController.release();
audioManager.abandonAudioFocus(onAudioFocusChangeListener);
}
/**
* Pauses if playing, starts playing if paused. This method should never be
* called on an unprepared player.
*/
public void togglePlaying() {
if (!player.isPlaying()) {
play();
} else {
pause();
}
}
public void play() {
if (player == null) {
return;
}
shouldPlay = true;
remoteController.register(context, audioManager);
if (!queue.isEmpty() && player.hasEpisode()) {
int audioFocus = audioManager.requestAudioFocus(
onAudioFocusChangeListener, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
if (audioFocus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
if (progressListener != null) {
startProgressReporter();
}
player.play();
context.registerReceiver(audioUnpluggedReceiver, audioUnpluggedIntent);
notificationArea.register(queue.peek());
remoteController.updateMetadata(queue.peek(), player.getDuration());
remoteController.updateState(true);
eventBus.fireEvent(new PlayerStateChangeEvent(true));
}
} else {
stopped();
}
}
public void pause() {
if (player == null) {
return;
}
shouldPlay = false;
if (progressListener != null) {
stopProgressReporter();
}
try {
context.unregisterReceiver(audioUnpluggedReceiver);
} catch (IllegalArgumentException e) {
// was already unregistered
}
player.pause();
notificationArea.unregister();
remoteController.updateState(false);
eventBus.fireEvent(new PlayerStateChangeEvent(false));
}
private void startProgressReporter() {
progressReporter = new PollingThread<Integer>(progressListener,
getProgress, UI_UPDATE_INTERVAL);
progressReporter.start();
}
private void stopProgressReporter() {
if (progressReporter != null) {
progressReporter.interrupt();
progressReporter = null;
}
}
public void seekTo(int msec) {
if (player != null) {
player.seekTo(msec);
}
}
/**
* Stops the player service and relinquishes all its resources, if it isn't
* playing and still needs them.
*/
public void releaseIfNotPlaying() {
if (player != null && !isPlaying()) {
player.stopSelf();
}
}
public boolean isPlaying() {
if (player != null) {
return player.isPlaying();
}
return false;
}
public void rewind() {
player.seekTo(player.getCurrentPosition() - REWIND_MS);
}
public void fastForward() {
player.seekTo(player.getCurrentPosition() + FORWARD_MS);
}
}