/* * Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner, * Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain, * Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter, * Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann, * Samuel Zweifel * * This file is part of Jukefox. * * Jukefox is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or any later version. Jukefox is * distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * Jukefox. If not, see <http://www.gnu.org/licenses/>. */ package ch.ethz.dcg.jukefox.controller.player.playbackcontroller; import java.io.File; import java.io.IOException; import java.util.Iterator; import java.util.List; import ch.ethz.dcg.jukefox.commons.Constants; import ch.ethz.dcg.jukefox.commons.DataUnavailableException; import ch.ethz.dcg.jukefox.commons.utils.JoinableThread; import ch.ethz.dcg.jukefox.commons.utils.Log; import ch.ethz.dcg.jukefox.controller.player.IPlaybackInfoBroadcaster; import ch.ethz.dcg.jukefox.controller.player.mediaplayer.IMediaPlayerWrapper; import ch.ethz.dcg.jukefox.controller.player.mediaplayer.InvalidPathException; import ch.ethz.dcg.jukefox.controller.player.mediaplayer.OnMediaPlayerEventListener; import ch.ethz.dcg.jukefox.controller.player.playlistmanager.IPlaylistManager; import ch.ethz.dcg.jukefox.model.AbstractCollectionModelManager; import ch.ethz.dcg.jukefox.model.AbstractPlayerModelManager; import ch.ethz.dcg.jukefox.model.collection.BaseAlbum; import ch.ethz.dcg.jukefox.model.collection.BaseArtist; import ch.ethz.dcg.jukefox.model.collection.IPlaylist; import ch.ethz.dcg.jukefox.model.collection.PlaylistSong; import ch.ethz.dcg.jukefox.model.commons.EmptyPlaylistException; import ch.ethz.dcg.jukefox.model.commons.NoNextSongException; import ch.ethz.dcg.jukefox.model.commons.PlaylistPositionOutOfRangeException; import ch.ethz.dcg.jukefox.model.player.PlayModeType; import ch.ethz.dcg.jukefox.model.player.PlayerAction; import ch.ethz.dcg.jukefox.model.player.PlayerState; import ch.ethz.dcg.jukefox.playmode.IPlayMode; import ch.ethz.dcg.jukefox.playmode.PlayerControllerCommand; import ch.ethz.dcg.jukefox.playmode.PlayerControllerCommands; import entagged.audioformats.AudioFile; import entagged.audioformats.AudioFileIO; import entagged.audioformats.Tag; import entagged.audioformats.generic.TagField; import entagged.audioformats.mp3.util.id3frames.TextId3Frame; public class BasePlaybackController implements IPlaybackController { private static final String TAG = BasePlaybackController.class.getSimpleName(); protected IMediaPlayerWrapper mediaPlayer; protected PlayerState state = PlayerState.STOP; protected int lastLoadedSongId; protected String lastSongPath; protected PlaylistSong<BaseArtist, BaseAlbum> lastSong; protected final IPlaybackInfoBroadcaster listenerInformer; protected long lastOnErrorTime = 0; protected final IPlaylistManager currentPlaylistManager; protected AbstractCollectionModelManager collectionModel; protected AbstractPlayerModelManager playerModel; public BasePlaybackController(IPlaybackInfoBroadcaster listenerInformer, AbstractCollectionModelManager collectionModel, AbstractPlayerModelManager playerModel, IPlaylistManager currentPlaylistManager, IMediaPlayerWrapper mediaPlayer) { this.currentPlaylistManager = currentPlaylistManager; this.listenerInformer = listenerInformer; this.collectionModel = collectionModel; this.playerModel = playerModel; OnMediaPlayerEventListener mediaPlayerEventListener = new OnMediaPlayerEventListener() { @Override public boolean onError(IMediaPlayerWrapper mp, int what, int extra) { return BasePlaybackController.this.onError(mp, what, extra); } @Override public boolean onInfo(IMediaPlayerWrapper mp, int what, int extra) { return BasePlaybackController.this.onInfo(mp, what, extra); } @Override public void onSongCompleted(IMediaPlayerWrapper mediaPlayer) { BasePlaybackController.this.onSongCompleted(mediaPlayer.getCurrentSong()); } }; this.mediaPlayer = mediaPlayer; this.mediaPlayer.setOnMediaPlayerEventListener(mediaPlayerEventListener); } @Override public int getDuration() { return getDuration(mediaPlayer); } @Override public void pause() { pause(mediaPlayer); } @Override public void play() { play(mediaPlayer); } @Override public void stop() { stop(mediaPlayer); } @Override public void seekTo(int position) { seekTo(mediaPlayer, position); } protected void seekTo(IMediaPlayerWrapper mp, int position) { mp.seekTo(position); } @Override public PlayerState getState() { return state; } protected void setPlayerState(final PlayerState newState) { if (newState != state) { state = newState; JoinableThread t = new JoinableThread(new Runnable() { @Override public void run() { listenerInformer.informPlayerStateChangedListeners(newState); } }); t.start(); // if (newState == PlayerState.PLAY) { // Log.v(TAG, "disable standard lock screen..."); // Log.v(TAG, "standard lock screen disabled."); // } else { // try { // JoinableThread.sleep(100); // } catch (InterruptedException e) { // Log.w(TAG, e); // } // Log.v(TAG, "enable standard lock screen..."); // JukefoxApplication.enableLockScreen(); // Log.v(TAG, "standard lock screen enabled."); // } } } @Override public void onDestroy() { if (mediaPlayer != null) { stop(); mediaPlayer.onDestroy(); } } public void onSongCompleted(PlaylistSong<BaseArtist, BaseAlbum> song) { listenerInformer.informSongCompletedListeners(song); Log.v(TAG, "onSongCompleted()"); PlayerControllerCommands commands; try { commands = currentPlaylistManager.getPlayMode().next(currentPlaylistManager.getCurrentPlaylist()); applyPlayerControlCommands(commands); } catch (NoNextSongException e) { Log.w(TAG, e); // TODO: maybe do something meaningful if it does not work } } protected boolean loadSongIntoPlayer(PlaylistSong<BaseArtist, BaseAlbum> song, String path, IMediaPlayerWrapper currentMP) { if (song == null || path == null) { return false; } if (lastSongPath != null && lastSong != null && lastSongPath.equals(path) && lastSong.getId() == song.getId()) { if (currentMP.isSongReadyToPlay() && currentMP.getCurrentSong().getId() == song.getId()) { Log.v(TAG, "Ignoring loading command because the song is already loaded in the player and prepared."); return true; } } lastSongPath = path; lastSong = song; // readReplayGain(path); try { resetAndSetSourceAndPrepare(song, path, currentMP); lastLoadedSongId = song.getId(); // doPlayPause(currentMP); return true; } catch (Exception e) { setPlayerState(PlayerState.ERROR); Log.w(TAG, "ERROR while trying to play " + path); Log.w(TAG, e); return false; } } /** * returns the song duration in milliseconds or -1 if it is not able to read the duration * * @param path * @return */ @SuppressWarnings({ "unused", "unchecked" }) private void readReplayGain(String path) { try { File f = new File(path); AudioFile af = AudioFileIO.read(f); Tag tag = af.getTag(); Iterator<TagField> it = tag.getFields(); while (it.hasNext()) { TagField field = it.next(); TextId3Frame frame = (TextId3Frame) field; Log.v(TAG, frame.getContent()); } if (tag.hasField("TXXX")) { // Replay gain tag in MP3s try { String content = readFieldContent(tag, "TXXX"); Log.v(TAG, "Replay Gain: " + content); } catch (Exception e) { Log.w(TAG, e); } } else if (tag.hasField("APETAGEX")) { try { String content = readFieldContent(tag, "APETAGEX"); Log.v(TAG, "Replay Gain: " + content); } catch (Exception e) { Log.w(TAG, e); } } else { Log.v(TAG, "File has no replay gain tag"); } } catch (Exception e) { Log.w(TAG, e); return; } } @SuppressWarnings("unchecked") private String readFieldContent(Tag tag, String id) { try { List<TagField> fields = tag.get(id); if (fields == null || fields.size() == 0) { return ""; } try { TextId3Frame frame = (TextId3Frame) fields.get(0); return frame.getContent(); } catch (Exception e) { Log.w(TAG, e); } // TODO: try to read GenericId3Frame? => how to get encoding?? } catch (Exception e) { Log.w(TAG, e); } return ""; } protected void resetAndSetSourceAndPrepare(PlaylistSong<BaseArtist, BaseAlbum> song, String path, IMediaPlayerWrapper mp) throws IllegalStateException, IOException, InvalidPathException { if (!validatePath(path)) { throw new InvalidPathException(); } mp.setSong(song, path); } protected synchronized void play(final IMediaPlayerWrapper mp) { PlaylistSong<BaseArtist, BaseAlbum> song = null; try { song = currentPlaylistManager.getCurrentSong(); } catch (EmptyPlaylistException e) { // if we start the player for the first time } if (song == null) { next(); } else { if (!mp.isSongReadyToPlay()) { try { loadSongIntoPlayer(song, collectionModel.getOtherDataProvider().getSongPath(song), mp); } catch (DataUnavailableException e) { Log.w(TAG, e); return; } if (currentPlaylistManager.getCurrentPlaylist().hasExtras()) { seekTo(currentPlaylistManager.getCurrentPlaylist().getPositionInSong()); } } try { try { // Log.v(TAG, "play (mp: " + mp + "), callstack:"); // Log.printStackTrace(TAG, // Thread.currentThread().getStackTrace()); } catch (Throwable t) { Log.w(TAG, t); } mp.play(); if (mp.isPlaying()) { setPlayerState(PlayerState.PLAY); listenerInformer.informSongStartedListeners(mp.getCurrentSong()); } } catch (Exception e) { Log.w(TAG, e); setPlayerState(PlayerState.ERROR); } } } protected synchronized void pause(final IMediaPlayerWrapper mp) { try { mp.pause(); Log.v(TAG, "pause."); setPlayerState(PlayerState.PAUSE); } catch (Exception e) { Log.w(TAG, e); setPlayerState(PlayerState.ERROR); } } protected synchronized void stop(final IMediaPlayerWrapper mp) { try { mp.stop(); mp.reset(); setPlayerState(PlayerState.STOP); } catch (Exception e) { Log.w(TAG, e); setPlayerState(PlayerState.ERROR); } } protected synchronized int getDuration(IMediaPlayerWrapper mp) { if (getState() == PlayerState.STOP || getState() == PlayerState.ERROR) { return 0; } try { int duration = mp.getDuration(); if (duration < 0) { duration = 0; } return duration; } catch (Exception e) { Log.w(TAG, e); return 0; } } protected synchronized int getCurrentPosition(IMediaPlayerWrapper mp) { return mp.getCurrentPosition(); } /** * * @param path * path of file to check * @return returns true if path is a valid and readable file system path */ protected boolean validatePath(String path) { if (path == null) { return false; } try { File test = new File(path); if (!test.canRead()) { return false; } } catch (Exception e) { Log.w(TAG, e); return false; } return true; } @Override public void reloadSettings() { } @Override public void mute() { mute(mediaPlayer); } @Override public void unmute() { unmute(mediaPlayer); } protected void mute(IMediaPlayerWrapper mp) { mp.setVolume(0f, 0f); } protected void unmute(IMediaPlayerWrapper mp) { mp.setVolume(0.9f, 0.9f); } public boolean onError(IMediaPlayerWrapper mp, int what, int extra) { Log.e(TAG, "onError() What: " + what + ", Extra: " + extra + ", playerState: " + state); try { if (System.currentTimeMillis() - lastOnErrorTime < 2000) { return true; // avoid endless loops due to erroneous player // handling } PlayerState origPlayerState = state; // TODO: should that be "what" or "extra"?? (see // http://developer.android.com/reference/android/media/MediaPlayer.OnErrorListener.html // and // http://android.git.kernel.org/?p=platform/external/opencore.git;a=blob;f=pvmi/pvmf/include/pvmf_return_codes.h;h=ed5a2539ca85ae60425229be41646b6bd7d9389c;hb=HEAD if (what == -38) { stop(mp); loadSongIntoPlayer(lastSong, lastSongPath, mp); if (origPlayerState == PlayerState.PLAY) { play(mp); } } return true; } finally { lastOnErrorTime = System.currentTimeMillis(); } } public boolean onInfo(IMediaPlayerWrapper mp, int what, int extra) { // kuhnmi, 6.8.2011: changed output from onError to onInfo Log.i(TAG, "onInfo() What: " + what + ", Extra: " + extra); return true; } @Override public IPlaylistManager getCurrentPlaylistManager() { return currentPlaylistManager; } @Override public int getPlaybackPosition() { return getCurrentPosition(mediaPlayer); } @Override public PlayerState getPlayerState() { return state; } @Override public void next() { try { PlaylistSong<BaseArtist, BaseAlbum> song = currentPlaylistManager.getCurrentSong(); if (song != null) { listenerInformer.informSongSkippedListeners(song); } } catch (EmptyPlaylistException e1) { // Log.w(TAG, e1); // We do not need to log this exception as this is can happen when // next is called in a n empty playlist } PlayerControllerCommands commands; try { commands = currentPlaylistManager.getPlayMode().next(currentPlaylistManager.getCurrentPlaylist()); applyPlayerControlCommands(commands); // for (PlaylistSong<BaseArtist, BaseAlbum> song : // currentPlaylistManager.getCurrentPlaylist().getSongList()) { // Log.v(TAG, song.toString()); // } } catch (NoNextSongException e) { Log.w(TAG, e); // TODO: maybe do something meaningful if it does not work } } @Override public void previous() { PlayerControllerCommands commands; try { commands = currentPlaylistManager.getPlayMode().previous(currentPlaylistManager.getCurrentPlaylist()); applyPlayerControlCommands(commands); } catch (NoNextSongException e) { Log.w(TAG, e); // TODO: maybe do something meaningful if it does not work } } protected void applyPlayerControlCommands(PlayerControllerCommands commands) { for (PlayerControllerCommand command : commands.getAllCommands()) { // Log.v(TAG, command.toString()); switch (command.getType()) { case ADD_SONG: try { currentPlaylistManager.insertSongAtPosition(command.getSong(), command.getPosition()); } catch (PlaylistPositionOutOfRangeException e) { Log.w(TAG, e); } break; case REMOVE_SONG: try { currentPlaylistManager.removeSongFromPlaylist(command.getPosition()); } catch (EmptyPlaylistException e) { Log.w(TAG, e); } catch (PlaylistPositionOutOfRangeException e) { Log.w(TAG, e); } break; case PLAYER_ACTION: if (command.getPlayerAction() == PlayerAction.PLAY) { play(); } else if (command.getPlayerAction() == PlayerAction.PAUSE) { pause(); } else if (command.getPlayerAction() == PlayerAction.STOP) { stop(); } break; case SET_POS_IN_LIST: jumpToPlaylistPosition(command.getPosition()); break; case SET_POS_IN_SONG: seekTo(command.getPosition()); break; case SET_PLAY_MODE: setPlayMode(command.getPlayMode(), 0, Constants.SAME_SONG_AVOIDANCE_NUM); // TODO: use real parameters break; } } } /** * Depending on the given play mode we create a different playlist controller core. The playlist controller core is * returned, so that the invoker obtains a reference. One can safely cast such a reference, because the given play * mode type clearly identifies which controller will be created. */ @Override public IPlayMode setPlayMode(PlayModeType playModeType, int artistAvoidance, int songAvoidance) { currentPlaylistManager.setPlayMode(playModeType, artistAvoidance, songAvoidance); PlayerControllerCommands commands = currentPlaylistManager.getPlayMode().initialize( currentPlaylistManager.getCurrentPlaylist()); applyPlayerControlCommands(commands); listenerInformer.informPlayModeChangeListener(currentPlaylistManager.getPlayMode()); return currentPlaylistManager.getPlayMode(); } @Override public boolean jumpToPlaylistPosition(int position) { try { currentPlaylistManager.setCurrentSongIndex(position); PlaylistSong<BaseArtist, BaseAlbum> song = currentPlaylistManager.getCurrentSong(); String path; try { path = collectionModel.getOtherDataProvider().getSongPath(song); Log.v(TAG, "loadSong() " + song.getArtist().getName() + " - " + song.getName()); if (loadSongIntoPlayer(song, path, mediaPlayer)) { setPlayerState(PlayerState.STOP); return true; } } catch (DataUnavailableException e) { Log.w(TAG, e); } } catch (PlaylistPositionOutOfRangeException e) { Log.w(TAG, e); } catch (EmptyPlaylistException e) { Log.w(TAG, e); } return false; } @Override public void setPlayMode(IPlayMode playMode) { currentPlaylistManager.setPlayMode(playMode); } @Override public void setPlaylist(IPlaylist playlist) { int positionToSeek = playlist.getPositionInSong(); currentPlaylistManager.setPlaylist(playlist); jumpToPlaylistPosition(playlist.getPositionInList()); seekTo(positionToSeek); } }