/* 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.dsub.service; import static android.support.v7.media.MediaRouter.RouteInfo; 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.Bookmark; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.domain.RemoteControlState; 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.MediaRouteManager; 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 github.daneren2005.serverproxy.BufferProxy; 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.support.v7.media.MediaRouteSelector; import android.util.Log; import android.support.v4.util.LruCache; import java.net.URLEncoder; /** * @author Sindre Mehus * @version $Id$ */ public class DownloadService extends Service { private static final String TAG = DownloadService.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"; public static final String CANCEL_DOWNLOADS = "github.daneren2005.dsub.CANCEL_DOWNLOADS"; public static final int FAST_FORWARD = 30000; public static final int REWIND = 10000; private RemoteControlClientHelper mRemoteControl; private final IBinder binder = new SimpleServiceBinder(this); private Looper mediaPlayerLooper; private MediaPlayer mediaPlayer; private MediaPlayer nextMediaPlayer; private boolean nextSetup = false; private boolean isPartial = true; private final List downloadList = new ArrayList(); private final List backgroundDownloadList = new ArrayList(); private final List toDelete = new ArrayList(); 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 downloadFileCache = new LruCache(100); private final List cleanupCandidates = new ArrayList(); private final Scrobbler scrobbler = new Scrobbler(); private RemoteController remoteController; private DownloadFile currentPlaying; private int currentPlayingIndex = -1; 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 long downloadRevision; private boolean downloadOngoing = false; private DownloadFile lastDownloaded = null; private static boolean equalizerAvailable; private static boolean visualizerAvailable; private EqualizerController equalizerController; private VisualizerController visualizerController; private boolean showVisualization; private RemoteControlState remoteState = RemoteControlState.LOCAL; private PositionCache positionCache; private BufferProxy proxy; private Timer sleepTimer; private int timerDuration; private boolean autoPlayStart = false; private MediaRouteManager mediaRouter; 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(DownloadService.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(); } }, "DownloadService").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); mediaRouter = new MediaRouteManager(this); instance = this; lifecycleSupport.onCreate(); if(prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false)) { getEqualizerController(); } if(prefs.getBoolean(Constants.PREFERENCES_VISUALIZER_ON, false)) { getVisualizerController(); showVisualization = true; } } @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(); } if(remoteController != null) { remoteController.stop(); remoteController.shutdown(); } if(proxy != null) { proxy.stop(); proxy = null; } mediaRouter.destroy(); Util.hidePlayingNotification(this, this, handler); Util.hideDownloadingNotification(this); } public static DownloadService getInstance() { return instance; } @Override public IBinder onBind(Intent intent) { return binder; } public synchronized void download(Bookmark bookmark) { clear(); DownloadFile downloadFile = new DownloadFile(this, bookmark.getEntry(), false); downloadList.add(downloadFile); revision++; updateJukeboxPlaylist(); play(0, true, bookmark.getPosition()); lifecycleSupport.serializeDownloadQueue(); } public synchronized void download(List 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) { if(song != null) { DownloadFile downloadFile = new DownloadFile(this, song, save); downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); offset++; } } setNextPlaying(); 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); currentPlayingIndex = 0; currentPlaying.setPlaying(true); } checkDownloads(); } lifecycleSupport.serializeDownloadQueue(); } public synchronized void downloadBackground(List songs, boolean save) { for (MusicDirectory.Entry song : songs) { DownloadFile downloadFile = new DownloadFile(this, song, save); if(!downloadFile.isWorkDone() || (downloadFile.shouldSave() && !downloadFile.isSaved())) { // Only add to list if there is work to be done backgroundDownloadList.add(downloadFile); } } revision++; checkDownloads(); lifecycleSupport.serializeDownloadQueue(); } private void updateJukeboxPlaylist() { if (remoteState != RemoteControlState.LOCAL) { remoteController.updatePlaylist(); } } public synchronized void restore(List songs, List toDelete, int currentPlayingIndex, int currentPlayingPosition) { SharedPreferences prefs = Util.getPreferences(this); remoteState = RemoteControlState.values()[prefs.getInt(Constants.PREFERENCES_KEY_CONTROL_MODE, 0)]; if(remoteState != RemoteControlState.LOCAL) { String id = prefs.getString(Constants.PREFERENCES_KEY_CONTROL_ID, null); setRemoteState(remoteState, null, id); } 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, autoPlayStart, currentPlayingPosition); autoPlayStart = false; } if(toDelete != null) { for(MusicDirectory.Entry entry: toDelete) { this.toDelete.add(forSong(entry)); } } } 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(); } public boolean isShufflePlayEnabled() { return shufflePlay; } public synchronized void shuffle() { Collections.shuffle(downloadList); currentPlayingIndex = downloadList.indexOf(currentPlaying); if (currentPlaying != null) { downloadList.remove(getCurrentPlayingIndex()); downloadList.add(0, currentPlaying); currentPlayingIndex = 0; } revision++; lifecycleSupport.serializeDownloadQueue(); updateJukeboxPlaylist(); setNextPlaying(); } public RepeatMode getRepeatMode() { return Util.getRepeatMode(this); } public void setRepeatMode(RepeatMode repeatMode) { Util.setRepeatMode(this, repeatMode); setNextPlaying(); } public boolean getKeepScreenOn() { return keepScreenOn; } 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(); } public boolean getShowVisualization() { return showVisualization; } public void setShowVisualization(boolean showVisualization) { this.showVisualization = showVisualization; SharedPreferences.Editor editor = Util.getPreferences(this).edit(); editor.putBoolean(Constants.PREFERENCES_VISUALIZER_ON, showVisualization); editor.commit(); } public synchronized DownloadFile forSong(MusicDirectory.Entry song) { DownloadFile returnFile = null; for (DownloadFile downloadFile : downloadList) { if (downloadFile.getSong().equals(song)) { if(((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && downloadFile.getPartialFile().exists()) || downloadFile.isWorkDone())) { // If downloading, return immediately return downloadFile; } else { // Otherwise, check to make sure there isn't a background download going on first returnFile = downloadFile; } } } for (DownloadFile downloadFile : backgroundDownloadList) { if (downloadFile.getSong().equals(song)) { return downloadFile; } } if(returnFile != null) { return returnFile; } DownloadFile downloadFile = downloadFileCache.get(song); if (downloadFile == null) { downloadFile = new DownloadFile(this, song, false); downloadFileCache.put(song, downloadFile); } return downloadFile; } public synchronized void clear() { clear(true); } public synchronized void clearBackground() { if(currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) { currentDownloading.cancelDownload(); currentDownloading = null; } backgroundDownloadList.clear(); Util.hideDownloadingNotification(this); } public synchronized void clearIncomplete() { reset(); Iterator iterator = downloadList.iterator(); while (iterator.hasNext()) { DownloadFile downloadFile = iterator.next(); if (!downloadFile.isCompleteFileAvailable()) { iterator.remove(); } } lifecycleSupport.serializeDownloadQueue(); updateJukeboxPlaylist(); } public synchronized void setOnline(boolean online) { if(online) { mediaRouter.addOfflineProviders(); } else { mediaRouter.removeOfflineProviders(); clearIncomplete(); } } public synchronized int size() { return downloadList.size(); } public synchronized void clear(boolean serialize) { // Delete podcast if fully listened to if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) { int duration = getPlayerDuration(); // Make sure > 90% of the way through int cutoffPoint = (int)(duration * 0.90); if(duration > 0 && cachedPosition > cutoffPoint) { currentPlaying.delete(); } } for(DownloadFile podcast: toDelete) { podcast.delete(); } toDelete.clear(); reset(); downloadList.clear(); revision++; if (currentDownloading != null) { currentDownloading.cancelDownload(); currentDownloading = null; } setCurrentPlaying(null, false); if (serialize) { lifecycleSupport.serializeDownloadQueue(); } updateJukeboxPlaylist(); setNextPlaying(); if(proxy != null) { proxy.stop(); proxy = null; } suggestedPlaylistName = null; suggestedPlaylistId = null; } public synchronized void remove(int which) { downloadList.remove(which); currentPlayingIndex = downloadList.indexOf(currentPlaying); } public synchronized void remove(DownloadFile downloadFile) { if (downloadFile == currentDownloading) { currentDownloading.cancelDownload(); currentDownloading = null; } if (downloadFile == currentPlaying) { reset(); setCurrentPlaying(null, false); } downloadList.remove(downloadFile); currentPlayingIndex = downloadList.indexOf(currentPlaying); backgroundDownloadList.remove(downloadFile); revision++; lifecycleSupport.serializeDownloadQueue(); updateJukeboxPlaylist(); if(downloadFile == nextPlaying) { setNextPlaying(); } } public synchronized void delete(List songs) { for (MusicDirectory.Entry song : songs) { forSong(song).delete(); } } 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) { if(this.currentPlaying != null) { this.currentPlaying.setPlaying(false); } this.currentPlaying = currentPlaying; if(currentPlaying == null) { currentPlayingIndex = -1; } else { currentPlayingIndex = downloadList.indexOf(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(DownloadService.this); boolean gaplessPlayback = prefs.getBoolean(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK, true); if(!gaplessPlayback) { nextPlaying = null; nextPlayerState = IDLE; return; } setNextPlayerState(IDLE); int index = getNextPlayingIndex(); 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; } } public int getCurrentPlayingIndex() { return currentPlayingIndex; } private int getNextPlayingIndex() { 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; } } return index; } public DownloadFile getCurrentPlaying() { return currentPlaying; } public DownloadFile getCurrentDownloading() { return currentDownloading; } public List getSongs() { return downloadList; } public List getToDelete() { return toDelete; } public synchronized List getDownloads() { List temp = new ArrayList(); temp.addAll(downloadList); temp.addAll(backgroundDownloadList); return temp; } public List 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); } } public synchronized void play(int index) { play(index, true); } private synchronized void play(int index, boolean start) { play(index, start, 0); } private synchronized void play(int index, boolean start, int position) { if (index < 0 || index >= size()) { reset(); setCurrentPlaying(null, false); lifecycleSupport.serializeDownloadQueue(); } else { if(nextPlayingTask != null) { nextPlayingTask.cancel(); nextPlayingTask = null; } setCurrentPlaying(index, start); if (start && remoteState != RemoteControlState.LOCAL) { remoteController.changeTrack(index, downloadList.get(index)); setPlayerState(STARTED); } if (remoteState == RemoteControlState.LOCAL) { bufferAndPlay(position, start); 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 if(!nextMediaPlayer.isPlaying()) { Log.w(TAG, "nextSetup lied about it's state!"); 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(); } } public synchronized void seekTo(int position) { if(position < 0) { position = 0; } try { if (remoteState != RemoteControlState.LOCAL) { remoteController.changePosition(position / 1000); } else { mediaPlayer.seekTo(position); cachedPosition = position; } } catch (Exception x) { handleError(x); } } public synchronized void previous() { int index = getCurrentPlayingIndex(); if (index == -1) { return; } // If only one song, just skip within song if(size() == 1) { seekTo(getPlayerPosition() - REWIND); return; } // Restart song if played more than five seconds. if (getPlayerPosition() > 5000 || (index == 0 && getRepeatMode() != RepeatMode.ALL)) { play(index); } else { if(index == 0) { index = size(); } play(index - 1); } } public synchronized void next() { // If only one song, just skip within song if(size() == 1) { seekTo(getPlayerPosition() + FAST_FORWARD); return; } // Delete podcast if fully listened to if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) { int duration = getPlayerDuration(); // Make sure > 90% of the way through int cutoffPoint = (int)(duration * 0.90); if(duration > 0 && cachedPosition > cutoffPoint) { toDelete.add(currentPlaying); } } int index = getCurrentPlayingIndex(); int nextPlayingIndex = getNextPlayingIndex(); // Make sure to actually go to next when repeat song is on if(index == nextPlayingIndex) { nextPlayingIndex++; } if (index != -1 && nextPlayingIndex < size()) { play(nextPlayingIndex); } } private void onSongCompleted() { play(getNextPlayingIndex()); } public synchronized void pause() { try { if (playerState == STARTED) { if (remoteState != RemoteControlState.LOCAL) { remoteController.stop(); } else { mediaPlayer.pause(); } setPlayerState(PAUSED); } } catch (Exception x) { handleError(x); } } public synchronized void stop() { try { if (playerState == STARTED) { if (remoteState != RemoteControlState.LOCAL) { remoteController.stop(); } else { mediaPlayer.pause(); } setPlayerState(STOPPED); } else if(playerState == PAUSED) { setPlayerState(STOPPED); } } catch(Exception x) { handleError(x); } } public synchronized void start() { try { if (remoteState != RemoteControlState.LOCAL) { remoteController.start(); } else { mediaPlayer.start(); } setPlayerState(STARTED); } catch (Exception x) { handleError(x); } } public synchronized void reset() { if (bufferTask != null) { bufferTask.cancel(); } try { // Only set to idle if it's not being killed to start RemoteController if(remoteState == RemoteControlState.LOCAL) { setPlayerState(IDLE); } mediaPlayer.setOnErrorListener(null); mediaPlayer.setOnCompletionListener(null); mediaPlayer.reset(); } catch (Exception x) { handleError(x); } } public int getPlayerPosition() { try { if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) { return 0; } if (remoteState != RemoteControlState.LOCAL) { return remoteController.getRemotePosition() * 1000; } else { return cachedPosition; } } catch (Exception x) { handleError(x); return 0; } } 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; } 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, (currentPlaying != null) ? currentPlaying.getSong() : null, 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, "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(1000L); } 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; } public void setSuggestedPlaylistName(String name, String id) { this.suggestedPlaylistName = name; this.suggestedPlaylistId = id; } public String getSuggestedPlaylistName() { return suggestedPlaylistName; } public String getSuggestedPlaylistId() { return suggestedPlaylistId; } public boolean getEqualizerAvailable() { return equalizerAvailable; } public boolean getVisualizerAvailable() { return visualizerAvailable; } public EqualizerController getEqualizerController() { if (equalizerAvailable && equalizerController == null) { equalizerController = new EqualizerController(this, mediaPlayer); if (!equalizerController.isAvailable()) { equalizerController = null; } else { equalizerController.loadSettings(); } } return equalizerController; } public VisualizerController getVisualizerController() { if (visualizerAvailable && visualizerController == null) { visualizerController = new VisualizerController(this, mediaPlayer); if (!visualizerController.isAvailable()) { visualizerController = null; } } return visualizerController; } public MediaRouteSelector getRemoteSelector() { return mediaRouter.getSelector(); } public boolean isRemoteEnabled() { return remoteState != RemoteControlState.LOCAL; } public void setRemoteEnabled(RemoteControlState newState) { if(instance != null) { setRemoteEnabled(newState, null); } } public void setRemoteEnabled(RemoteControlState newState, Object ref) { setRemoteState(newState, ref); RouteInfo info = mediaRouter.getSelectedRoute(); String routeId = info.getId(); SharedPreferences.Editor editor = Util.getPreferences(this).edit(); editor.putInt(Constants.PREFERENCES_KEY_CONTROL_MODE, newState.getValue()); editor.putString(Constants.PREFERENCES_KEY_CONTROL_ID, routeId); editor.commit(); } private void setRemoteState(RemoteControlState newState, Object ref) { setRemoteState(newState, ref, null); } private void setRemoteState(final RemoteControlState newState, final Object ref, final String routeId) { boolean isPlaying = playerState == STARTED; int position = getPlayerPosition(); if(remoteController != null) { remoteController.stop(); setPlayerState(PlayerState.IDLE); remoteController.shutdown(); remoteController = null; } remoteState = newState; switch(newState) { case JUKEBOX_SERVER: remoteController = new JukeboxController(this, handler); break; case CHROMECAST: if(ref == null) { remoteState = RemoteControlState.LOCAL; break; } remoteController = (RemoteController) ref; break; case LOCAL: default: break; } if(remoteController != null) { remoteController.create(isPlaying, position / 1000); } else { play(getCurrentPlayingIndex(), isPlaying, position); } if (remoteState != RemoteControlState.LOCAL) { reset(); // Cancel current download, if necessary. if (currentDownloading != null) { currentDownloading.cancelDownload(); } // Cancels current setup tasks if(bufferTask != null && bufferTask.isRunning()) { bufferTask.cancel(); bufferTask = null; } if(nextPlayingTask != null && nextPlayingTask.isRunning()) { nextPlayingTask.cancel(); nextPlayingTask = null; } } SharedPreferences prefs = Util.getPreferences(this); if(currentPlaying != null && prefs.getBoolean(Constants.PREFERENCES_KEY_PERSISTENT_NOTIFICATION, false)) { Util.showPlayingNotification(this, this, handler, currentPlaying.getSong()); } else { Util.hidePlayingNotification(this, this, handler); } if(routeId != null) { handler.post(new Runnable() { @Override public void run() { RouteInfo info = mediaRouter.getRouteForId(routeId); if(info == null) { setRemoteState(RemoteControlState.LOCAL, null); } else if(newState == RemoteControlState.CHROMECAST) { RemoteController controller = mediaRouter.getRemoteController(info); if(controller != null) { setRemoteState(RemoteControlState.CHROMECAST, controller); } } } }); } } public void setRemoteVolume(boolean up) { remoteController.setVolume(up); } public void startRemoteScan() { mediaRouter.startScan(); } public void stopRemoteScan() { mediaRouter.stopScan(); } private synchronized void bufferAndPlay() { bufferAndPlay(0); } private synchronized void bufferAndPlay(int position) { bufferAndPlay(position, true); } private synchronized void bufferAndPlay(int position, boolean start) { if(playerState != PREPARED) { reset(); bufferTask = new BufferTask(currentPlaying, position, start); bufferTask.start(); } else { doPlay(currentPlaying, position, start); } } 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 BufferProxy(this); proxy.start(); } proxy.setBufferFile(downloadFile); dataSource = proxy.getPrivateAddress(dataSource); 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 (DownloadService.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); } } // Only call when starting, setPlayerState(PAUSED) already calls this if(start) { 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.setOnErrorListener(null); nextMediaPlayer.reset(); nextMediaPlayer.release(); nextMediaPlayer = null; } // Exit when using remote controllers if(remoteState != RemoteControlState.LOCAL) { return; } nextMediaPlayer = new MediaPlayer(); nextMediaPlayer.setWakeMode(DownloadService.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(); // Finished loading, delete when list is cleared if(downloadFile.getSong() instanceof PodcastEpisode) { toDelete.add(downloadFile); } } else { // If file is not completely downloaded, restart the playback from the current position. synchronized (DownloadService.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, true); bufferTask.start(); } } } } }); } public void setSleepTimerDuration(int duration){ timerDuration = duration; } 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); } public void stopSleepTimer() { if(sleepTimer != null){ sleepTimer.cancel(); sleepTimer.purge(); } sleepTimer = null; } public boolean getSleepTimer() { return sleepTimer != null; } public void setVolume(float volume) { if(mediaPlayer != null) { mediaPlayer.setVolume(volume, volume); } } public synchronized void swap(boolean mainList, int from, int to) { List 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(remoteState != RemoteControlState.LOCAL && 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); if(mediaPlayer != null) { 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 (remoteState != RemoteControlState.LOCAL || !Util.isNetworkConnected(this) || Util.isOffline(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() && !downloadFile.isFailedMax()) { 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 { if(!downloadFile.isFailedMax()) { currentDownloading = downloadFile; currentDownloading.download(); cleanupCandidates.add(currentDownloading); break; } } } } } if(!backgroundDownloadList.isEmpty()) { DownloadFile speedFile = null; // Updating existing notification if(downloadOngoing) { // Changing download, use speed of last DownloadFile if(revision != downloadRevision && lastDownloaded != null) { speedFile = lastDownloaded; } else { // Updated mid-download speedFile = currentDownloading; } } Util.showDownloadingNotification(this, currentDownloading, backgroundDownloadList.size(), speedFile); downloadRevision = revision; lastDownloaded = currentDownloading; downloadOngoing = true; } else if(backgroundDownloadList.isEmpty() && downloadOngoing) { Util.hideDownloadingNotification(this); downloadOngoing = false; lastDownloaded = null; } // 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++; } } currentPlayingIndex = downloadList.indexOf(currentPlaying); 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 final DownloadFile downloadFile; private final int position; private final long expectedFileSize; private final File partialFile; private final boolean start; public BufferTask(DownloadFile downloadFile, int position, boolean start) { this.downloadFile = downloadFile; this.position = position; partialFile = downloadFile.getPartialFile(); this.start = start; // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. int bitRate = downloadFile.getBitRate(); long byteCount = Math.max(100000, bitRate * 1024L / 8L * 5L); // 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, start); } 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) { 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 + ")"; } } }