/**
* This work is licensed under the Creative Commons Attribution-NonCommercial-
* NoDerivs 3.0 Unported License. To view a copy of this license, visit
* http://creativecommons.org/licenses/by-nc-nd/3.0/ or send a letter to
* Creative Commons, 444 Castro Street, Suite 900, Mountain View, California,
* 94041, USA.
*
* Use of this work is permitted only in accordance with license rights granted.
* Materials provided "AS IS"; no representations or warranties provided.
*
* Copyright � 2012 Marcus Parkkinen, Aki K�kel�, Fredrik �hs.
**/
package edu.chalmers.dat255.audiobookplayer.ctrl;
import java.io.IOException;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.util.Log;
import edu.chalmers.dat255.audiobookplayer.constants.Constants;
import edu.chalmers.dat255.audiobookplayer.interfaces.IPlayerEvents;
import edu.chalmers.dat255.audiobookplayer.model.Bookshelf;
/**
* Manages playing audio files. When its functions are called it will mutate the
* bookshelf.
* <p>
* Handles timed updates of the model.
* <p>
* Wraps the android.media.MediaPlayer class.
*
* @author Aki K�kel�
* @version 0.6
*/
public class PlayerController implements IPlayerEvents, OnPreparedListener,
OnCompletionListener {
private static final String TAG = "PlayerController";
/**
* The audio player
*/
private MediaPlayer mp;
/**
* The model to mutate
*/
private Bookshelf bs;
/**
* An update thread which writes the elapsed time to the model
*/
private Thread trackTimeUpdateThread;
/**
* If audio is playing, this is true
*/
private boolean isStarted = false;
/**
* When audio starts, it will seek to this value. Needed both for safety and
* to prevent errors with MediaPlayer.
*/
private transient int seekPosition = 0;
private static final int UPDATE_FREQUENCY = Constants.Value.UPDATE_FREQUENCY;
private static final double ONE_TENTH = 0.1; // 10%
private static final double MAX_SEEK_PERCENTAGE = 1.0; // 100%
/**
* Creates a PlayerController instance and initializes the Media Player and
* Bookshelf.
*/
public PlayerController(Bookshelf bs) {
this.mp = new MediaPlayer();
this.bs = bs;
}
/**
* Stops the update timer (audio may still keep playing).
*/
public void stopTimer() {
if (trackTimeUpdateThread != null && trackTimeUpdateThread.isAlive()) {
this.trackTimeUpdateThread.interrupt();
}
}
/**
* Starts a new model update thread.
*/
public void startTimer() {
// stop the old timer
stopTimer();
// start a new timer
this.trackTimeUpdateThread = new Thread(new TrackElapsedTimeUpdater());
this.trackTimeUpdateThread.start();
}
/**
* Starts the audio player from the beginning. Starts updating the model.
* <p>
* Can be called in any state (stopped, paused, resumed, uninitialized).
* <p>
* To start from the a given time, see {@link PlayerController#startAt(int)}.
*
* @precondition The Bookshelf must have been initialized and the path at
* the selected track index can not be null. Otherwise nothing
* is done.
*/
public void start() {
setup();
}
/**
* Can only be called if the path is valid.
*
* @return True if setup was run without problems. False if nothing was done
* (no selected track or path is null).
*/
private boolean setup() {
isStarted = false;
seekPosition = 0;
// do nothing if no track is selected
if (bs.getSelectedTrackIndex() == Constants.Value.NO_TRACK_SELECTED) {
Log.i(TAG, "Stopping since track index is not selected.");
stop();
// no setup was done since no track was selected
return false;
}
// get the path
String path = null;
try {
path = bs.getSelectedTrackPath();
} catch (IllegalArgumentException e) {
// there was no track selected
}
if (path != null) {
/*
* Now we are ready to set the source and start the audio. The audio
* is not started yet.
*/
// stop any currently running timer
stopTimer();
/*
* Reset the media player and then prepare it, providing a file
* path.
*/
mp.reset();
// start listening for when MediaPlayer is prepared
mp.setOnPreparedListener(this);
// listen to track completion
mp.setOnCompletionListener(this);
// set the stream type before preparing
mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
// set the data source and start preparing
try {
mp.setDataSource(path);
mp.prepareAsync();
} catch (IllegalArgumentException e) {
Log.e(TAG, "Illegal argument");
} catch (SecurityException e) {
Log.e(TAG, "Security exception");
} catch (IllegalStateException e) {
Log.e(TAG, "Illegal state");
} catch (IOException e) {
Log.e(TAG, "IO Exception");
}
isStarted = true;
// mark that the setup went without problems
return true;
}
// path was null, so no setup was done
return false;
}
/**
* Stops the audio player. Stops updating the model.
* <p>
* Call when activities are paused or stopped to free resources.
*/
public void stop() {
tearDown();
}
/**
* Reverts what setup does.
*/
private void tearDown() {
seekPosition = 0;
isStarted = false;
stopTimer();
if (isPlaying()) {
mp.stop();
mp.reset();
}
}
/**
* Convenience method.
*
* @return
*/
private int getTrackDuration() {
// check that there is a selected track
if (bs.getSelectedTrackIndex() != Constants.Value.NO_TRACK_SELECTED) {
return bs.getSelectedTrackDuration();
}
// no track is selected, so the duration is 0ms.
return 0;
}
/* IPlayerEvents */
/*
* (non-Javadoc)
*
* @see edu.chalmers.dat255.audiobookplayer.interfaces.IPlayerEvents#pause()
*/
public void pause() {
if (isStarted && mp.isPlaying()) {
stopTimer();
mp.pause();
}
}
/*
* (non-Javadoc)
*
* @see
* edu.chalmers.dat255.audiobookplayer.interfaces.IPlayerEvents#resume()
*/
public void resume() {
if (isStarted && !mp.isPlaying()) {
startTimer();
mp.start();
}
}
/*
* (non-Javadoc)
*
* @see
* edu.chalmers.dat255.audiobookplayer.interfaces.IPlayerEvents#previousTrack
* ()
*/
public void previousTrack() {
if (isAllowedBookIndex()) {
int trackIndex = bs.getSelectedTrackIndex();
/*
* previousTrack will always modify the selected track index of the
* model. If no track is selected, the first track will be selected.
* If the last track is selected, no track will be selected (it will
* be stopped).
*/
if (trackIndex == Constants.Value.NO_TRACK_SELECTED) {
// no track is selected, so start the last one
// verify that it is legal (there are tracks)
int lastIndex = bs.getNumberOfTracks() - 1;
if (bs.isLegalTrackIndex(lastIndex)) {
bs.setSelectedTrackIndex(lastIndex);
}
} else if (trackIndex == 0) {
// first track is selected, so start the first one
// verify that there are tracks first
if (bs.getNumberOfTracks() > 0) {
bs.setSelectedTrackIndex(0);
}
} else {
/*
* a track between the second first (index 1) and the last is
* selected, so just select the previous track since it will be
* legal.
*/
bs.setSelectedTrackIndex(trackIndex - 1);
}
}
}
/*
* (non-Javadoc)
*
* @see
* edu.chalmers.dat255.audiobookplayer.interfaces.IPlayerEvents#nextTrack()
*/
public void nextTrack() {
if (isAllowedBookIndex()) {
int trackIndex = bs.getSelectedTrackIndex();
/*
* nextTrack will always modify the selected track index of the
* model. If no track is selected, the first track will be selected.
* If the last track is selected, no track will be selected (it will
* be stopped).
*/
if (trackIndex == Constants.Value.NO_TRACK_SELECTED) {
// no track is selected, so start the first one
if (bs.isLegalTrackIndex(0)) {
bs.setSelectedTrackIndex(0);
}
} else if (trackIndex == bs.getNumberOfTracks() - 1) {
// last track is selected
bs.setSelectedTrackIndex(Constants.Value.NO_TRACK_SELECTED);
} else {
/*
* a track between the first and the second last is selected, so
* just select the next track since it will be legal.
*/
bs.setSelectedTrackIndex(trackIndex + 1);
}
}
}
/*
* (non-Javadoc)
*
* @see
* edu.chalmers.dat255.audiobookplayer.interfaces.IPlayerEvents#seekRight
* (boolean)
*/
public void seekRight(boolean seek) {
/*
* Note: in the future, 'seek' would have been used to determine a
* stopped/started state to end/start seeking.
*/
if (isAllowedTrackIndex() && getTrackDuration() != 0) {
seekTo((int) (ONE_TENTH * getTrackDuration() + mp
.getCurrentPosition()));
}
}
/*
* (non-Javadoc)
*
* @see
* edu.chalmers.dat255.audiobookplayer.interfaces.IPlayerEvents#seekLeft
* (boolean)
*/
public void seekLeft(boolean seek) {
/*
* Note: in the future, 'seek' would have been used to determine a
* stopped/started state to end/start seeking.
*/
if (isAllowedTrackIndex() && getTrackDuration() != 0) {
seekTo((int) (mp.getCurrentPosition() - ONE_TENTH
* getTrackDuration()));
}
}
/*
* (non-Javadoc)
*
* @see edu.chalmers.dat255.audiobookplayer.interfaces.IPlayerEvents#
* seekToPercentageInTrack(double)
*/
public void seekToPercentageInTrack(double percentage) {
if (!isLegalPercentage(percentage)) {
// simply play the next track if this happens.
nextTrack();
} else if (isAllowedBookIndex()) {
if (bs.getSelectedTrackIndex() == Constants.Value.NO_TRACK_SELECTED) {
// set the selected track index to the first one
bs.setSelectedTrackIndex(0);
} else {
// seek to the new time
seekTo((int) (mp.getDuration() * percentage));
}
}
}
/*
* (non-Javadoc)
*
* @see edu.chalmers.dat255.audiobookplayer.interfaces.IPlayerEvents#
* seekToPercentageInBook(double)
*/
public void seekToPercentageInBook(double percentage) {
if (!isLegalPercentage(percentage)) {
Log.e(TAG,
"Seeked to an illegal book state (negative or above 100%). "
+ "Player stopping.");
// stop if this happens.
stop();
} else if (isAllowedBookIndex()) {
// get the duration of the book
int bookDuration = bs.getSelectedBookDuration();
// calculate the seek time (ms)
seekPosition = (int) (bookDuration * percentage);
// go through the tracks and calculate the remainder and track index
int track = 0;
int trackDuration = 0;
int selectedBook = bs.getSelectedBookIndex();
while (seekPosition > (trackDuration = bs.getTrackDurationAt(
selectedBook, track))) {
seekPosition -= trackDuration;
track++;
}
// change to the correct track
bs.setSelectedTrackIndex(track);
// setting the track index already starts the player
}
}
/*
* (non-Javadoc)
*
* @see
* edu.chalmers.dat255.audiobookplayer.interfaces.IPlayerEvents#isStarted()
*/
public boolean isStarted() {
return isStarted;
}
/*
* (non-Javadoc)
*
* @see
* edu.chalmers.dat255.audiobookplayer.interfaces.IPlayerEvents#isPlaying()
*/
public boolean isPlaying() {
return mp.isPlaying();
}
/* End IPlayerEvents */
/**
* Checks whether the given percentage is legal (0-100).
*
* @param percentage
* @return True if legal.
*/
private boolean isLegalPercentage(double percentage) {
return percentage >= 0 && percentage <= MAX_SEEK_PERCENTAGE;
}
/**
* Seeks to the given time.
*
* @param time
* ms
*/
public void seekTo(int time) {
if (time > mp.getDuration() || time < 0) {
Log.e(TAG, "Attempted to seek to an invalid position: " + time
+ ". Fixing by seeking to the end or beginning.");
if (time > 0) {
// just play the next track directly
nextTrack();
} else {
// reset the current track
mp.seekTo(0);
}
} else {
// seek to the given, valid time
mp.seekTo(time);
}
}
/**
* Convenience method.
*
* @return
*/
private boolean isAllowedTrackIndex() {
return isAllowedBookIndex()
&& bs.isLegalTrackIndex(bs.getSelectedTrackIndex());
}
/**
* Convenience method.
*
* @return
*/
private boolean isAllowedBookIndex() {
return bs.isLegalBookIndex(bs.getSelectedBookIndex());
}
/**
* Thread that updates the elapsed time in a track.
*
* @author Aki K�kel�
* @version 0.2
*
*/
private class TrackElapsedTimeUpdater implements Runnable {
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
public void run() {
while (isStarted && mp.isPlaying()) {
updateTrackTime();
try {
Thread.sleep(UPDATE_FREQUENCY);
} catch (InterruptedException e) {
// the thread was interrupted, so simply end the run method.
return;
}
}
}
}
/**
* Convenience method.
*/
private void updateTrackTime() {
// check that there is a selected track
if (isAllowedTrackIndex()) {
this.bs.setSelectedTrackElapsedTime(mp.getCurrentPosition());
}
}
/*
* (non-Javadoc)
*
* @see
* android.media.MediaPlayer.OnPreparedListener#onPrepared(android.media
* .MediaPlayer)
*/
public void onPrepared(MediaPlayer mp) {
// seek to starting position
if (seekPosition != 0) {
seekTo(seekPosition);
}
// start the media player
mp.start();
// start a new timer
startTimer();
}
/*
* (non-Javadoc)
*
* @see
* android.media.MediaPlayer.OnCompletionListener#onCompletion(android.media
* .MediaPlayer)
*/
public void onCompletion(MediaPlayer mp) {
Log.i(TAG, "onComplete: Track finished. Starting next track.");
nextTrack();
}
/*
* *** FOR TESTING PURPOSES ONLY ***
*/
/**
* *** FOR TESTING PURPOSES ONLY ***
*
* Returns the media player instance for testing.
*
*/
public MediaPlayer getMp() {
return mp;
}
/**
* *** FOR TESTING PURPOSES ONLY ***
*
* Returns the bookshelf for testing.
*
*/
public Bookshelf getBs() {
return bs;
}
/**
* *** FOR TESTING PURPOSES ONLY ***
*
* Returns the thread for testing.
*
*/
public Thread getTrackTimeUpdateThread() {
return trackTimeUpdateThread;
}
/**
* Set to 0 to always start playing from the beginning.
*
* Set to other values (0 to the duration of the track) to seek when
* starting.
*
* @param pos
* Seek position in milliseconds to start at.
*/
public void setStartPosition(int pos) {
this.seekPosition = pos;
}
/*
* *** END TESTING PURPOSES ONLY ***
*/
}