/* 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 . Copyright 2009 (C) Sindre Mehus */ package github.daneren2005.subphonic.service; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.media.AudioManager; import android.media.MediaPlayer; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; import android.util.Log; import github.daneren2005.subphonic.audiofx.EqualizerController; import github.daneren2005.subphonic.audiofx.VisualizerController; import github.daneren2005.subphonic.domain.MusicDirectory; import github.daneren2005.subphonic.domain.PlayerState; import github.daneren2005.subphonic.domain.RepeatMode; import github.daneren2005.subphonic.util.CancellableTask; import github.daneren2005.subphonic.util.LRUCache; import github.daneren2005.subphonic.util.ShufflePlayBuffer; import github.daneren2005.subphonic.util.SimpleServiceBinder; import github.daneren2005.subphonic.util.Util; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import static github.daneren2005.subphonic.domain.PlayerState.*; import github.daneren2005.subphonic.util.*; /** * @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.subphonic.CMD_PLAY"; public static final String CMD_TOGGLEPAUSE = "github.daneren2005.subphonic.CMD_TOGGLEPAUSE"; public static final String CMD_PAUSE = "github.daneren2005.subphonic.CMD_PAUSE"; public static final String CMD_STOP = "github.daneren2005.subphonic.CMD_STOP"; public static final String CMD_PREVIOUS = "github.daneren2005.subphonic.CMD_PREVIOUS"; public static final String CMD_NEXT = "github.daneren2005.subphonic.CMD_NEXT"; private final IBinder binder = new SimpleServiceBinder(this); private MediaPlayer mediaPlayer; private final List downloadList = new ArrayList(); private final Handler handler = new Handler(); private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this); private final ShufflePlayBuffer shufflePlayBuffer = new ShufflePlayBuffer(this); private final LRUCache downloadFileCache = new LRUCache(100); private final List cleanupCandidates = new ArrayList(); private final Scrobbler scrobbler = new Scrobbler(); private final JukeboxService jukeboxService = new JukeboxService(this); private DownloadFile currentPlaying; private DownloadFile currentDownloading; private CancellableTask bufferTask; private PlayerState playerState = IDLE; private boolean shufflePlay; private long revision; private static DownloadService instance; private String suggestedPlaylistName; private PowerManager.WakeLock wakeLock; private boolean keepScreenOn = false; private static boolean equalizerAvailable; private static boolean visualizerAvailable; private EqualizerController equalizerController; private VisualizerController visualizerController; private boolean showVisualization; private boolean jukeboxEnabled; static { try { EqualizerController.checkAvailable(); equalizerAvailable = true; } catch (Throwable t) { equalizerAvailable = false; } } static { try { VisualizerController.checkAvailable(); visualizerAvailable = true; } catch (Throwable t) { visualizerAvailable = false; } } @Override public void onCreate() { super.onCreate(); mediaPlayer = new MediaPlayer(); mediaPlayer.setWakeMode(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; } }); if (equalizerAvailable) { equalizerController = new EqualizerController(this, mediaPlayer); if (!equalizerController.isAvailable()) { equalizerController = null; } else { equalizerController.loadSettings(); } } if (visualizerAvailable) { visualizerController = new VisualizerController(this, mediaPlayer); if (!visualizerController.isAvailable()) { visualizerController = null; } } PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); wakeLock.setReferenceCounted(false); instance = this; lifecycleSupport.onCreate(); } @Override public void onStart(Intent intent, int startId) { super.onStart(intent, startId); lifecycleSupport.onStart(intent); } @Override public void onDestroy() { super.onDestroy(); lifecycleSupport.onDestroy(); mediaPlayer.release(); shufflePlayBuffer.shutdown(); if (equalizerController != null) { equalizerController.release(); } if (visualizerController != null) { visualizerController.release(); } instance = null; } public static DownloadService getInstance() { return instance; } @Override public IBinder onBind(Intent intent) { return binder; } @Override public synchronized void download(List songs, boolean save, boolean autoplay, boolean playNext) { shufflePlay = 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 { for (MusicDirectory.Entry song : songs) { DownloadFile downloadFile = new DownloadFile(this, song, save); downloadList.add(downloadFile); } revision++; } updateJukeboxPlaylist(); if (autoplay) { play(0); } else { if (currentPlaying == null) { currentPlaying = downloadList.get(0); } checkDownloads(); } lifecycleSupport.serializeDownloadQueue(); } private void updateJukeboxPlaylist() { if (jukeboxEnabled) { jukeboxService.updatePlaylist(); } } public void restore(List songs, int currentPlayingIndex, int currentPlayingPosition) { download(songs, false, false, false); if (currentPlayingIndex != -1) { play(currentPlayingIndex, false); if (currentPlaying.isCompleteFileAvailable()) { doPlay(currentPlaying, currentPlayingPosition, false); } } } @Override public synchronized void setShufflePlayEnabled(boolean enabled) { shufflePlay = enabled; if (shufflePlay) { clear(); checkDownloads(); } } @Override public synchronized 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(); } @Override public RepeatMode getRepeatMode() { return Util.getRepeatMode(this); } @Override public void setRepeatMode(RepeatMode repeatMode) { Util.setRepeatMode(this, repeatMode); } @Override public boolean getKeepScreenOn() { return keepScreenOn; } @Override public void setKeepScreenOn(boolean keepScreenOn) { this.keepScreenOn = keepScreenOn; } @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)) { 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 clearIncomplete() { reset(); Iterator 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(); } @Override public synchronized void remove(DownloadFile downloadFile) { if (downloadFile == currentDownloading) { currentDownloading.cancelDownload(); currentDownloading = null; } if (downloadFile == currentPlaying) { reset(); setCurrentPlaying(null, false); } downloadList.remove(downloadFile); revision++; lifecycleSupport.serializeDownloadQueue(); updateJukeboxPlaylist(); } @Override public synchronized void delete(List songs) { for (MusicDirectory.Entry song : songs) { forSong(song).delete(); } } @Override public synchronized void unpin(List 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) { this.currentPlaying = currentPlaying; if (currentPlaying != null) { Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); } else { Util.broadcastNewTrackInfo(this, null); } if (currentPlaying != null && showNotification) { Util.showPlayingNotification(this, this, handler, currentPlaying.getSong()); } else { Util.hidePlayingNotification(this, this, handler); } } @Override public synchronized int getCurrentPlayingIndex() { return downloadList.indexOf(currentPlaying); } @Override public DownloadFile getCurrentPlaying() { return currentPlaying; } @Override public DownloadFile getCurrentDownloading() { return currentDownloading; } @Override public synchronized List getDownloads() { return new ArrayList(downloadList); } /** 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); } else { setCurrentPlaying(index, start); checkDownloads(); if (start) { if (jukeboxEnabled) { jukeboxService.skip(getCurrentPlayingIndex(), 0); setPlayerState(STARTED); } else { bufferAndPlay(); } } } } /** Plays or resumes the playback, depending on the current player state. */ public synchronized void togglePlayPause() { if (playerState == PAUSED || playerState == COMPLETED) { start(); } else if (playerState == STOPPED || playerState == IDLE) { 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); } } 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 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 { mediaPlayer.reset(); setPlayerState(IDLE); } 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 mediaPlayer.getCurrentPosition(); } } 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; } synchronized void setPlayerState(PlayerState playerState) { Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")"); if (playerState == PAUSED) { lifecycleSupport.serializeDownloadQueue(); } boolean show = this.playerState == PAUSED && playerState == PlayerState.STARTED; boolean hide = this.playerState == STARTED && playerState == PlayerState.PAUSED; Util.broadcastPlaybackStatusChange(this, playerState); this.playerState = playerState; if (show) { Util.showPlayingNotification(this, this, handler, currentPlaying.getSong()); } else if (hide) { Util.hidePlayingNotification(this, this, handler); } if (playerState == STARTED) { scrobbler.scrobble(this, currentPlaying, false); } else if (playerState == COMPLETED) { scrobbler.scrobble(this, currentPlaying, true); } } @Override public void setSuggestedPlaylistName(String name) { this.suggestedPlaylistName = name; } @Override public String getSuggestedPlaylistName() { return suggestedPlaylistName; } @Override public EqualizerController getEqualizerController() { return equalizerController; } @Override public VisualizerController getVisualizerController() { 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() { reset(); bufferTask = new BufferTask(currentPlaying, 0); bufferTask.start(); } private synchronized void doPlay(final DownloadFile downloadFile, int position, boolean start) { // TODO: Start play at curr pos on rebuffer instead of restart try { final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); downloadFile.updateModificationDate(); mediaPlayer.setOnCompletionListener(null); mediaPlayer.reset(); setPlayerState(IDLE); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setDataSource(file.getPath()); setPlayerState(PREPARING); mediaPlayer.prepare(); setPlayerState(PREPARED); 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); setPlayerState(COMPLETED); // If COMPLETED and not playing partial file, we are *really" finished // with the song and can move on to the next. if (!file.equals(downloadFile.getPartialFile())) { onSongCompleted(); return; } // If file is not completely downloaded, restart the playback from the current position. int pos = mediaPlayer.getCurrentPosition(); synchronized (DownloadServiceImpl.this) { // Work-around for apparent bug on certain phones: If close (less than ten seconds) to the end // of the song, skip to the next rather than restarting it. Integer duration = downloadFile.getSong().getDuration() == null ? null : downloadFile.getSong().getDuration() * 1000; if (duration != null) { if (Math.abs(duration - pos) < 10000) { Log.i(TAG, "Skipping restart from " + pos + " of " + duration); onSongCompleted(); return; } } Log.i(TAG, "Requesting restart from " + pos + " of " + duration); reset(); bufferTask = new BufferTask(downloadFile, pos); bufferTask.start(); } } }); if (position != 0) { Log.i(TAG, "Restarting player from position " + position); mediaPlayer.seekTo(position); } if (start) { mediaPlayer.start(); setPlayerState(STARTED); } else { setPlayerState(PAUSED); } lifecycleSupport.serializeDownloadQueue(); } catch (Exception x) { handleError(x); } } private void handleError(Exception x) { Log.w(TAG, "Media player error: " + x, x); mediaPlayer.reset(); setPlayerState(IDLE); } protected synchronized void checkDownloads() { if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) { return; } if (shufflePlay) { checkShufflePlay(); } if (jukeboxEnabled || !Util.isNetworkConnected(this)) { return; } if (downloadList.isEmpty()) { return; } // Need to download current playing? if (currentPlaying != null && currentPlaying != currentDownloading && !currentPlaying.isCompleteFileAvailable()) { // 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()) { int n = size(); if (n == 0) { return; } int preloaded = 0; int start = currentPlaying == null ? 0 : getCurrentPlayingIndex(); 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); break; } } else if (currentPlaying != downloadFile) { preloaded++; } i = (i + 1) % n; } while (i != start); } // 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 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 static final int BUFFER_LENGTH_SECONDS = 5; 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(); // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. int bitRate = downloadFile.getBitRate(); long byteCount = Math.max(100000, bitRate * 1024 / 8 * BUFFER_LENGTH_SECONDS); // Find out how large the file should grow before resuming playback. expectedFileSize = partialFile.length() + 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.isCompleteFileAvailable(); long size = partialFile.length(); Log.i(TAG, "Buffering " + partialFile + " (" + size + "/" + expectedFileSize + ", " + completeFileAvailable + ")"); return completeFileAvailable || size >= expectedFileSize; } @Override public String toString() { return "BufferTask (" + downloadFile + ")"; } } }