Audinaut-subsonic-app-android/app/src/main/java/net/nullsum/audinaut/service/DownloadService.java

2060 lines
72 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 android.app.Service;
import android.content.ComponentCallbacks2;
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.audiofx.AudioEffect;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.support.v4.util.LruCache;
import android.util.Log;
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.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.Util;
import net.nullsum.audinaut.util.tags.BastpUtil;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
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;
/**
* @author Sindre Mehus
* @version $Id$
*/
public class DownloadService extends Service {
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;
public static final int METADATA_UPDATED_ALL = 0;
public static final int METADATA_UPDATED_COVER_ART = 8;
private static final String TAG = DownloadService.class.getSimpleName();
private static final long DEFAULT_DELAY_UPDATE_PROGRESS = 1000L;
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 DownloadService instance;
private final IBinder binder = new SimpleServiceBinder<>();
private final List<DownloadFile> downloadList = new ArrayList<>();
private final List<DownloadFile> backgroundDownloadList = new ArrayList<>();
private final List<DownloadFile> toDelete = new ArrayList<>();
private final Handler handler = new Handler();
private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this);
private final LruCache<MusicDirectory.Entry, DownloadFile> downloadFileCache = new LruCache<>(100);
private final List<DownloadFile> cleanupCandidates = new ArrayList<>();
private final List<OnSongChangedListener> onSongChangedListeners = new ArrayList<>();
private final long delayUpdateProgress = DEFAULT_DELAY_UPDATE_PROGRESS;
private final AudioNoisyReceiver audioNoisyReceiver = new AudioNoisyReceiver();
private Looper mediaPlayerLooper;
private MediaPlayer mediaPlayer;
private MediaPlayer nextMediaPlayer;
private int audioSessionId;
private boolean nextSetup = false;
private Handler mediaPlayerHandler;
private ShufflePlayBuffer shufflePlayBuffer;
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 long revision;
private String suggestedPlaylistName;
private String suggestedPlaylistId;
private PowerManager.WakeLock wakeLock;
private int cachedPosition = 0;
private boolean downloadOngoing = false;
private float volume = 1.0f;
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;
public static DownloadService getInstance() {
return instance;
}
@Override
public void onCreate() {
super.onCreate();
final SharedPreferences prefs = Util.getPreferences(this);
new Thread(() -> {
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((mediaPlayer, what, 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);
wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "downloadServiceLock");
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);
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) {
download(songs, save, autoplay, playNext, shuffle, 0);
}
public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, int start) {
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, 0);
} else if (start != 0) {
play(start, false, 0);
} 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 boolean isRemovePlayed() {
return removePlayed;
}
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 isShufflePlayEnabled() {
return shufflePlay;
}
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 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 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(() -> {
if (online) {
checkDownloads();
} else {
clearIncomplete();
}
});
}
public synchronized int size() {
return downloadList.size();
}
public synchronized void clear() {
// Delete podcast if fully listened to
for (DownloadFile podcast : toDelete) {
podcast.delete();
}
toDelete.clear();
reset();
downloadList.clear();
onSongsChanged();
if (currentDownloading != null && !backgroundDownloadList.contains(currentDownloading)) {
currentDownloading.cancelDownload();
currentDownloading = null;
}
setCurrentPlaying(null);
lifecycleSupport.serializeDownloadQueue();
updateRemotePlaylist();
setNextPlaying();
if (proxy != null) {
proxy.stop();
proxy = null;
}
suggestedPlaylistName = null;
suggestedPlaylistId = null;
setShufflePlayEnabled(false);
checkDownloads();
}
public synchronized void remove(DownloadFile downloadFile) {
if (downloadFile == currentDownloading) {
currentDownloading.cancelDownload();
currentDownloading = null;
}
if (downloadFile == currentPlaying) {
reset();
setCurrentPlaying(null);
}
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();
}
}
private synchronized void setCurrentPlaying(int currentPlayingIndex) {
try {
setCurrentPlaying(downloadList.get(currentPlayingIndex));
} catch (IndexOutOfBoundsException x) {
// Ignored
}
}
private 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(() -> Util.toast(DownloadService.this, R.string.download_playerstate_mobile_disabled));
return -1;
}
next = downloadList.get(index);
}
}
}
return index;
}
public DownloadFile getCurrentPlaying() {
return currentPlaying;
}
private synchronized void setCurrentPlaying(DownloadFile currentPlaying) {
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();
}
public DownloadFile getCurrentDownloading() {
return currentDownloading;
}
public List<DownloadFile> getSongs() {
return downloadList;
}
public List<DownloadFile> getToDelete() {
return toDelete;
}
public synchronized List<DownloadFile> getDownloads() {
List<DownloadFile> temp = new ArrayList<>();
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);
Notifications.hidePlayingNotification(this, this, handler);
} else {
setCurrentPlaying(null);
}
lifecycleSupport.serializeDownloadQueue();
} else {
if (nextPlayingTask != null) {
nextPlayingTask.cancel();
nextPlayingTask = null;
}
setCurrentPlaying(index);
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);
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);
}
private 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 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;
}
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);
}
}
private void onSongCompleted() {
setPlayerStateCompleted();
play(getNextPlayingIndex());
}
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);
}
}
private 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;
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;
}
private 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();
}
private synchronized void setNextPlayerState(PlayerState playerState) {
Log.i(TAG, "Next: " + this.nextPlayerState.name() + " -> " + playerState.name() + " (" + nextPlaying + ")");
this.nextPlayerState = playerState;
}
private 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();
}
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;
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
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;
}
private boolean isSeekable() {
return currentPlaying != null && currentPlaying.isWorkDone() && playerState != PREPARING;
}
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(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;
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();
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((mp, percent) -> {
Log.i(TAG, "Buffered " + percent + "%");
if (percent == 100) {
mediaPlayer.setOnBufferingUpdateListener(null);
}
});
mediaPlayer.setOnPreparedListener(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(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((mediaPlayer, what, 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((mediaPlayer, what, 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(mediaPlayer -> {
setPlayerStateCompleted();
int pos = getPlayerPosition();
Log.i(TAG, "Ending position " + pos + " of " + duration);
if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000)) || nextSetup) {
playNext();
} 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, 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() {
if (playerState == PlayerState.PAUSED) {
lifecycleSupport.serializeDownloadQueue();
}
}
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();
}
}
}
}
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 void acquireWakelock() {
wakeLock.acquire(30000);
}
public void addOnSongChangedListener(OnSongChangedListener listener) {
synchronized (onSongChangedListeners) {
int index = onSongChangedListeners.indexOf(listener);
if (index == -1) {
onSongChangedListeners.add(listener);
}
}
if (mediaPlayerHandler != null) {
mediaPlayerHandler.post(() -> {
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(() -> {
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(this::onSongProgress);
}
}
}
private void onSongsChanged() {
final long atRevision = revision;
synchronized (onSongChangedListeners) {
for (final OnSongChangedListener listener : onSongChangedListeners) {
handler.post(() -> {
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(() -> {
if (revision == atRevision && instance != null) {
listener.onSongProgress(currentPlaying, position, duration, isSeekable);
}
});
}
}
if (manual) {
handler.post(() -> {
});
}
}
private void onStateUpdate() {
final long atRevision = revision;
synchronized (onSongChangedListeners) {
for (final OnSongChangedListener listener : onSongChangedListeners) {
handler.post(() -> {
if (revision == atRevision && instance != null) {
listener.onStateUpdate(playerState);
}
});
}
}
}
public void onMetadataUpdate(final int updateType) {
synchronized (onSongChangedListeners) {
for (final OnSongChangedListener listener : onSongChangedListeners) {
handler.post(() -> {
if (instance != null) {
MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null;
listener.onMetadataUpdate(entry, updateType);
}
});
}
}
handler.post(() -> {
});
}
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(PlayerState playerState);
void onMetadataUpdate(MusicDirectory.Entry entry, int fieldChange);
}
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);
Thread.sleep(delayUpdateProgress);
} catch (Exception e) {
Log.w(TAG, "Crashed getting current position", e);
isRunning = false;
positionCache = null;
}
}
}
}
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(() -> {
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 + ")";
}
}
}