diff options
Diffstat (limited to 'src/github/daneren2005/dsub/service/DownloadServiceImpl.java')
-rw-r--r-- | src/github/daneren2005/dsub/service/DownloadServiceImpl.java | 1539 |
1 files changed, 1539 insertions, 0 deletions
diff --git a/src/github/daneren2005/dsub/service/DownloadServiceImpl.java b/src/github/daneren2005/dsub/service/DownloadServiceImpl.java new file mode 100644 index 00000000..04875f34 --- /dev/null +++ b/src/github/daneren2005/dsub/service/DownloadServiceImpl.java @@ -0,0 +1,1539 @@ +/* + This file is part of Subsonic. + + Subsonic 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 + (at your option) any later version. + + Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import static github.daneren2005.dsub.domain.PlayerState.COMPLETED; +import static github.daneren2005.dsub.domain.PlayerState.DOWNLOADING; +import static github.daneren2005.dsub.domain.PlayerState.IDLE; +import static github.daneren2005.dsub.domain.PlayerState.PAUSED; +import static github.daneren2005.dsub.domain.PlayerState.PREPARED; +import static github.daneren2005.dsub.domain.PlayerState.PREPARING; +import static github.daneren2005.dsub.domain.PlayerState.STARTED; +import static github.daneren2005.dsub.domain.PlayerState.STOPPED; +import github.daneren2005.dsub.audiofx.EqualizerController; +import github.daneren2005.dsub.audiofx.VisualizerController; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.PlayerState; +import github.daneren2005.dsub.domain.RepeatMode; +import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver; +import github.daneren2005.dsub.util.CancellableTask; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.ShufflePlayBuffer; +import github.daneren2005.dsub.util.SimpleServiceBinder; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.util.compat.RemoteControlClientHelper; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.audiofx.AudioEffect; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.PowerManager; +import android.util.Log; +import android.support.v4.util.LruCache; +import java.net.URLEncoder; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class DownloadServiceImpl extends Service implements DownloadService { + + private static final String TAG = DownloadServiceImpl.class.getSimpleName(); + + public static final String CMD_PLAY = "github.daneren2005.dsub.CMD_PLAY"; + public static final String CMD_TOGGLEPAUSE = "github.daneren2005.dsub.CMD_TOGGLEPAUSE"; + public static final String CMD_PAUSE = "github.daneren2005.dsub.CMD_PAUSE"; + public static final String CMD_STOP = "github.daneren2005.dsub.CMD_STOP"; + public static final String CMD_PREVIOUS = "github.daneren2005.dsub.CMD_PREVIOUS"; + public static final String CMD_NEXT = "github.daneren2005.dsub.CMD_NEXT"; + + + private RemoteControlClientHelper mRemoteControl; + + private final IBinder binder = new SimpleServiceBinder<DownloadService>(this); + private Looper mediaPlayerLooper; + private MediaPlayer mediaPlayer; + private MediaPlayer nextMediaPlayer; + private boolean nextSetup = false; + private boolean isPartial = true; + private final List<DownloadFile> downloadList = new ArrayList<DownloadFile>(); + private final List<DownloadFile> backgroundDownloadList = new ArrayList<DownloadFile>(); + private final Handler handler = new Handler(); + private Handler mediaPlayerHandler; + private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this); + private final ShufflePlayBuffer shufflePlayBuffer = new ShufflePlayBuffer(this); + + private final LruCache<MusicDirectory.Entry, DownloadFile> downloadFileCache = new LruCache<MusicDirectory.Entry, DownloadFile>(100); + private final List<DownloadFile> cleanupCandidates = new ArrayList<DownloadFile>(); + private final Scrobbler scrobbler = new Scrobbler(); + private final JukeboxService jukeboxService = new JukeboxService(this); + private DownloadFile currentPlaying; + private DownloadFile nextPlaying; + private DownloadFile currentDownloading; + private CancellableTask bufferTask; + private CancellableTask nextPlayingTask; + private PlayerState playerState = IDLE; + private PlayerState nextPlayerState = IDLE; + private boolean shufflePlay; + private long revision; + private static DownloadService instance; + private String suggestedPlaylistName; + private String suggestedPlaylistId; + private PowerManager.WakeLock wakeLock; + private boolean keepScreenOn; + private int cachedPosition = 0; + + private static boolean equalizerAvailable; + private static boolean visualizerAvailable; + private EqualizerController equalizerController; + private VisualizerController visualizerController; + private boolean showVisualization; + private boolean jukeboxEnabled; + private PositionCache positionCache; + private StreamProxy proxy; + + private Timer sleepTimer; + private int timerDuration; + private boolean autoPlayStart = false; + + static { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + equalizerAvailable = true; + visualizerAvailable = true; + } + } + + @Override + public void onCreate() { + super.onCreate(); + + new Thread(new Runnable() { + public void run() { + Looper.prepare(); + + mediaPlayer = new MediaPlayer(); + mediaPlayer.setWakeMode(DownloadServiceImpl.this, PowerManager.PARTIAL_WAKE_LOCK); + + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int more) { + handleError(new Exception("MediaPlayer error: " + what + " (" + more + ")")); + return false; + } + }); + + try { + Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); + sendBroadcast(i); + } catch(Throwable e) { + // Froyo or lower + } + + mediaPlayerLooper = Looper.myLooper(); + mediaPlayerHandler = new Handler(mediaPlayerLooper); + Looper.loop(); + } + }).start(); + + Util.registerMediaButtonEventReceiver(this); + + if (mRemoteControl == null) { + // Use the remote control APIs (if available) to set the playback state + mRemoteControl = RemoteControlClientHelper.createInstance(); + ComponentName mediaButtonReceiverComponent = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName()); + mRemoteControl.register(this, mediaButtonReceiverComponent); + } + + PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); + wakeLock.setReferenceCounted(false); + + SharedPreferences prefs = Util.getPreferences(this); + try { + timerDuration = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION, "5")); + } catch(Throwable e) { + timerDuration = 5; + } + sleepTimer = null; + + keepScreenOn = prefs.getBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, false); + + instance = this; + lifecycleSupport.onCreate(); + + if(prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false)) { + getEqualizerController(); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + lifecycleSupport.onStart(intent); + return START_NOT_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + instance = null; + + if(currentPlaying != null) currentPlaying.setPlaying(false); + if(sleepTimer != null){ + sleepTimer.cancel(); + sleepTimer.purge(); + } + lifecycleSupport.onDestroy(); + + try { + Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); + sendBroadcast(i); + } catch(Throwable e) { + // Froyo or lower + } + + mediaPlayer.release(); + if(nextMediaPlayer != null) { + nextMediaPlayer.release(); + } + mediaPlayerLooper.quit(); + shufflePlayBuffer.shutdown(); + if (equalizerController != null) { + equalizerController.release(); + } + if (visualizerController != null) { + visualizerController.release(); + } + if (mRemoteControl != null) { + mRemoteControl.unregister(this); + mRemoteControl = null; + } + + if(bufferTask != null) { + bufferTask.cancel(); + } + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + } + Util.hidePlayingNotification(this, this, handler); + } + + public static DownloadService getInstance() { + return instance; + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) { + setShufflePlayEnabled(false); + int offset = 1; + + if (songs.isEmpty()) { + return; + } + if (playNext) { + if (autoplay && getCurrentPlayingIndex() >= 0) { + offset = 0; + } + for (MusicDirectory.Entry song : songs) { + DownloadFile downloadFile = new DownloadFile(this, song, save); + downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); + offset++; + } + revision++; + } else { + int size = size(); + int index = getCurrentPlayingIndex(); + for (MusicDirectory.Entry song : songs) { + DownloadFile downloadFile = new DownloadFile(this, song, save); + downloadList.add(downloadFile); + } + if(!autoplay && (size - 1) == index) { + setNextPlaying(); + } + revision++; + } + updateJukeboxPlaylist(); + + if(shuffle) { + shuffle(); + } + + if (autoplay) { + play(0); + } else { + if (currentPlaying == null) { + currentPlaying = downloadList.get(0); + currentPlaying.setPlaying(true); + } + checkDownloads(); + } + lifecycleSupport.serializeDownloadQueue(); + } + public synchronized void downloadBackground(List<MusicDirectory.Entry> songs, boolean save) { + for (MusicDirectory.Entry song : songs) { + DownloadFile downloadFile = new DownloadFile(this, song, save); + backgroundDownloadList.add(downloadFile); + } + revision++; + + checkDownloads(); + lifecycleSupport.serializeDownloadQueue(); + } + + private void updateJukeboxPlaylist() { + if (jukeboxEnabled) { + jukeboxService.updatePlaylist(); + } + } + + public void restore(List<MusicDirectory.Entry> songs, int currentPlayingIndex, int currentPlayingPosition) { + SharedPreferences prefs = Util.getPreferences(this); + boolean startShufflePlay = prefs.getBoolean(Constants.PREFERENCES_KEY_SHUFFLE_MODE, false); + download(songs, false, false, false, false); + if(startShufflePlay) { + shufflePlay = true; + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_SHUFFLE_MODE, true); + editor.commit(); + } + if (currentPlayingIndex != -1) { + while(mediaPlayer == null) { + Util.sleepQuietly(50L); + } + + play(currentPlayingIndex, false); + if (currentPlaying != null && currentPlaying.isCompleteFileAvailable()) { + doPlay(currentPlaying, currentPlayingPosition, autoPlayStart); + } + autoPlayStart = false; + } + } + + @Override + public synchronized void setShufflePlayEnabled(boolean enabled) { + shufflePlay = enabled; + if (shufflePlay) { + clear(); + checkDownloads(); + } + SharedPreferences.Editor editor = Util.getPreferences(this).edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_SHUFFLE_MODE, enabled); + editor.commit(); + } + + @Override + public boolean isShufflePlayEnabled() { + return shufflePlay; + } + + @Override + public synchronized void shuffle() { + Collections.shuffle(downloadList); + if (currentPlaying != null) { + downloadList.remove(getCurrentPlayingIndex()); + downloadList.add(0, currentPlaying); + } + revision++; + lifecycleSupport.serializeDownloadQueue(); + updateJukeboxPlaylist(); + setNextPlaying(); + } + + @Override + public RepeatMode getRepeatMode() { + return Util.getRepeatMode(this); + } + + @Override + public void setRepeatMode(RepeatMode repeatMode) { + Util.setRepeatMode(this, repeatMode); + setNextPlaying(); + } + + @Override + public boolean getKeepScreenOn() { + return keepScreenOn; + } + + @Override + public void setKeepScreenOn(boolean keepScreenOn) { + this.keepScreenOn = keepScreenOn; + + SharedPreferences prefs = Util.getPreferences(this); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, keepScreenOn); + editor.commit(); + } + + @Override + public boolean getShowVisualization() { + return showVisualization; + } + + @Override + public void setShowVisualization(boolean showVisualization) { + this.showVisualization = showVisualization; + } + + @Override + public synchronized DownloadFile forSong(MusicDirectory.Entry song) { + for (DownloadFile downloadFile : downloadList) { + if (downloadFile.getSong().equals(song) && + ((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && downloadFile.getPartialFile().exists()) + || downloadFile.isWorkDone())) { + return downloadFile; + } + } + for (DownloadFile downloadFile : backgroundDownloadList) { + if (downloadFile.getSong().equals(song)) { + return downloadFile; + } + } + + DownloadFile downloadFile = downloadFileCache.get(song); + if (downloadFile == null) { + downloadFile = new DownloadFile(this, song, false); + downloadFileCache.put(song, downloadFile); + } + return downloadFile; + } + + @Override + public synchronized void clear() { + clear(true); + } + + @Override + public synchronized void clearBackground() { + if(currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + backgroundDownloadList.clear(); + } + + @Override + public synchronized void clearIncomplete() { + reset(); + Iterator<DownloadFile> iterator = downloadList.iterator(); + while (iterator.hasNext()) { + DownloadFile downloadFile = iterator.next(); + if (!downloadFile.isCompleteFileAvailable()) { + iterator.remove(); + } + } + lifecycleSupport.serializeDownloadQueue(); + updateJukeboxPlaylist(); + } + + @Override + public synchronized int size() { + return downloadList.size(); + } + + public synchronized void clear(boolean serialize) { + reset(); + downloadList.clear(); + revision++; + if (currentDownloading != null) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + setCurrentPlaying(null, false); + + if (serialize) { + lifecycleSupport.serializeDownloadQueue(); + } + updateJukeboxPlaylist(); + setNextPlaying(); + } + + @Override + public synchronized void remove(int which) { + downloadList.remove(which); + } + + @Override + public synchronized void remove(DownloadFile downloadFile) { + if (downloadFile == currentDownloading) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + if (downloadFile == currentPlaying) { + reset(); + setCurrentPlaying(null, false); + } + downloadList.remove(downloadFile); + backgroundDownloadList.remove(downloadFile); + revision++; + lifecycleSupport.serializeDownloadQueue(); + updateJukeboxPlaylist(); + if(downloadFile == nextPlaying) { + setNextPlaying(); + } + } + + @Override + public synchronized void delete(List<MusicDirectory.Entry> songs) { + for (MusicDirectory.Entry song : songs) { + forSong(song).delete(); + } + } + + @Override + public synchronized void unpin(List<MusicDirectory.Entry> songs) { + for (MusicDirectory.Entry song : songs) { + forSong(song).unpin(); + } + } + + synchronized void setCurrentPlaying(int currentPlayingIndex, boolean showNotification) { + try { + setCurrentPlaying(downloadList.get(currentPlayingIndex), showNotification); + } catch (IndexOutOfBoundsException x) { + // Ignored + } + } + + synchronized void setCurrentPlaying(DownloadFile currentPlaying, boolean showNotification) { + if(this.currentPlaying != null) { + this.currentPlaying.setPlaying(false); + } + this.currentPlaying = currentPlaying; + + if (currentPlaying != null) { + Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); + mRemoteControl.updateMetadata(this, currentPlaying.getSong()); + } else { + Util.broadcastNewTrackInfo(this, null); + Util.hidePlayingNotification(this, this, handler); + } + } + + synchronized void setNextPlaying() { + SharedPreferences prefs = Util.getPreferences(DownloadServiceImpl.this); + boolean gaplessPlayback = prefs.getBoolean(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK, true); + if(!gaplessPlayback) { + nextPlaying = null; + nextPlayerState = IDLE; + return; + } + + int index = getCurrentPlayingIndex(); + if (index != -1) { + switch (getRepeatMode()) { + case OFF: + index = index + 1; + break; + case ALL: + index = (index + 1) % size(); + break; + case SINGLE: + break; + default: + break; + } + } + + nextSetup = false; + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + if(index < size() && index != -1) { + nextPlaying = downloadList.get(index); + nextPlayingTask = new CheckCompletionTask(nextPlaying); + nextPlayingTask.start(); + } else { + nextPlaying = null; + setNextPlayerState(IDLE); + } + } + + @Override + public synchronized int getCurrentPlayingIndex() { + return downloadList.indexOf(currentPlaying); + } + + @Override + public DownloadFile getCurrentPlaying() { + return currentPlaying; + } + + @Override + public DownloadFile getCurrentDownloading() { + return currentDownloading; + } + + @Override + public List<DownloadFile> getSongs() { + return downloadList; + } + + @Override + public synchronized List<DownloadFile> getDownloads() { + List<DownloadFile> temp = new ArrayList<DownloadFile>(); + temp.addAll(downloadList); + temp.addAll(backgroundDownloadList); + return temp; + } + + @Override + public List<DownloadFile> getBackgroundDownloads() { + return backgroundDownloadList; + } + + /** Plays either the current song (resume) or the first/next one in queue. */ + public synchronized void play() + { + int current = getCurrentPlayingIndex(); + if (current == -1) { + play(0); + } else { + play(current); + } + } + + @Override + public synchronized void play(int index) { + play(index, true); + } + + private synchronized void play(int index, boolean start) { + if (index < 0 || index >= size()) { + reset(); + setCurrentPlaying(null, false); + lifecycleSupport.serializeDownloadQueue(); + } else { + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + setCurrentPlaying(index, start); + if (start) { + if (jukeboxEnabled) { + jukeboxService.skip(getCurrentPlayingIndex(), 0); + setPlayerState(STARTED); + } else { + bufferAndPlay(); + } + } + checkDownloads(); + setNextPlaying(); + } + } + private synchronized void playNext() { + if(nextPlaying != null && nextPlayerState == PlayerState.PREPARED) { + if(!nextSetup) { + playNext(true); + } else { + nextSetup = false; + playNext(false); + } + } else { + onSongCompleted(); + } + } + private synchronized void playNext(boolean start) { + // Swap the media players since nextMediaPlayer is ready to play + if(start) { + nextMediaPlayer.start(); + } else { + Log.i(TAG, "nextMediaPlayer already playing"); + } + MediaPlayer tmp = mediaPlayer; + mediaPlayer = nextMediaPlayer; + nextMediaPlayer = tmp; + setCurrentPlaying(nextPlaying, true); + setPlayerState(PlayerState.STARTED); + setupHandlers(currentPlaying, false); + setNextPlaying(); + + // Proxy should not be being used here since the next player was already setup to play + if(proxy != null) { + proxy.stop(); + proxy = null; + } + } + + /** Plays or resumes the playback, depending on the current player state. */ + public synchronized void togglePlayPause() { + if (playerState == PAUSED || playerState == COMPLETED || playerState == STOPPED) { + start(); + } else if (playerState == STOPPED || playerState == IDLE) { + autoPlayStart = true; + play(); + } else if (playerState == STARTED) { + pause(); + } + } + + @Override + public synchronized void seekTo(int position) { + try { + if (jukeboxEnabled) { + jukeboxService.skip(getCurrentPlayingIndex(), position / 1000); + } else { + mediaPlayer.seekTo(position); + cachedPosition = position; + } + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized void previous() { + int index = getCurrentPlayingIndex(); + if (index == -1) { + return; + } + + // Restart song if played more than five seconds. + if (getPlayerPosition() > 5000 || index == 0) { + play(index); + } else { + play(index - 1); + } + } + + @Override + public synchronized void next() { + int index = getCurrentPlayingIndex(); + if (index != -1) { + play(index + 1); + } + } + + private void onSongCompleted() { + int index = getCurrentPlayingIndex(); + if (index != -1) { + switch (getRepeatMode()) { + case OFF: + play(index + 1); + break; + case ALL: + play((index + 1) % size()); + break; + case SINGLE: + play(index); + break; + default: + break; + } + } + } + + @Override + public synchronized void pause() { + try { + if (playerState == STARTED) { + if (jukeboxEnabled) { + jukeboxService.stop(); + } else { + mediaPlayer.pause(); + } + setPlayerState(PAUSED); + } + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized void stop() { + try { + if (playerState == STARTED) { + if (jukeboxEnabled) { + jukeboxService.stop(); + } else { + mediaPlayer.pause(); + } + setPlayerState(STOPPED); + } else if(playerState == PAUSED) { + setPlayerState(STOPPED); + } + } catch(Exception x) { + handleError(x); + } + } + + @Override + public synchronized void start() { + try { + if (jukeboxEnabled) { + jukeboxService.start(); + } else { + mediaPlayer.start(); + } + setPlayerState(STARTED); + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized void reset() { + if (bufferTask != null) { + bufferTask.cancel(); + } + try { + setPlayerState(IDLE); + mediaPlayer.setOnErrorListener(null); + mediaPlayer.setOnCompletionListener(null); + mediaPlayer.reset(); + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized int getPlayerPosition() { + try { + if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) { + return 0; + } + if (jukeboxEnabled) { + return jukeboxService.getPositionSeconds() * 1000; + } else { + return cachedPosition; + } + } catch (Exception x) { + handleError(x); + return 0; + } + } + + @Override + public synchronized int getPlayerDuration() { + if (currentPlaying != null) { + Integer duration = currentPlaying.getSong().getDuration(); + if (duration != null) { + return duration * 1000; + } + } + if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) { + try { + return mediaPlayer.getDuration(); + } catch (Exception x) { + handleError(x); + } + } + return 0; + } + + @Override + public PlayerState getPlayerState() { + return playerState; + } + + public synchronized void setPlayerState(final PlayerState playerState) { + Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")"); + + if (playerState == PAUSED) { + lifecycleSupport.serializeDownloadQueue(); + } + + boolean show = playerState == PlayerState.STARTED; + boolean pause = playerState == PlayerState.PAUSED; + boolean hide = playerState == PlayerState.STOPPED; + Util.broadcastPlaybackStatusChange(this, playerState); + + this.playerState = playerState; + + if(playerState == STARTED) { + Util.requestAudioFocus(this); + } + + if (show) { + Util.showPlayingNotification(this, this, handler, currentPlaying.getSong()); + } else if (pause) { + SharedPreferences prefs = Util.getPreferences(this); + if(prefs.getBoolean(Constants.PREFERENCES_KEY_PERSISTENT_NOTIFICATION, false)) { + Util.showPlayingNotification(this, this, handler, currentPlaying.getSong()); + } else { + Util.hidePlayingNotification(this, this, handler); + } + } else if(hide) { + Util.hidePlayingNotification(this, this, handler); + } + mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState()); + + if (playerState == STARTED) { + scrobbler.scrobble(this, currentPlaying, false); + } else if (playerState == COMPLETED) { + scrobbler.scrobble(this, currentPlaying, true); + } + + if(playerState == STARTED && positionCache == null) { + positionCache = new PositionCache(); + Thread thread = new Thread(positionCache); + thread.start(); + } else if(playerState != STARTED && positionCache != null) { + positionCache.stop(); + positionCache = null; + } + } + + private class PositionCache implements Runnable { + boolean isRunning = true; + + public void stop() { + isRunning = false; + } + + @Override + public void run() { + // Stop checking position before the song reaches completion + while(isRunning) { + try { + if(mediaPlayer != null && playerState == STARTED) { + cachedPosition = mediaPlayer.getCurrentPosition(); + } + Thread.sleep(200L); + } + catch(Exception e) { + Log.w(TAG, "Crashed getting current position", e); + isRunning = false; + positionCache = null; + } + } + } + } + + private void setPlayerStateCompleted() { + Log.i(TAG, this.playerState.name() + " -> " + PlayerState.COMPLETED + " (" + currentPlaying + ")"); + this.playerState = PlayerState.COMPLETED; + if(positionCache != null) { + positionCache.stop(); + positionCache = null; + } + scrobbler.scrobble(this, currentPlaying, true); + } + + private synchronized void setNextPlayerState(PlayerState playerState) { + Log.i(TAG, "Next: " + this.nextPlayerState.name() + " -> " + playerState.name() + " (" + nextPlaying + ")"); + this.nextPlayerState = playerState; + } + + @Override + public void setSuggestedPlaylistName(String name, String id) { + this.suggestedPlaylistName = name; + this.suggestedPlaylistId = id; + } + + @Override + public String getSuggestedPlaylistName() { + return suggestedPlaylistName; + } + + @Override + public String getSuggestedPlaylistId() { + return suggestedPlaylistId; + } + + @Override + public boolean getEqualizerAvailable() { + return equalizerAvailable; + } + + @Override + public boolean getVisualizerAvailable() { + return visualizerAvailable; + } + + @Override + public EqualizerController getEqualizerController() { + if (equalizerAvailable && equalizerController == null) { + equalizerController = new EqualizerController(this, mediaPlayer); + if (!equalizerController.isAvailable()) { + equalizerController = null; + } else { + equalizerController.loadSettings(); + } + } + return equalizerController; + } + + @Override + public VisualizerController getVisualizerController() { + if (visualizerAvailable && visualizerController == null) { + visualizerController = new VisualizerController(this, mediaPlayer); + if (!visualizerController.isAvailable()) { + visualizerController = null; + } + } + return visualizerController; + } + + @Override + public boolean isJukeboxEnabled() { + return jukeboxEnabled; + } + + @Override + public void setJukeboxEnabled(boolean jukeboxEnabled) { + this.jukeboxEnabled = jukeboxEnabled; + jukeboxService.setEnabled(jukeboxEnabled); + if (jukeboxEnabled) { + reset(); + + // Cancel current download, if necessary. + if (currentDownloading != null) { + currentDownloading.cancelDownload(); + } + } + } + + @Override + public void adjustJukeboxVolume(boolean up) { + jukeboxService.adjustVolume(up); + } + + private synchronized void bufferAndPlay() { + if(playerState != PREPARED) { + reset(); + + bufferTask = new BufferTask(currentPlaying, 0); + bufferTask.start(); + } else { + doPlay(currentPlaying, 0, true); + } + } + + private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) { + try { + downloadFile.setPlaying(true); + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + isPartial = file.equals(downloadFile.getPartialFile()); + downloadFile.updateModificationDate(); + + mediaPlayer.setOnCompletionListener(null); + mediaPlayer.reset(); + setPlayerState(IDLE); + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + String dataSource = file.getPath(); + if(isPartial) { + if (proxy == null) { + proxy = new StreamProxy(this); + proxy.start(); + } + dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), URLEncoder.encode(dataSource, Constants.UTF_8)); + Log.i(TAG, "Data Source: " + dataSource); + } else if(proxy != null) { + proxy.stop(); + proxy = null; + } + mediaPlayer.setDataSource(dataSource); + setPlayerState(PREPARING); + + mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { + public void onBufferingUpdate(MediaPlayer mp, int percent) { + Log.i(TAG, "Buffered " + percent + "%"); + if(percent == 100) { + mediaPlayer.setOnBufferingUpdateListener(null); + } + } + }); + + mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mediaPlayer) { + try { + setPlayerState(PREPARED); + + synchronized (DownloadServiceImpl.this) { + if (position != 0) { + Log.i(TAG, "Restarting player from position " + position); + mediaPlayer.seekTo(position); + } + cachedPosition = position; + + if (start) { + mediaPlayer.start(); + setPlayerState(STARTED); + } else { + setPlayerState(PAUSED); + } + } + + lifecycleSupport.serializeDownloadQueue(); + } catch (Exception x) { + handleError(x); + } + } + }); + + setupHandlers(downloadFile, isPartial); + + mediaPlayer.prepareAsync(); + } catch (Exception x) { + handleError(x); + } + } + + private synchronized void setupNext(final DownloadFile downloadFile) { + try { + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + if(nextMediaPlayer != null) { + nextMediaPlayer.setOnCompletionListener(null); + nextMediaPlayer.release(); + nextMediaPlayer = null; + } + nextMediaPlayer = new MediaPlayer(); + nextMediaPlayer.setWakeMode(DownloadServiceImpl.this, PowerManager.PARTIAL_WAKE_LOCK); + try { + nextMediaPlayer.setAudioSessionId(mediaPlayer.getAudioSessionId()); + } catch(Throwable e) { + nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + } + nextMediaPlayer.setDataSource(file.getPath()); + setNextPlayerState(PREPARING); + + nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mp) { + try { + setNextPlayerState(PREPARED); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)) { + mediaPlayer.setNextMediaPlayer(nextMediaPlayer); + nextSetup = true; + } + } catch (Exception x) { + handleErrorNext(x); + } + } + }); + + nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { + Log.w(TAG, "Error on playing next " + "(" + what + ", " + extra + "): " + downloadFile); + return true; + } + }); + + nextMediaPlayer.prepareAsync(); + } catch (Exception x) { + handleErrorNext(x); + } + } + + private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) { + final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000; + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { + Log.w(TAG, "Error on playing file " + "(" + what + ", " + extra + "): " + downloadFile); + int pos = cachedPosition; + reset(); + if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000))) { + playNext(); + } else { + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, true); + downloadFile.setPlaying(true); + } + return true; + } + }); + + mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mediaPlayer) { + // Acquire a temporary wakelock, since when we return from + // this callback the MediaPlayer will release its wakelock + // and allow the device to go to sleep. + wakeLock.acquire(60000); + + setPlayerStateCompleted(); + + int pos = cachedPosition; + Log.i(TAG, "Ending position " + pos + " of " + duration); + if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000))) { + playNext(); + return; + } + + // If file is not completely downloaded, restart the playback from the current position. + synchronized (DownloadServiceImpl.this) { + if(downloadFile.isWorkDone()) { + // Complete was called early even though file is fully buffered + Log.i(TAG, "Requesting restart from " + pos + " of " + duration); + reset(); + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, true); + downloadFile.setPlaying(true); + } else { + Log.i(TAG, "Requesting restart from " + pos + " of " + duration); + reset(); + bufferTask = new BufferTask(downloadFile, pos); + bufferTask.start(); + } + } + } + }); + } + + @Override + public void setSleepTimerDuration(int duration){ + timerDuration = duration; + } + + @Override + public void startSleepTimer(){ + if(sleepTimer != null){ + sleepTimer.cancel(); + sleepTimer.purge(); + } + + sleepTimer = new Timer(); + + sleepTimer.schedule(new TimerTask() { + @Override + public void run() { + pause(); + sleepTimer.cancel(); + sleepTimer.purge(); + sleepTimer = null; + } + + }, timerDuration * 60 * 1000); + } + + @Override + public void stopSleepTimer() { + if(sleepTimer != null){ + sleepTimer.cancel(); + sleepTimer.purge(); + } + sleepTimer = null; + } + + @Override + public boolean getSleepTimer() { + return sleepTimer != null; + } + + @Override + public void setVolume(float volume) { + if(mediaPlayer != null) { + mediaPlayer.setVolume(volume, volume); + } + } + + @Override + public synchronized void swap(boolean mainList, int from, int to) { + List<DownloadFile> list = mainList ? downloadList : backgroundDownloadList; + int max = list.size(); + if(to >= max) { + to = max - 1; + } + else if(to < 0) { + to = 0; + } + + int currentPlayingIndex = getCurrentPlayingIndex(); + DownloadFile movedSong = list.remove(from); + list.add(to, movedSong); + if(jukeboxEnabled && mainList) { + updateJukeboxPlaylist(); + } else if(mainList && (movedSong == nextPlaying || (currentPlayingIndex + 1) == to)) { + // Moving next playing or moving a song to be next playing + setNextPlaying(); + } + } + + private void handleError(Exception x) { + Log.w(TAG, "Media player error: " + x, x); + mediaPlayer.reset(); + setPlayerState(IDLE); + } + private void handleErrorNext(Exception x) { + Log.w(TAG, "Next Media player error: " + x, x); + nextMediaPlayer.reset(); + setNextPlayerState(IDLE); + } + + protected synchronized void checkDownloads() { + if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) { + return; + } + + if (shufflePlay) { + checkShufflePlay(); + } + + if (jukeboxEnabled || !Util.isNetworkConnected(this)) { + return; + } + + if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) { + return; + } + + // Need to download current playing? + if (currentPlaying != null && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) { + // Cancel current download, if necessary. + if (currentDownloading != null) { + currentDownloading.cancelDownload(); + } + + currentDownloading = currentPlaying; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + } + + // Find a suitable target for download. + else if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed() && (!downloadList.isEmpty() || !backgroundDownloadList.isEmpty())) { + currentDownloading = null; + int n = size(); + + int preloaded = 0; + + if(n != 0) { + int start = currentPlaying == null ? 0 : getCurrentPlayingIndex(); + if(start == -1) { + start = 0; + } + int i = start; + do { + DownloadFile downloadFile = downloadList.get(i); + if (!downloadFile.isWorkDone()) { + if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(this)) { + currentDownloading = downloadFile; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + if(i == (start + 1)) { + setNextPlayerState(DOWNLOADING); + } + break; + } + } else if (currentPlaying != downloadFile) { + preloaded++; + } + + i = (i + 1) % n; + } while (i != start); + } + + if((preloaded + 1 == n || preloaded >= Util.getPreloadCount(this) || downloadList.isEmpty()) && !backgroundDownloadList.isEmpty()) { + for(int i = 0; i < backgroundDownloadList.size(); i++) { + DownloadFile downloadFile = backgroundDownloadList.get(i); + if(downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved())) { + // Don't need to keep list like active song list + backgroundDownloadList.remove(i); + revision++; + i--; + } else { + currentDownloading = downloadFile; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + break; + } + } + } + } + + // Delete obsolete .partial and .complete files. + cleanup(); + } + + private synchronized void checkShufflePlay() { + + // Get users desired random playlist size + SharedPreferences prefs = Util.getPreferences(this); + int listSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_RANDOM_SIZE, "20")); + boolean wasEmpty = downloadList.isEmpty(); + + long revisionBefore = revision; + + // First, ensure that list is at least 20 songs long. + int size = size(); + if (size < listSize) { + for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) { + DownloadFile downloadFile = new DownloadFile(this, song, false); + downloadList.add(downloadFile); + revision++; + } + } + + int currIndex = currentPlaying == null ? 0 : getCurrentPlayingIndex(); + + // Only shift playlist if playing song #5 or later. + if (currIndex > 4) { + int songsToShift = currIndex - 2; + for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift)) { + downloadList.add(new DownloadFile(this, song, false)); + downloadList.get(0).cancelDownload(); + downloadList.remove(0); + revision++; + } + } + + if (revisionBefore != revision) { + updateJukeboxPlaylist(); + } + + if (wasEmpty && !downloadList.isEmpty()) { + play(0); + } + } + + public long getDownloadListUpdateRevision() { + return revision; + } + + private synchronized void cleanup() { + Iterator<DownloadFile> iterator = cleanupCandidates.iterator(); + while (iterator.hasNext()) { + DownloadFile downloadFile = iterator.next(); + if (downloadFile != currentPlaying && downloadFile != currentDownloading) { + if (downloadFile.cleanup()) { + iterator.remove(); + } + } + } + } + + private class BufferTask extends CancellableTask { + private final DownloadFile downloadFile; + private final int position; + private final long expectedFileSize; + private final File partialFile; + + public BufferTask(DownloadFile downloadFile, int position) { + this.downloadFile = downloadFile; + this.position = position; + partialFile = downloadFile.getPartialFile(); + + SharedPreferences prefs = Util.getPreferences(DownloadServiceImpl.this); + long bufferLength = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_BUFFER_LENGTH, "5")); + if(bufferLength == 0) { + // Set to seconds in a day, basically infinity + bufferLength = 86400L; + } + + // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. + int bitRate = downloadFile.getBitRate(); + long byteCount = Math.max(100000, bitRate * 1024L / 8L * bufferLength); + + // Find out how large the file should grow before resuming playback. + Log.i(TAG, "Buffering from position " + position + " and bitrate " + bitRate); + expectedFileSize = (position * bitRate / 8) + byteCount; + } + + @Override + public void execute() { + setPlayerState(DOWNLOADING); + + while (!bufferComplete()) { + Util.sleepQuietly(1000L); + if (isCancelled()) { + return; + } + } + doPlay(downloadFile, position, true); + } + + private boolean bufferComplete() { + boolean completeFileAvailable = downloadFile.isWorkDone(); + long size = partialFile.length(); + + Log.i(TAG, "Buffering " + partialFile + " (" + size + "/" + expectedFileSize + ", " + completeFileAvailable + ")"); + return completeFileAvailable || size >= expectedFileSize; + } + + @Override + public String toString() { + return "BufferTask (" + downloadFile + ")"; + } + } + + private class CheckCompletionTask extends CancellableTask { + private final DownloadFile downloadFile; + private final File partialFile; + + public CheckCompletionTask(DownloadFile downloadFile) { + setNextPlayerState(PlayerState.IDLE); + this.downloadFile = downloadFile; + if(downloadFile != null) { + partialFile = downloadFile.getPartialFile(); + } else { + partialFile = null; + } + } + + @Override + public void execute() { + if(downloadFile == null) { + return; + } + + // Do an initial sleep so this prepare can't compete with main prepare + Util.sleepQuietly(5000L); + while (!bufferComplete()) { + Util.sleepQuietly(5000L); + if (isCancelled()) { + return; + } + } + + // Start the setup of the next media player + mediaPlayerHandler.post(new Runnable() { + public void run() { + setupNext(downloadFile); + } + }); + } + + private boolean bufferComplete() { + boolean completeFileAvailable = downloadFile.isWorkDone(); + Log.i(TAG, "Buffering next " + partialFile + " (" + partialFile.length() + ")"); + return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED); + } + + @Override + public String toString() { + return "CheckCompletionTask (" + downloadFile + ")"; + } + } +} |