2257 lines
80 KiB
Java
2257 lines
80 KiB
Java
/*
|
|
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 net.nullsum.audinaut.service;
|
|
|
|
import static net.nullsum.audinaut.domain.PlayerState.COMPLETED;
|
|
import static net.nullsum.audinaut.domain.PlayerState.DOWNLOADING;
|
|
import static net.nullsum.audinaut.domain.PlayerState.IDLE;
|
|
import static net.nullsum.audinaut.domain.PlayerState.PAUSED;
|
|
import static net.nullsum.audinaut.domain.PlayerState.PAUSED_TEMP;
|
|
import static net.nullsum.audinaut.domain.PlayerState.PREPARED;
|
|
import static net.nullsum.audinaut.domain.PlayerState.PREPARING;
|
|
import static net.nullsum.audinaut.domain.PlayerState.STARTED;
|
|
import static net.nullsum.audinaut.domain.PlayerState.STOPPED;
|
|
|
|
import net.nullsum.audinaut.R;
|
|
import net.nullsum.audinaut.activity.SubsonicActivity;
|
|
import net.nullsum.audinaut.audiofx.AudioEffectsController;
|
|
import net.nullsum.audinaut.audiofx.EqualizerController;
|
|
import net.nullsum.audinaut.domain.MusicDirectory;
|
|
import net.nullsum.audinaut.domain.PlayerState;
|
|
import net.nullsum.audinaut.domain.RepeatMode;
|
|
import net.nullsum.audinaut.receiver.AudioNoisyReceiver;
|
|
import net.nullsum.audinaut.receiver.MediaButtonIntentReceiver;
|
|
import net.nullsum.audinaut.util.BufferProxy;
|
|
import net.nullsum.audinaut.util.Constants;
|
|
import net.nullsum.audinaut.util.ImageLoader;
|
|
import net.nullsum.audinaut.util.Notifications;
|
|
import net.nullsum.audinaut.util.ShufflePlayBuffer;
|
|
import net.nullsum.audinaut.util.SilentBackgroundTask;
|
|
import net.nullsum.audinaut.util.SimpleServiceBinder;
|
|
import net.nullsum.audinaut.util.UpdateHelper;
|
|
import net.nullsum.audinaut.util.Util;
|
|
import net.nullsum.audinaut.util.tags.BastpUtil;
|
|
import net.nullsum.audinaut.view.UpdateView;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
|
|
import android.app.Service;
|
|
import android.content.ComponentCallbacks2;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.content.SharedPreferences;
|
|
import android.media.AudioManager;
|
|
import android.media.MediaPlayer;
|
|
import android.media.PlaybackParams;
|
|
import android.media.audiofx.AudioEffect;
|
|
import android.net.wifi.WifiManager;
|
|
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 android.view.KeyEvent;
|
|
|
|
/**
|
|
* @author Sindre Mehus
|
|
* @version $Id$
|
|
*/
|
|
public class DownloadService extends Service {
|
|
private static final String TAG = DownloadService.class.getSimpleName();
|
|
|
|
public static final String CMD_PLAY = "net.nullsum.audinaut.CMD_PLAY";
|
|
public static final String CMD_TOGGLEPAUSE = "net.nullsum.audinaut.CMD_TOGGLEPAUSE";
|
|
public static final String CMD_PAUSE = "net.nullsum.audinaut.CMD_PAUSE";
|
|
public static final String CMD_STOP = "net.nullsum.audinaut.CMD_STOP";
|
|
public static final String CMD_PREVIOUS = "net.nullsum.audinaut.CMD_PREVIOUS";
|
|
public static final String CMD_NEXT = "net.nullsum.audinaut.CMD_NEXT";
|
|
public static final String CANCEL_DOWNLOADS = "net.nullsum.audinaut.CANCEL_DOWNLOADS";
|
|
public static final String START_PLAY = "net.nullsum.audinaut.START_PLAYING";
|
|
public static final int FAST_FORWARD = 30000;
|
|
public static final int REWIND = 10000;
|
|
private static final long DEFAULT_DELAY_UPDATE_PROGRESS = 1000L;
|
|
private static final double DELETE_CUTOFF = 0.84;
|
|
private static final int REQUIRED_ALBUM_MATCHES = 4;
|
|
private static final int REMOTE_PLAYLIST_TOTAL = 3;
|
|
private static final int SHUFFLE_MODE_NONE = 0;
|
|
private static final int SHUFFLE_MODE_ALL = 1;
|
|
private static final int SHUFFLE_MODE_ARTIST = 2;
|
|
|
|
public static final int METADATA_UPDATED_ALL = 0;
|
|
public static final int METADATA_UPDATED_STAR = 1;
|
|
public static final int METADATA_UPDATED_COVER_ART = 8;
|
|
|
|
private final IBinder binder = new SimpleServiceBinder<>(this);
|
|
private Looper mediaPlayerLooper;
|
|
private MediaPlayer mediaPlayer;
|
|
private MediaPlayer nextMediaPlayer;
|
|
private int audioSessionId;
|
|
private boolean nextSetup = false;
|
|
private final List<DownloadFile> downloadList = new ArrayList<DownloadFile>();
|
|
private final List<DownloadFile> backgroundDownloadList = new ArrayList<DownloadFile>();
|
|
private final List<DownloadFile> toDelete = new ArrayList<DownloadFile>();
|
|
private final Handler handler = new Handler();
|
|
private Handler mediaPlayerHandler;
|
|
private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this);
|
|
private ShufflePlayBuffer shufflePlayBuffer;
|
|
|
|
private final LruCache<MusicDirectory.Entry, DownloadFile> downloadFileCache = new LruCache<MusicDirectory.Entry, DownloadFile>(100);
|
|
private final List<DownloadFile> cleanupCandidates = new ArrayList<DownloadFile>();
|
|
private DownloadFile currentPlaying;
|
|
private int currentPlayingIndex = -1;
|
|
private DownloadFile nextPlaying;
|
|
private DownloadFile currentDownloading;
|
|
private SilentBackgroundTask bufferTask;
|
|
private SilentBackgroundTask nextPlayingTask;
|
|
private PlayerState playerState = IDLE;
|
|
private PlayerState nextPlayerState = IDLE;
|
|
private boolean removePlayed;
|
|
private boolean shufflePlay;
|
|
private final List<OnSongChangedListener> onSongChangedListeners = new ArrayList<>();
|
|
private long revision;
|
|
private static DownloadService instance;
|
|
private String suggestedPlaylistName;
|
|
private String suggestedPlaylistId;
|
|
private PowerManager.WakeLock wakeLock;
|
|
private WifiManager.WifiLock wifiLock;
|
|
private boolean keepScreenOn;
|
|
private int cachedPosition = 0;
|
|
private boolean downloadOngoing = false;
|
|
private float volume = 1.0f;
|
|
private long delayUpdateProgress = DEFAULT_DELAY_UPDATE_PROGRESS;
|
|
|
|
private AudioEffectsController effectsController;
|
|
private PositionCache positionCache;
|
|
private BufferProxy proxy;
|
|
|
|
private boolean autoPlayStart = false;
|
|
private boolean runListenersOnInit = false;
|
|
|
|
// Variables to manage getCurrentPosition sometimes starting from an arbitrary non-zero number
|
|
private long subtractNextPosition = 0;
|
|
private int subtractPosition = 0;
|
|
|
|
private AudioNoisyReceiver audioNoisyReceiver = new AudioNoisyReceiver();
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
super.onCreate();
|
|
|
|
final SharedPreferences prefs = Util.getPreferences(this);
|
|
new Thread(new Runnable() {
|
|
public void run() {
|
|
Looper.prepare();
|
|
|
|
mediaPlayer = new MediaPlayer();
|
|
mediaPlayer.setWakeMode(DownloadService.this, PowerManager.PARTIAL_WAKE_LOCK);
|
|
|
|
audioSessionId = -1;
|
|
Integer id = prefs.getInt(Constants.CACHE_AUDIO_SESSION_ID, -1);
|
|
if(id != -1) {
|
|
try {
|
|
audioSessionId = id;
|
|
mediaPlayer.setAudioSessionId(audioSessionId);
|
|
} catch (Throwable e) {
|
|
audioSessionId = -1;
|
|
}
|
|
}
|
|
|
|
if(audioSessionId == -1) {
|
|
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
|
audioSessionId = mediaPlayer.getAudioSessionId();
|
|
prefs.edit().putInt(Constants.CACHE_AUDIO_SESSION_ID, audioSessionId).apply();
|
|
}
|
|
|
|
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
|
|
@Override
|
|
public boolean onError(MediaPlayer mediaPlayer, int what, int more) {
|
|
handleError(new Exception("MediaPlayer error: " + what + " (" + more + ")"));
|
|
return false;
|
|
}
|
|
});
|
|
|
|
effectsController = new AudioEffectsController(DownloadService.this, audioSessionId);
|
|
if(prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false)) {
|
|
getEqualizerController();
|
|
}
|
|
|
|
mediaPlayerLooper = Looper.myLooper();
|
|
mediaPlayerHandler = new Handler(mediaPlayerLooper);
|
|
|
|
if(runListenersOnInit) {
|
|
onSongsChanged();
|
|
onSongProgress();
|
|
onStateUpdate();
|
|
}
|
|
|
|
Looper.loop();
|
|
}
|
|
}, "DownloadService").start();
|
|
|
|
Util.registerMediaButtonEventReceiver(this);
|
|
|
|
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
|
|
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
|
|
wakeLock.setReferenceCounted(false);
|
|
|
|
WifiManager wifiManager = (WifiManager) this.getSystemService(Context.WIFI_SERVICE);
|
|
wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "downloadServiceLock");
|
|
|
|
keepScreenOn = prefs.getBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, false);
|
|
|
|
instance = this;
|
|
shufflePlayBuffer = new ShufflePlayBuffer(this);
|
|
lifecycleSupport.onCreate();
|
|
|
|
IntentFilter filter = new IntentFilter();
|
|
filter.addAction("android.media.AUDIO_BECOMING_NOISY");
|
|
registerReceiver(audioNoisyReceiver, filter);
|
|
}
|
|
|
|
@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 onTrimMemory(int level) {
|
|
ImageLoader imageLoader = SubsonicActivity.getStaticImageLoader(this);
|
|
if(imageLoader != null) {
|
|
Log.i(TAG, "Memory Trim Level: " + level);
|
|
if (level < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
|
|
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
|
|
imageLoader.onLowMemory(0.75f);
|
|
} else if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
|
|
imageLoader.onLowMemory(0.50f);
|
|
} else if(level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE) {
|
|
imageLoader.onLowMemory(0.25f);
|
|
}
|
|
} else if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
|
|
imageLoader.onLowMemory(0.25f);
|
|
} else if(level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
|
|
imageLoader.onLowMemory(0.75f);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
instance = null;
|
|
|
|
if(currentPlaying != null) currentPlaying.setPlaying(false);
|
|
lifecycleSupport.onDestroy();
|
|
|
|
Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
|
|
i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
|
|
i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
|
|
sendBroadcast(i);
|
|
|
|
mediaPlayer.release();
|
|
if(nextMediaPlayer != null) {
|
|
nextMediaPlayer.release();
|
|
}
|
|
mediaPlayerLooper.quit();
|
|
shufflePlayBuffer.shutdown();
|
|
effectsController.release();
|
|
|
|
if(bufferTask != null) {
|
|
bufferTask.cancel();
|
|
bufferTask = null;
|
|
}
|
|
if(nextPlayingTask != null) {
|
|
nextPlayingTask.cancel();
|
|
nextPlayingTask = null;
|
|
}
|
|
if(proxy != null) {
|
|
proxy.stop();
|
|
proxy = null;
|
|
}
|
|
Notifications.hidePlayingNotification(this, this, handler);
|
|
Notifications.hideDownloadingNotification(this, this, handler);
|
|
|
|
unregisterReceiver(audioNoisyReceiver);
|
|
}
|
|
|
|
public static DownloadService getInstance() {
|
|
return instance;
|
|
}
|
|
|
|
@Override
|
|
public IBinder onBind(Intent intent) {
|
|
return binder;
|
|
}
|
|
|
|
public void post(Runnable r) {
|
|
handler.post(r);
|
|
}
|
|
public void postDelayed(Runnable r, long millis) {
|
|
handler.postDelayed(r, millis);
|
|
}
|
|
|
|
public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) {
|
|
download(songs, save, autoplay, playNext, shuffle, 0, 0);
|
|
}
|
|
public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, int start, int position) {
|
|
setShufflePlayEnabled(false);
|
|
int offset = 1;
|
|
boolean noNetwork = !Util.isOffline(this) && !Util.isNetworkConnected(this);
|
|
boolean warnNetwork = false;
|
|
|
|
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);
|
|
addToDownloadList(downloadFile, getCurrentPlayingIndex() + offset);
|
|
if(noNetwork && !warnNetwork) {
|
|
if(!downloadFile.isCompleteFileAvailable()) {
|
|
warnNetwork = true;
|
|
}
|
|
}
|
|
offset++;
|
|
}
|
|
}
|
|
|
|
setNextPlaying();
|
|
} else {
|
|
int size = size();
|
|
int index = getCurrentPlayingIndex();
|
|
for (MusicDirectory.Entry song : songs) {
|
|
if(song == null) {
|
|
continue;
|
|
}
|
|
|
|
DownloadFile downloadFile = new DownloadFile(this, song, save);
|
|
addToDownloadList(downloadFile, -1);
|
|
if(noNetwork && !warnNetwork) {
|
|
if(!downloadFile.isCompleteFileAvailable()) {
|
|
warnNetwork = true;
|
|
}
|
|
}
|
|
}
|
|
if(!autoplay && (size - 1) == index) {
|
|
setNextPlaying();
|
|
}
|
|
}
|
|
revision++;
|
|
onSongsChanged();
|
|
updateRemotePlaylist();
|
|
|
|
if(shuffle) {
|
|
shuffle();
|
|
}
|
|
if(warnNetwork) {
|
|
Util.toast(this, R.string.select_album_no_network);
|
|
}
|
|
|
|
if (autoplay) {
|
|
play(start, true, position);
|
|
} else if(start != 0 || position != 0) {
|
|
play(start, false, position);
|
|
} else {
|
|
if (currentPlaying == null) {
|
|
currentPlaying = downloadList.get(0);
|
|
currentPlayingIndex = 0;
|
|
currentPlaying.setPlaying(true);
|
|
} else {
|
|
currentPlayingIndex = downloadList.indexOf(currentPlaying);
|
|
}
|
|
checkDownloads();
|
|
}
|
|
lifecycleSupport.serializeDownloadQueue();
|
|
}
|
|
private void addToDownloadList(DownloadFile file, int offset) {
|
|
if(offset == -1) {
|
|
downloadList.add(file);
|
|
} else {
|
|
downloadList.add(offset, file);
|
|
}
|
|
}
|
|
public synchronized void downloadBackground(List<MusicDirectory.Entry> 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);
|
|
} else if(downloadFile.isSaved() && !save) {
|
|
// Quickly unpin song instead of adding it to work to be done
|
|
downloadFile.unpin();
|
|
}
|
|
}
|
|
revision++;
|
|
|
|
if(!Util.isOffline(this) && !Util.isNetworkConnected(this)) {
|
|
Util.toast(this, R.string.select_album_no_network);
|
|
}
|
|
|
|
checkDownloads();
|
|
lifecycleSupport.serializeDownloadQueue();
|
|
}
|
|
|
|
private synchronized void updateRemotePlaylist() {
|
|
List<DownloadFile> playlist = new ArrayList<>();
|
|
if(currentPlaying != null) {
|
|
int index = downloadList.indexOf(currentPlaying);
|
|
if(index == -1) {
|
|
index = 0;
|
|
}
|
|
|
|
int size = size();
|
|
int end = index + REMOTE_PLAYLIST_TOTAL;
|
|
for(int i = index; i < size && i < end; i++) {
|
|
playlist.add(downloadList.get(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
public synchronized void restore(List<MusicDirectory.Entry> songs, List<MusicDirectory.Entry> toDelete, int currentPlayingIndex, int currentPlayingPosition) {
|
|
SharedPreferences prefs = Util.getPreferences(this);
|
|
if(prefs.getBoolean(Constants.PREFERENCES_KEY_REMOVE_PLAYED, false)) {
|
|
removePlayed = true;
|
|
}
|
|
int startShufflePlay = prefs.getInt(Constants.PREFERENCES_KEY_SHUFFLE_MODE, SHUFFLE_MODE_NONE);
|
|
download(songs, false, false, false, false);
|
|
if(startShufflePlay != SHUFFLE_MODE_NONE) {
|
|
if(startShufflePlay == SHUFFLE_MODE_ALL) {
|
|
shufflePlay = true;
|
|
}
|
|
SharedPreferences.Editor editor = prefs.edit();
|
|
editor.putInt(Constants.PREFERENCES_KEY_SHUFFLE_MODE, startShufflePlay);
|
|
editor.apply();
|
|
}
|
|
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));
|
|
}
|
|
}
|
|
|
|
suggestedPlaylistName = prefs.getString(Constants.PREFERENCES_KEY_PLAYLIST_NAME, null);
|
|
suggestedPlaylistId = prefs.getString(Constants.PREFERENCES_KEY_PLAYLIST_ID, null);
|
|
}
|
|
|
|
public boolean isInitialized() {
|
|
return lifecycleSupport != null && lifecycleSupport.isInitialized();
|
|
}
|
|
|
|
public synchronized Date getLastStateChanged() {
|
|
return lifecycleSupport.getLastChange();
|
|
}
|
|
|
|
public synchronized void setRemovePlayed(boolean enabled) {
|
|
removePlayed = enabled;
|
|
if(removePlayed) {
|
|
checkDownloads();
|
|
lifecycleSupport.serializeDownloadQueue();
|
|
}
|
|
SharedPreferences.Editor editor = Util.getPreferences(this).edit();
|
|
editor.putBoolean(Constants.PREFERENCES_KEY_REMOVE_PLAYED, enabled);
|
|
editor.apply();
|
|
}
|
|
public boolean isRemovePlayed() {
|
|
return removePlayed;
|
|
}
|
|
|
|
public synchronized void setShufflePlayEnabled(boolean enabled) {
|
|
shufflePlay = enabled;
|
|
if (shufflePlay) {
|
|
checkDownloads();
|
|
}
|
|
SharedPreferences.Editor editor = Util.getPreferences(this).edit();
|
|
editor.putInt(Constants.PREFERENCES_KEY_SHUFFLE_MODE, enabled ? SHUFFLE_MODE_ALL : SHUFFLE_MODE_NONE);
|
|
editor.apply();
|
|
}
|
|
|
|
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++;
|
|
onSongsChanged();
|
|
lifecycleSupport.serializeDownloadQueue();
|
|
updateRemotePlaylist();
|
|
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.apply();
|
|
}
|
|
|
|
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 clearBackground() {
|
|
if(currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) {
|
|
currentDownloading.cancelDownload();
|
|
currentDownloading = null;
|
|
}
|
|
backgroundDownloadList.clear();
|
|
revision++;
|
|
Notifications.hideDownloadingNotification(this, this, handler);
|
|
}
|
|
|
|
public synchronized void clearIncomplete() {
|
|
Iterator<DownloadFile> iterator = downloadList.iterator();
|
|
while (iterator.hasNext()) {
|
|
DownloadFile downloadFile = iterator.next();
|
|
if (!downloadFile.isCompleteFileAvailable()) {
|
|
iterator.remove();
|
|
|
|
// Reset if the current playing song has been removed
|
|
if(currentPlaying == downloadFile) {
|
|
reset();
|
|
}
|
|
|
|
currentPlayingIndex = downloadList.indexOf(currentPlaying);
|
|
}
|
|
}
|
|
lifecycleSupport.serializeDownloadQueue();
|
|
updateRemotePlaylist();
|
|
onSongsChanged();
|
|
}
|
|
|
|
public void setOnline(final boolean online) {
|
|
if(shufflePlay) {
|
|
setShufflePlayEnabled(false);
|
|
}
|
|
|
|
lifecycleSupport.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (online) {
|
|
checkDownloads();
|
|
} else {
|
|
clearIncomplete();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
public synchronized int size() {
|
|
return downloadList.size();
|
|
}
|
|
|
|
public synchronized void clear() {
|
|
clear(true);
|
|
}
|
|
public synchronized void clear(boolean serialize) {
|
|
// Delete podcast if fully listened to
|
|
int position = getPlayerPosition();
|
|
int duration = getPlayerDuration();
|
|
boolean cutoff = isPastCutoff(position, duration, true);
|
|
for(DownloadFile podcast: toDelete) {
|
|
podcast.delete();
|
|
}
|
|
toDelete.clear();
|
|
|
|
reset();
|
|
downloadList.clear();
|
|
onSongsChanged();
|
|
if (currentDownloading != null && !backgroundDownloadList.contains(currentDownloading)) {
|
|
currentDownloading.cancelDownload();
|
|
currentDownloading = null;
|
|
}
|
|
setCurrentPlaying(null, false);
|
|
|
|
if (serialize) {
|
|
lifecycleSupport.serializeDownloadQueue();
|
|
}
|
|
updateRemotePlaylist();
|
|
setNextPlaying();
|
|
if(proxy != null) {
|
|
proxy.stop();
|
|
proxy = null;
|
|
}
|
|
|
|
suggestedPlaylistName = null;
|
|
suggestedPlaylistId = null;
|
|
|
|
setShufflePlayEnabled(false);
|
|
checkDownloads();
|
|
}
|
|
|
|
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++;
|
|
onSongsChanged();
|
|
lifecycleSupport.serializeDownloadQueue();
|
|
updateRemotePlaylist();
|
|
if(downloadFile == nextPlaying) {
|
|
setNextPlaying();
|
|
}
|
|
|
|
checkDownloads();
|
|
}
|
|
public synchronized void removeBackground(DownloadFile downloadFile) {
|
|
if (downloadFile == currentDownloading && downloadFile != currentPlaying && downloadFile != nextPlaying) {
|
|
currentDownloading.cancelDownload();
|
|
currentDownloading = null;
|
|
}
|
|
|
|
backgroundDownloadList.remove(downloadFile);
|
|
revision++;
|
|
checkDownloads();
|
|
}
|
|
|
|
public synchronized void delete(List<MusicDirectory.Entry> songs) {
|
|
for (MusicDirectory.Entry song : songs) {
|
|
forSong(song).delete();
|
|
}
|
|
}
|
|
|
|
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) {
|
|
currentPlayingIndex = -1;
|
|
setPlayerState(IDLE);
|
|
} else {
|
|
currentPlayingIndex = downloadList.indexOf(currentPlaying);
|
|
}
|
|
|
|
if (currentPlaying != null && currentPlaying.getSong() != null) {
|
|
Util.broadcastNewTrackInfo(this, currentPlaying.getSong());
|
|
} else {
|
|
Util.broadcastNewTrackInfo(this, null);
|
|
Notifications.hidePlayingNotification(this, this, handler);
|
|
}
|
|
onSongChanged();
|
|
}
|
|
|
|
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();
|
|
|
|
if(nextPlayingTask != null) {
|
|
nextPlayingTask.cancel();
|
|
nextPlayingTask = null;
|
|
}
|
|
resetNext();
|
|
|
|
if(index < size() && index != -1 && index != currentPlayingIndex) {
|
|
nextPlaying = downloadList.get(index);
|
|
|
|
nextPlayingTask = new CheckCompletionTask(nextPlaying);
|
|
nextPlayingTask.execute();
|
|
} else {
|
|
nextPlaying = null;
|
|
}
|
|
}
|
|
|
|
public int getCurrentPlayingIndex() {
|
|
return currentPlayingIndex;
|
|
}
|
|
private int getNextPlayingIndex() {
|
|
int index = getCurrentPlayingIndex();
|
|
if (index != -1) {
|
|
RepeatMode repeatMode = getRepeatMode();
|
|
switch (repeatMode) {
|
|
case OFF:
|
|
index = index + 1;
|
|
break;
|
|
case ALL:
|
|
index = (index + 1) % size();
|
|
break;
|
|
case SINGLE:
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
index = checkNextIndexValid(index, repeatMode);
|
|
}
|
|
return index;
|
|
}
|
|
private int checkNextIndexValid(int index, RepeatMode repeatMode) {
|
|
int startIndex = index;
|
|
int size = size();
|
|
if(index < size && index != -1) {
|
|
if(!Util.isAllowedToDownload(this)){
|
|
DownloadFile next = downloadList.get(index);
|
|
while(!next.isCompleteFileAvailable()) {
|
|
index++;
|
|
|
|
if (index >= size) {
|
|
if(repeatMode == RepeatMode.ALL) {
|
|
index = 0;
|
|
} else {
|
|
return -1;
|
|
}
|
|
} else if(index == startIndex) {
|
|
handler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
Util.toast(DownloadService.this, R.string.download_playerstate_mobile_disabled);
|
|
}
|
|
});
|
|
return -1;
|
|
}
|
|
|
|
next = downloadList.get(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
public DownloadFile getCurrentPlaying() {
|
|
return currentPlaying;
|
|
}
|
|
|
|
public DownloadFile getCurrentDownloading() {
|
|
return currentDownloading;
|
|
}
|
|
|
|
public DownloadFile getNextPlaying() {
|
|
return nextPlaying;
|
|
}
|
|
|
|
public List<DownloadFile> getSongs() {
|
|
return downloadList;
|
|
}
|
|
|
|
public List<DownloadFile> getToDelete() { return toDelete; }
|
|
|
|
public synchronized List<DownloadFile> getDownloads() {
|
|
List<DownloadFile> temp = new ArrayList<DownloadFile>();
|
|
temp.addAll(downloadList);
|
|
temp.addAll(backgroundDownloadList);
|
|
return temp;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
public synchronized void play(int index) {
|
|
play(index, true);
|
|
}
|
|
public synchronized void play(DownloadFile downloadFile) {
|
|
play(downloadList.indexOf(downloadFile));
|
|
}
|
|
private synchronized void play(int index, boolean start) {
|
|
play(index, start, 0);
|
|
}
|
|
private synchronized void play(int index, boolean start, int position) {
|
|
int size = this.size();
|
|
cachedPosition = 0;
|
|
if (index < 0 || index >= size) {
|
|
reset();
|
|
if(index >= size && size != 0) {
|
|
setCurrentPlaying(0, false);
|
|
Notifications.hidePlayingNotification(this, this, handler);
|
|
} else {
|
|
setCurrentPlaying(null, false);
|
|
}
|
|
lifecycleSupport.serializeDownloadQueue();
|
|
} else {
|
|
if(nextPlayingTask != null) {
|
|
nextPlayingTask.cancel();
|
|
nextPlayingTask = null;
|
|
}
|
|
setCurrentPlaying(index, start);
|
|
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) {
|
|
Util.broadcastPlaybackStatusChange(this, currentPlaying.getSong(), PlayerState.PREPARED);
|
|
|
|
// Swap the media players since nextMediaPlayer is ready to play
|
|
subtractPosition = 0;
|
|
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");
|
|
|
|
// Next time the cachedPosition is updated, use that as position 0
|
|
subtractNextPosition = System.currentTimeMillis();
|
|
}
|
|
MediaPlayer tmp = mediaPlayer;
|
|
mediaPlayer = nextMediaPlayer;
|
|
nextMediaPlayer = tmp;
|
|
setCurrentPlaying(nextPlaying, true);
|
|
setPlayerState(PlayerState.STARTED);
|
|
setupHandlers(currentPlaying, false, start);
|
|
setNextPlaying();
|
|
|
|
// Proxy should not be being used here since the next player was already setup to play
|
|
if(proxy != null) {
|
|
proxy.stop();
|
|
proxy = null;
|
|
}
|
|
checkDownloads();
|
|
updateRemotePlaylist();
|
|
}
|
|
|
|
/** 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(proxy != null && currentPlaying.isCompleteFileAvailable()) {
|
|
doPlay(currentPlaying, position, playerState == STARTED);
|
|
return;
|
|
}
|
|
|
|
mediaPlayer.seekTo(position);
|
|
subtractPosition = 0;
|
|
cachedPosition = position;
|
|
|
|
onSongProgress();
|
|
if(playerState == PAUSED) {
|
|
lifecycleSupport.serializeDownloadQueue();
|
|
}
|
|
} catch (Exception x) {
|
|
handleError(x);
|
|
}
|
|
}
|
|
public synchronized int rewind() {
|
|
return seekToWrapper(-REWIND);
|
|
}
|
|
public synchronized int fastForward() {
|
|
return seekToWrapper(FAST_FORWARD);
|
|
}
|
|
protected int seekToWrapper(int difference) {
|
|
int msPlayed = Math.max(0, getPlayerPosition());
|
|
Integer duration = getPlayerDuration();
|
|
int msTotal = duration == null ? 0 : duration;
|
|
|
|
int seekTo;
|
|
if(msPlayed + difference > msTotal) {
|
|
seekTo = msTotal;
|
|
} else {
|
|
seekTo = msPlayed + difference;
|
|
}
|
|
seekTo(seekTo);
|
|
|
|
return seekTo;
|
|
}
|
|
|
|
public synchronized void previous() {
|
|
int index = getCurrentPlayingIndex();
|
|
if (index == -1) {
|
|
return;
|
|
}
|
|
|
|
// If only one song, just skip within song
|
|
if(size() == 1 || (currentPlaying != null && !currentPlaying.isSong())) {
|
|
rewind();
|
|
return;
|
|
}
|
|
|
|
|
|
// Restart song if played more than five seconds.
|
|
if (getPlayerPosition() > 5000 || (index == 0 && getRepeatMode() != RepeatMode.ALL)) {
|
|
seekTo(0);
|
|
} else {
|
|
if(index == 0) {
|
|
index = size();
|
|
}
|
|
|
|
play(index - 1, playerState != PAUSED && playerState != STOPPED && playerState != IDLE);
|
|
}
|
|
}
|
|
|
|
public synchronized void next() {
|
|
next(false);
|
|
}
|
|
public synchronized void next(boolean forceCutoff) {
|
|
next(forceCutoff, false);
|
|
}
|
|
public synchronized void next(boolean forceCutoff, boolean forceStart) {
|
|
// If only one song, just skip within song
|
|
if(size() == 1 || (currentPlaying != null && !currentPlaying.isSong())) {
|
|
fastForward();
|
|
return;
|
|
} else if(playerState == PREPARING || playerState == PREPARED) {
|
|
return;
|
|
}
|
|
|
|
// Delete podcast if fully listened to
|
|
int position = getPlayerPosition();
|
|
int duration = getPlayerDuration();
|
|
boolean cutoff;
|
|
if(forceCutoff) {
|
|
cutoff = true;
|
|
} else {
|
|
cutoff = isPastCutoff(position, duration);
|
|
}
|
|
|
|
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, playerState != PAUSED && playerState != STOPPED && playerState != IDLE || forceStart);
|
|
}
|
|
}
|
|
|
|
public void onSongCompleted() {
|
|
setPlayerStateCompleted();
|
|
postPlayCleanup();
|
|
play(getNextPlayingIndex());
|
|
}
|
|
public void onNextStarted(DownloadFile nextPlaying) {
|
|
setPlayerStateCompleted();
|
|
postPlayCleanup();
|
|
setCurrentPlaying(nextPlaying, true);
|
|
setPlayerState(STARTED);
|
|
setNextPlayerState(IDLE);
|
|
}
|
|
|
|
public synchronized void pause() {
|
|
pause(false);
|
|
}
|
|
public synchronized void pause(boolean temp) {
|
|
try {
|
|
if (playerState == STARTED) {
|
|
mediaPlayer.pause();
|
|
setPlayerState(temp ? PAUSED_TEMP : PAUSED);
|
|
} else if(playerState == PAUSED_TEMP) {
|
|
setPlayerState(temp ? PAUSED_TEMP : PAUSED);
|
|
}
|
|
} catch (Exception x) {
|
|
handleError(x);
|
|
}
|
|
}
|
|
|
|
public synchronized void stop() {
|
|
try {
|
|
if (playerState == STARTED) {
|
|
mediaPlayer.pause();
|
|
setPlayerState(STOPPED);
|
|
} else if(playerState == PAUSED) {
|
|
setPlayerState(STOPPED);
|
|
}
|
|
} catch(Exception x) {
|
|
handleError(x);
|
|
}
|
|
}
|
|
|
|
public synchronized void start() {
|
|
try {
|
|
// Only start if done preparing
|
|
if(playerState != PREPARING) {
|
|
mediaPlayer.start();
|
|
} else {
|
|
// Otherwise, we need to set it up to start when done preparing
|
|
autoPlayStart = true;
|
|
}
|
|
setPlayerState(STARTED);
|
|
} catch (Exception x) {
|
|
handleError(x);
|
|
}
|
|
}
|
|
|
|
public synchronized void reset() {
|
|
if (bufferTask != null) {
|
|
bufferTask.cancel();
|
|
bufferTask = null;
|
|
}
|
|
try {
|
|
setPlayerState(IDLE);
|
|
mediaPlayer.setOnErrorListener(null);
|
|
mediaPlayer.setOnCompletionListener(null);
|
|
if(nextSetup) {
|
|
mediaPlayer.setNextMediaPlayer(null);
|
|
nextSetup = false;
|
|
}
|
|
mediaPlayer.reset();
|
|
subtractPosition = 0;
|
|
} catch (Exception x) {
|
|
handleError(x);
|
|
}
|
|
}
|
|
|
|
public synchronized void resetNext() {
|
|
try {
|
|
if (nextMediaPlayer != null) {
|
|
if (nextSetup) {
|
|
mediaPlayer.setNextMediaPlayer(null);
|
|
}
|
|
nextSetup = false;
|
|
|
|
nextMediaPlayer.setOnCompletionListener(null);
|
|
nextMediaPlayer.setOnErrorListener(null);
|
|
nextMediaPlayer.reset();
|
|
nextMediaPlayer.release();
|
|
nextMediaPlayer = null;
|
|
} else if(nextSetup) {
|
|
nextSetup = false;
|
|
}
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Failed to reset next media player");
|
|
}
|
|
}
|
|
|
|
public int getPlayerPosition() {
|
|
try {
|
|
if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) {
|
|
return 0;
|
|
}
|
|
return Math.max(0, cachedPosition - subtractPosition);
|
|
} catch (Exception x) {
|
|
handleError(x);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
public synchronized int getPlayerDuration() {
|
|
if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) {
|
|
int duration = 0;
|
|
try {
|
|
duration = mediaPlayer.getDuration();
|
|
} catch (Exception x) {
|
|
duration = 0;
|
|
}
|
|
|
|
if(duration != 0) {
|
|
return duration;
|
|
}
|
|
}
|
|
|
|
if (currentPlaying != null) {
|
|
Integer duration = currentPlaying.getSong().getDuration();
|
|
if (duration != null) {
|
|
return duration * 1000;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public PlayerState getPlayerState() {
|
|
return playerState;
|
|
}
|
|
|
|
public PlayerState getNextPlayerState() {
|
|
return nextPlayerState;
|
|
}
|
|
|
|
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) {
|
|
Notifications.showPlayingNotification(this, this, handler, currentPlaying.getSong());
|
|
} else if (pause) {
|
|
SharedPreferences prefs = Util.getPreferences(this);
|
|
if(prefs.getBoolean(Constants.PREFERENCES_KEY_PERSISTENT_NOTIFICATION, false)) {
|
|
Notifications.showPlayingNotification(this, this, handler, currentPlaying.getSong());
|
|
} else {
|
|
Notifications.hidePlayingNotification(this, this, handler);
|
|
}
|
|
} else if(hide) {
|
|
Notifications.hidePlayingNotification(this, this, handler);
|
|
}
|
|
if(playerState == STARTED && positionCache == null) {
|
|
positionCache = new LocalPositionCache();
|
|
Thread thread = new Thread(positionCache, "PositionCache");
|
|
thread.start();
|
|
} else if(playerState != STARTED && positionCache != null) {
|
|
positionCache.stop();
|
|
positionCache = null;
|
|
}
|
|
|
|
|
|
onStateUpdate();
|
|
}
|
|
|
|
public void setPlayerStateCompleted() {
|
|
// Acquire a temporary wakelock
|
|
acquireWakelock();
|
|
|
|
Log.i(TAG, this.playerState.name() + " -> " + PlayerState.COMPLETED + " (" + currentPlaying + ")");
|
|
this.playerState = PlayerState.COMPLETED;
|
|
if(positionCache != null) {
|
|
positionCache.stop();
|
|
positionCache = null;
|
|
}
|
|
|
|
onStateUpdate();
|
|
}
|
|
|
|
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 {
|
|
onSongProgress();
|
|
Thread.sleep(delayUpdateProgress);
|
|
}
|
|
catch(Exception e) {
|
|
isRunning = false;
|
|
positionCache = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
private class LocalPositionCache extends PositionCache {
|
|
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) {
|
|
int newPosition = mediaPlayer.getCurrentPosition();
|
|
|
|
// If sudden jump in position, something is wrong
|
|
if(subtractNextPosition == 0 && newPosition > (cachedPosition + 5000)) {
|
|
// Only 1 second should have gone by, subtract the rest
|
|
subtractPosition += (newPosition - cachedPosition) - 1000;
|
|
}
|
|
|
|
cachedPosition = newPosition;
|
|
|
|
if(subtractNextPosition > 0) {
|
|
// Subtraction amount is current position - how long ago onCompletionListener was called
|
|
subtractPosition = cachedPosition - (int) (System.currentTimeMillis() - subtractNextPosition);
|
|
if(subtractPosition < 0) {
|
|
subtractPosition = 0;
|
|
}
|
|
subtractNextPosition = 0;
|
|
}
|
|
}
|
|
onSongProgress(cachedPosition < 2000 ? true: false);
|
|
Thread.sleep(delayUpdateProgress);
|
|
}
|
|
catch(Exception e) {
|
|
Log.w(TAG, "Crashed getting current position", e);
|
|
isRunning = false;
|
|
positionCache = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public 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;
|
|
|
|
SharedPreferences.Editor editor = Util.getPreferences(this).edit();
|
|
editor.putString(Constants.PREFERENCES_KEY_PLAYLIST_NAME, name);
|
|
editor.putString(Constants.PREFERENCES_KEY_PLAYLIST_ID, id);
|
|
editor.apply();
|
|
}
|
|
|
|
public String getSuggestedPlaylistName() {
|
|
return suggestedPlaylistName;
|
|
}
|
|
|
|
public String getSuggestedPlaylistId() {
|
|
return suggestedPlaylistId;
|
|
}
|
|
|
|
public EqualizerController getEqualizerController() {
|
|
EqualizerController controller = null;
|
|
try {
|
|
controller = effectsController.getEqualizerController();
|
|
if(controller.getEqualizer() == null) {
|
|
throw new Exception("Failed to get EQ");
|
|
}
|
|
} catch(Exception e) {
|
|
Log.w(TAG, "Failed to start EQ, retrying with new mediaPlayer: " + e);
|
|
|
|
// If we failed, we are going to try to reinitialize the MediaPlayer
|
|
boolean playing = playerState == STARTED;
|
|
int pos = getPlayerPosition();
|
|
mediaPlayer.pause();
|
|
Util.sleepQuietly(10L);
|
|
reset();
|
|
|
|
try {
|
|
// Resetup media player
|
|
mediaPlayer.setAudioSessionId(audioSessionId);
|
|
mediaPlayer.setDataSource(currentPlaying.getFile().getCanonicalPath());
|
|
|
|
controller = effectsController.getEqualizerController();
|
|
if(controller.getEqualizer() == null) {
|
|
throw new Exception("Failed to get EQ");
|
|
}
|
|
} catch(Exception e2) {
|
|
Log.w(TAG, "Failed to setup EQ even after reinitialization");
|
|
// Don't try again, just resetup media player and continue on
|
|
controller = null;
|
|
}
|
|
|
|
// Restart from same position and state we left off in
|
|
play(getCurrentPlayingIndex(), false, pos);
|
|
}
|
|
|
|
return controller;
|
|
}
|
|
|
|
public boolean isSeekable() {
|
|
return currentPlaying != null && currentPlaying.isWorkDone() && playerState != PREPARING;
|
|
}
|
|
|
|
public void updateRemoteVolume(boolean up) {
|
|
AudioManager audioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
|
|
audioManager.adjustVolume(up ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI);
|
|
}
|
|
|
|
private synchronized void bufferAndPlay() {
|
|
bufferAndPlay(0);
|
|
}
|
|
private synchronized void bufferAndPlay(int position) {
|
|
bufferAndPlay(position, true);
|
|
}
|
|
private synchronized void bufferAndPlay(int position, boolean start) {
|
|
if(!currentPlaying.isCompleteFileAvailable()) {
|
|
if(Util.isAllowedToDownload(this)) {
|
|
reset();
|
|
|
|
bufferTask = new BufferTask(currentPlaying, position, start);
|
|
bufferTask.execute();
|
|
} else {
|
|
next(false, start);
|
|
}
|
|
} else {
|
|
doPlay(currentPlaying, position, start);
|
|
}
|
|
}
|
|
|
|
private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) {
|
|
try {
|
|
subtractPosition = 0;
|
|
mediaPlayer.setOnCompletionListener(null);
|
|
mediaPlayer.setOnPreparedListener(null);
|
|
mediaPlayer.setOnErrorListener(null);
|
|
mediaPlayer.reset();
|
|
setPlayerState(IDLE);
|
|
try {
|
|
mediaPlayer.setAudioSessionId(audioSessionId);
|
|
} catch(Throwable e) {
|
|
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
|
}
|
|
|
|
String dataSource;
|
|
boolean isPartial = false;
|
|
downloadFile.setPlaying(true);
|
|
final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile();
|
|
isPartial = file.equals(downloadFile.getPartialFile());
|
|
downloadFile.updateModificationDate();
|
|
|
|
dataSource = file.getAbsolutePath();
|
|
if (isPartial && !Util.isOffline(this)) {
|
|
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;
|
|
|
|
applyReplayGain(mediaPlayer, downloadFile);
|
|
|
|
if (start || autoPlayStart) {
|
|
mediaPlayer.start();
|
|
setPlayerState(STARTED);
|
|
|
|
// Disable autoPlayStart after done
|
|
autoPlayStart = false;
|
|
} else {
|
|
setPlayerState(PAUSED);
|
|
onSongProgress();
|
|
}
|
|
|
|
updateRemotePlaylist();
|
|
}
|
|
|
|
// Only call when starting, setPlayerState(PAUSED) already calls this
|
|
if(start) {
|
|
lifecycleSupport.serializeDownloadQueue();
|
|
}
|
|
} catch (Exception x) {
|
|
handleError(x);
|
|
}
|
|
}
|
|
});
|
|
|
|
setupHandlers(downloadFile, isPartial, start);
|
|
|
|
mediaPlayer.prepareAsync();
|
|
} catch (Exception x) {
|
|
handleError(x);
|
|
}
|
|
}
|
|
|
|
private synchronized void setupNext(final DownloadFile downloadFile) {
|
|
try {
|
|
final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile();
|
|
resetNext();
|
|
|
|
nextMediaPlayer = new MediaPlayer();
|
|
nextMediaPlayer.setWakeMode(DownloadService.this, PowerManager.PARTIAL_WAKE_LOCK);
|
|
try {
|
|
nextMediaPlayer.setAudioSessionId(audioSessionId);
|
|
} catch(Throwable e) {
|
|
nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
|
}
|
|
nextMediaPlayer.setDataSource(file.getPath());
|
|
setNextPlayerState(PREPARING);
|
|
|
|
nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
|
|
public void onPrepared(MediaPlayer mp) {
|
|
// Changed to different while preparing so ignore
|
|
if(nextMediaPlayer != mp) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setNextPlayerState(PREPARED);
|
|
|
|
if(playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED) {
|
|
mediaPlayer.setNextMediaPlayer(nextMediaPlayer);
|
|
nextSetup = true;
|
|
}
|
|
|
|
applyReplayGain(nextMediaPlayer, downloadFile);
|
|
} 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 boolean isPlaying) {
|
|
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 = getPlayerPosition();
|
|
reset();
|
|
if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000))) {
|
|
playNext();
|
|
} else {
|
|
downloadFile.setPlaying(false);
|
|
doPlay(downloadFile, pos, isPlaying);
|
|
downloadFile.setPlaying(true);
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
|
|
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
|
|
@Override
|
|
public void onCompletion(MediaPlayer mediaPlayer) {
|
|
setPlayerStateCompleted();
|
|
|
|
int pos = getPlayerPosition();
|
|
Log.i(TAG, "Ending position " + pos + " of " + duration);
|
|
if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000)) || nextSetup) {
|
|
playNext();
|
|
postPlayCleanup(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.execute();
|
|
}
|
|
}
|
|
checkDownloads();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
public void setVolume(float volume) {
|
|
if(mediaPlayer != null && (playerState == STARTED || playerState == PAUSED || playerState == STOPPED)) {
|
|
try {
|
|
this.volume = volume;
|
|
reapplyVolume();
|
|
} catch(Exception e) {
|
|
Log.w(TAG, "Failed to set volume");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void reapplyVolume() {
|
|
applyReplayGain(mediaPlayer, currentPlaying);
|
|
}
|
|
|
|
public synchronized void swap(boolean mainList, DownloadFile from, DownloadFile to) {
|
|
List<DownloadFile> list = mainList ? downloadList : backgroundDownloadList;
|
|
swap(mainList, list.indexOf(from), list.indexOf(to));
|
|
}
|
|
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;
|
|
}
|
|
|
|
DownloadFile movedSong = list.remove(from);
|
|
list.add(to, movedSong);
|
|
currentPlayingIndex = downloadList.indexOf(currentPlaying);
|
|
if(mainList) {
|
|
// Moving next playing, current playing, or moving a song to be next playing
|
|
if(movedSong == nextPlaying || movedSong == currentPlaying || (currentPlayingIndex + 1) == to) {
|
|
setNextPlaying();
|
|
}
|
|
}
|
|
}
|
|
|
|
public synchronized void serializeQueue() {
|
|
serializeQueue(true);
|
|
}
|
|
public synchronized void serializeQueue(boolean serializeRemote) {
|
|
if(playerState == PlayerState.PAUSED) {
|
|
lifecycleSupport.serializeDownloadQueue(serializeRemote);
|
|
}
|
|
}
|
|
|
|
private void handleError(Exception x) {
|
|
Log.w(TAG, "Media player error: " + x, x);
|
|
if(mediaPlayer != null) {
|
|
try {
|
|
mediaPlayer.reset();
|
|
} catch(Exception e) {
|
|
Log.e(TAG, "Failed to reset player in error handler");
|
|
}
|
|
}
|
|
setPlayerState(IDLE);
|
|
}
|
|
private void handleErrorNext(Exception x) {
|
|
Log.w(TAG, "Next Media player error: " + x, x);
|
|
try {
|
|
nextMediaPlayer.reset();
|
|
} catch(Exception e) {
|
|
Log.e(TAG, "Failed to reset next media player", x);
|
|
}
|
|
setNextPlayerState(IDLE);
|
|
}
|
|
|
|
public synchronized void checkDownloads() {
|
|
if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) {
|
|
return;
|
|
}
|
|
|
|
if(removePlayed) {
|
|
checkRemovePlayed();
|
|
}
|
|
if (shufflePlay) {
|
|
checkShufflePlay();
|
|
}
|
|
|
|
if (!Util.isAllowedToDownload(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()) || downloadFile.isFailedMax()) {
|
|
// Don't need to keep list like active song list
|
|
backgroundDownloadList.remove(i);
|
|
revision++;
|
|
i--;
|
|
} else {
|
|
currentDownloading = downloadFile;
|
|
currentDownloading.download();
|
|
cleanupCandidates.add(currentDownloading);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!backgroundDownloadList.isEmpty()) {
|
|
Notifications.showDownloadingNotification(this, this, handler, currentDownloading, backgroundDownloadList.size());
|
|
downloadOngoing = true;
|
|
} else if(backgroundDownloadList.isEmpty() && downloadOngoing) {
|
|
Notifications.hideDownloadingNotification(this, this, handler);
|
|
downloadOngoing = false;
|
|
}
|
|
|
|
// Delete obsolete .partial and .complete files.
|
|
cleanup();
|
|
}
|
|
|
|
private synchronized void checkRemovePlayed() {
|
|
boolean changed = false;
|
|
SharedPreferences prefs = Util.getPreferences(this);
|
|
int keepCount = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_KEEP_PLAYED_CNT, "0"));
|
|
while(currentPlayingIndex > keepCount) {
|
|
downloadList.remove(0);
|
|
currentPlayingIndex = downloadList.indexOf(currentPlaying);
|
|
changed = true;
|
|
}
|
|
|
|
if(changed) {
|
|
revision++;
|
|
onSongsChanged();
|
|
}
|
|
}
|
|
|
|
private synchronized void checkShufflePlay() {
|
|
|
|
// Get users desired random playlist size
|
|
SharedPreferences prefs = Util.getPreferences(this);
|
|
int listSize = Math.max(1, 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) {
|
|
onSongsChanged();
|
|
updateRemotePlaylist();
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void postPlayCleanup() {
|
|
postPlayCleanup(currentPlaying);
|
|
}
|
|
public void postPlayCleanup(DownloadFile downloadFile) {
|
|
if(downloadFile == null) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
private boolean isPastCutoff() {
|
|
return isPastCutoff(getPlayerPosition(), getPlayerDuration());
|
|
}
|
|
private boolean isPastCutoff(int position, int duration) {
|
|
return isPastCutoff(position, duration, false);
|
|
}
|
|
private boolean isPastCutoff(int position, int duration, boolean allowSkipping) {
|
|
if(currentPlaying == null) {
|
|
return false;
|
|
}
|
|
|
|
// Make cutoff a maximum of 10 minutes
|
|
int cutoffPoint = Math.max((int) (duration * DELETE_CUTOFF), duration - 10 * 60 * 1000);
|
|
boolean isPastCutoff = duration > 0 && position > cutoffPoint;
|
|
|
|
return isPastCutoff;
|
|
}
|
|
|
|
private void applyReplayGain(MediaPlayer mediaPlayer, DownloadFile downloadFile) {
|
|
if(currentPlaying == null) {
|
|
return;
|
|
}
|
|
|
|
SharedPreferences prefs = Util.getPreferences(this);
|
|
try {
|
|
float adjust = 0f;
|
|
if (prefs.getBoolean(Constants.PREFERENCES_KEY_REPLAY_GAIN, false)) {
|
|
float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */
|
|
boolean singleAlbum = false;
|
|
|
|
String replayGainType = prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE, "1");
|
|
// 1 => Smart replay gain
|
|
if("1".equals(replayGainType)) {
|
|
// Check if part of at least <REQUIRED_ALBUM_MATCHES> consequetive songs of the same album
|
|
|
|
int index = downloadList.indexOf(downloadFile);
|
|
if(index != -1) {
|
|
String albumName = downloadFile.getSong().getAlbum();
|
|
int matched = 0;
|
|
|
|
// Check forwards
|
|
for(int i = index + 1; i < downloadList.size() && matched < REQUIRED_ALBUM_MATCHES; i++) {
|
|
if(albumName.equals(downloadList.get(i).getSong().getAlbum())) {
|
|
matched++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check backwards
|
|
for(int i = index - 1; i >= 0 && matched < REQUIRED_ALBUM_MATCHES; i--) {
|
|
if(albumName.equals(downloadList.get(i).getSong().getAlbum())) {
|
|
matched++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(matched >= REQUIRED_ALBUM_MATCHES) {
|
|
singleAlbum = true;
|
|
}
|
|
}
|
|
}
|
|
// 2 => Use album tags
|
|
else if("2".equals(replayGainType)) {
|
|
singleAlbum = true;
|
|
}
|
|
// 3 => Use track tags
|
|
// Already false, no need to do anything here
|
|
|
|
|
|
// If playing a single album or no track gain, use album gain
|
|
if((singleAlbum || rg[0] == 0) && rg[1] != 0) {
|
|
adjust = rg[1];
|
|
} else {
|
|
// Otherwise, give priority to track gain
|
|
adjust = rg[0];
|
|
}
|
|
|
|
if (adjust == 0) {
|
|
/* No RG value found: decrease volume for untagged song if requested by user */
|
|
int untagged = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED, "0"));
|
|
adjust = (untagged - 150) / 10f;
|
|
} else {
|
|
int bump = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP, "150"));
|
|
adjust += (bump - 150) / 10f;
|
|
}
|
|
}
|
|
|
|
float rg_result = ((float) Math.pow(10, (adjust / 20))) * volume;
|
|
if (rg_result > 1.0f) {
|
|
rg_result = 1.0f; /* android would IGNORE the change if this is > 1 and we would end up with the wrong volume */
|
|
} else if (rg_result < 0.0f) {
|
|
rg_result = 0.0f;
|
|
}
|
|
mediaPlayer.setVolume(rg_result, rg_result);
|
|
} catch(IOException e) {
|
|
Log.w(TAG, "Failed to apply replay gain values", e);
|
|
}
|
|
}
|
|
|
|
private synchronized boolean isNextPlayingSameAlbum() {
|
|
return isNextPlayingSameAlbum(currentPlaying, nextPlaying);
|
|
}
|
|
private synchronized boolean isNextPlayingSameAlbum(DownloadFile currentPlaying, DownloadFile nextPlaying) {
|
|
if(currentPlaying == null || nextPlaying == null) {
|
|
return false;
|
|
} else {
|
|
return currentPlaying.getSong().getAlbum().equals(nextPlaying.getSong().getAlbum());
|
|
}
|
|
}
|
|
|
|
public void acquireWakelock() {
|
|
acquireWakelock(30000);
|
|
}
|
|
public void acquireWakelock(int ms) {
|
|
wakeLock.acquire(ms);
|
|
}
|
|
|
|
public void handleKeyEvent(KeyEvent keyEvent) {
|
|
lifecycleSupport.handleKeyEvent(keyEvent);
|
|
}
|
|
|
|
public void addOnSongChangedListener(OnSongChangedListener listener) {
|
|
addOnSongChangedListener(listener, false);
|
|
}
|
|
public void addOnSongChangedListener(OnSongChangedListener listener, boolean run) {
|
|
synchronized(onSongChangedListeners) {
|
|
int index = onSongChangedListeners.indexOf(listener);
|
|
if (index == -1) {
|
|
onSongChangedListeners.add(listener);
|
|
}
|
|
}
|
|
|
|
if(run) {
|
|
if(mediaPlayerHandler != null) {
|
|
mediaPlayerHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
onSongsChanged();
|
|
onSongProgress();
|
|
onStateUpdate();
|
|
onMetadataUpdate(METADATA_UPDATED_ALL);
|
|
}
|
|
});
|
|
} else {
|
|
runListenersOnInit = true;
|
|
}
|
|
}
|
|
}
|
|
public void removeOnSongChangeListener(OnSongChangedListener listener) {
|
|
synchronized(onSongChangedListeners) {
|
|
int index = onSongChangedListeners.indexOf(listener);
|
|
if (index != -1) {
|
|
onSongChangedListeners.remove(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void onSongChanged() {
|
|
final long atRevision = revision;
|
|
synchronized(onSongChangedListeners) {
|
|
for (final OnSongChangedListener listener : onSongChangedListeners) {
|
|
handler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (revision == atRevision && instance != null) {
|
|
listener.onSongChanged(currentPlaying, currentPlayingIndex);
|
|
|
|
MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null;
|
|
listener.onMetadataUpdate(entry, METADATA_UPDATED_ALL);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (mediaPlayerHandler != null && !onSongChangedListeners.isEmpty()) {
|
|
mediaPlayerHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
onSongProgress();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
private void onSongsChanged() {
|
|
final long atRevision = revision;
|
|
synchronized(onSongChangedListeners) {
|
|
for (final OnSongChangedListener listener : onSongChangedListeners) {
|
|
handler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (revision == atRevision && instance != null) {
|
|
listener.onSongsChanged(downloadList, currentPlaying, currentPlayingIndex);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private void onSongProgress() {
|
|
onSongProgress(true);
|
|
}
|
|
private synchronized void onSongProgress(boolean manual) {
|
|
final long atRevision = revision;
|
|
final Integer duration = getPlayerDuration();
|
|
final boolean isSeekable = isSeekable();
|
|
final int position = getPlayerPosition();
|
|
|
|
synchronized(onSongChangedListeners) {
|
|
for (final OnSongChangedListener listener : onSongChangedListeners) {
|
|
handler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (revision == atRevision && instance != null) {
|
|
listener.onSongProgress(currentPlaying, position, duration, isSeekable);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if(manual) {
|
|
handler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
}
|
|
});
|
|
}
|
|
}
|
|
private void onStateUpdate() {
|
|
final long atRevision = revision;
|
|
synchronized(onSongChangedListeners) {
|
|
for (final OnSongChangedListener listener : onSongChangedListeners) {
|
|
handler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (revision == atRevision && instance != null) {
|
|
listener.onStateUpdate(currentPlaying, playerState);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
public void onMetadataUpdate() {
|
|
onMetadataUpdate(METADATA_UPDATED_ALL);
|
|
}
|
|
public void onMetadataUpdate(final int updateType) {
|
|
synchronized(onSongChangedListeners) {
|
|
for (final OnSongChangedListener listener : onSongChangedListeners) {
|
|
handler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (instance != null) {
|
|
MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null;
|
|
listener.onMetadataUpdate(entry, updateType);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
handler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
}
|
|
});
|
|
}
|
|
|
|
private class BufferTask extends SilentBackgroundTask<Void> {
|
|
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) {
|
|
super(instance);
|
|
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 doInBackground() throws InterruptedException {
|
|
setPlayerState(DOWNLOADING);
|
|
|
|
while (!bufferComplete()) {
|
|
Thread.sleep(1000L);
|
|
if (isCancelled() || downloadFile.isFailedMax()) {
|
|
return null;
|
|
} else if(!downloadFile.isFailedMax() && !downloadFile.isDownloading()) {
|
|
checkDownloads();
|
|
}
|
|
}
|
|
doPlay(downloadFile, position, start);
|
|
|
|
return null;
|
|
}
|
|
|
|
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 SilentBackgroundTask<Void> {
|
|
private final DownloadFile downloadFile;
|
|
private final File partialFile;
|
|
|
|
public CheckCompletionTask(DownloadFile downloadFile) {
|
|
super(instance);
|
|
this.downloadFile = downloadFile;
|
|
if(downloadFile != null) {
|
|
partialFile = downloadFile.getPartialFile();
|
|
} else {
|
|
partialFile = null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Void doInBackground() throws InterruptedException {
|
|
if(downloadFile == null) {
|
|
return null;
|
|
}
|
|
|
|
// Do an initial sleep so this prepare can't compete with main prepare
|
|
Thread.sleep(5000L);
|
|
while (!bufferComplete()) {
|
|
Thread.sleep(5000L);
|
|
if (isCancelled()) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Start the setup of the next media player
|
|
mediaPlayerHandler.post(new Runnable() {
|
|
public void run() {
|
|
if(!CheckCompletionTask.this.isCancelled()) {
|
|
setupNext(downloadFile);
|
|
}
|
|
}
|
|
});
|
|
return null;
|
|
}
|
|
|
|
private boolean bufferComplete() {
|
|
boolean completeFileAvailable = downloadFile.isWorkDone();
|
|
Log.i(TAG, "Buffering next " + partialFile + " (" + partialFile.length() + "): " + completeFileAvailable);
|
|
return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "CheckCompletionTask (" + downloadFile + ")";
|
|
}
|
|
}
|
|
|
|
public interface OnSongChangedListener {
|
|
void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex);
|
|
void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex);
|
|
void onSongProgress(DownloadFile currentPlaying, int millisPlayed, Integer duration, boolean isSeekable);
|
|
void onStateUpdate(DownloadFile downloadFile, PlayerState playerState);
|
|
void onMetadataUpdate(MusicDirectory.Entry entry, int fieldChange);
|
|
}
|
|
}
|