454 lines
14 KiB
Java
454 lines
14 KiB
Java
package org.moire.ultrasonic.service;
|
|
|
|
import android.content.Context;
|
|
import android.util.Log;
|
|
|
|
import org.moire.ultrasonic.domain.MusicDirectory;
|
|
import org.moire.ultrasonic.util.LRUCache;
|
|
import org.moire.ultrasonic.util.ShufflePlayBuffer;
|
|
import org.moire.ultrasonic.util.Util;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import kotlin.Lazy;
|
|
|
|
import static org.koin.java.standalone.KoinJavaComponent.inject;
|
|
import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING;
|
|
import static org.moire.ultrasonic.domain.PlayerState.STARTED;
|
|
|
|
/**
|
|
* This class is responsible for maintaining the playlist and downloading
|
|
* its items from the network to the filesystem.
|
|
*/
|
|
public class Downloader
|
|
{
|
|
private static final String TAG = Downloader.class.getSimpleName();
|
|
|
|
public final List<DownloadFile> downloadList = new ArrayList<>();
|
|
public final List<DownloadFile> backgroundDownloadList = new ArrayList<>();
|
|
public DownloadFile currentDownloading;
|
|
|
|
private final ShufflePlayBuffer shufflePlayBuffer;
|
|
private final ExternalStorageMonitor externalStorageMonitor;
|
|
private final LocalMediaPlayer localMediaPlayer;
|
|
private final Context context;
|
|
|
|
// TODO: This is a circular reference, try to remove
|
|
private Lazy<JukeboxMediaPlayer> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
|
|
|
|
private final List<DownloadFile> cleanupCandidates = new ArrayList<>();
|
|
private final LRUCache<MusicDirectory.Entry, DownloadFile> downloadFileCache = new LRUCache<>(100);
|
|
private ScheduledExecutorService executorService;
|
|
private long revision;
|
|
|
|
public Downloader(Context context, ShufflePlayBuffer shufflePlayBuffer, ExternalStorageMonitor externalStorageMonitor,
|
|
LocalMediaPlayer localMediaPlayer)
|
|
{
|
|
this.context = context;
|
|
this.shufflePlayBuffer = shufflePlayBuffer;
|
|
this.externalStorageMonitor = externalStorageMonitor;
|
|
this.localMediaPlayer = localMediaPlayer;
|
|
}
|
|
|
|
public void onCreate()
|
|
{
|
|
Runnable downloadChecker = new Runnable()
|
|
{
|
|
@Override
|
|
public void run()
|
|
{
|
|
try
|
|
{
|
|
checkDownloads();
|
|
}
|
|
catch (Throwable x)
|
|
{
|
|
Log.e(TAG, "checkDownloads() failed.", x);
|
|
}
|
|
}
|
|
};
|
|
|
|
executorService = Executors.newSingleThreadScheduledExecutor();
|
|
executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS);
|
|
Log.i(TAG, "Downloader created");
|
|
}
|
|
|
|
public void onDestroy()
|
|
{
|
|
stop();
|
|
clear();
|
|
clearBackground();
|
|
Log.i(TAG, "Downloader destroyed");
|
|
}
|
|
|
|
public void stop()
|
|
{
|
|
executorService.shutdown();
|
|
Log.i(TAG, "Downloader stopped");
|
|
}
|
|
|
|
public synchronized void checkDownloads()
|
|
{
|
|
if (!Util.isExternalStoragePresent() || !externalStorageMonitor.isExternalStorageAvailable())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (shufflePlayBuffer.isEnabled)
|
|
{
|
|
checkShufflePlay(context);
|
|
}
|
|
|
|
if (jukeboxMediaPlayer.getValue().isEnabled() || !Util.isNetworkConnected(context))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (downloadList.isEmpty() && backgroundDownloadList.isEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Need to download current playing?
|
|
if (localMediaPlayer.currentPlaying != null && localMediaPlayer.currentPlaying != currentDownloading && !localMediaPlayer.currentPlaying.isWorkDone())
|
|
{
|
|
// Cancel current download, if necessary.
|
|
if (currentDownloading != null)
|
|
{
|
|
currentDownloading.cancelDownload();
|
|
}
|
|
|
|
currentDownloading = localMediaPlayer.currentPlaying;
|
|
currentDownloading.download();
|
|
cleanupCandidates.add(currentDownloading);
|
|
|
|
// Delete obsolete .partial and .complete files.
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
// Find a suitable target for download.
|
|
if (currentDownloading != null &&
|
|
!currentDownloading.isWorkDone() &&
|
|
(!currentDownloading.isFailed() || (downloadList.isEmpty() && backgroundDownloadList.isEmpty())))
|
|
{
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
// There is a target to download
|
|
currentDownloading = null;
|
|
int n = downloadList.size();
|
|
|
|
int preloaded = 0;
|
|
|
|
if (n != 0)
|
|
{
|
|
int start = localMediaPlayer.currentPlaying == null ? 0 : getCurrentPlayingIndex();
|
|
if (start == -1) start = 0;
|
|
|
|
int i = start;
|
|
// Check all DownloadFiles on the playlist
|
|
do
|
|
{
|
|
DownloadFile downloadFile = downloadList.get(i);
|
|
if (!downloadFile.isWorkDone())
|
|
{
|
|
if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(context))
|
|
{
|
|
currentDownloading = downloadFile;
|
|
currentDownloading.download();
|
|
cleanupCandidates.add(currentDownloading);
|
|
if (i == (start + 1))
|
|
{
|
|
// The next file on the playlist is currently downloading
|
|
localMediaPlayer.setNextPlayerState(DOWNLOADING);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else if (localMediaPlayer.currentPlaying != downloadFile)
|
|
{
|
|
preloaded++;
|
|
}
|
|
|
|
i = (i + 1) % n;
|
|
} while (i != start);
|
|
}
|
|
|
|
// If the downloadList contains no work, check the backgroundDownloadList
|
|
if ((preloaded + 1 == n || preloaded >= Util.getPreloadCount(context) || downloadList.isEmpty()) && !backgroundDownloadList.isEmpty())
|
|
{
|
|
for (int i = 0; i < backgroundDownloadList.size(); i++)
|
|
{
|
|
DownloadFile downloadFile = backgroundDownloadList.get(i);
|
|
if (downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved()))
|
|
{
|
|
if (Util.getShouldScanMedia(context))
|
|
{
|
|
Util.scanMedia(context, downloadFile.getCompleteFile());
|
|
}
|
|
|
|
// Don't need to keep list like active song list
|
|
backgroundDownloadList.remove(i);
|
|
revision++;
|
|
i--;
|
|
}
|
|
else
|
|
{
|
|
currentDownloading = downloadFile;
|
|
currentDownloading.download();
|
|
cleanupCandidates.add(currentDownloading);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete obsolete .partial and .complete files.
|
|
cleanup();
|
|
}
|
|
|
|
public synchronized int getCurrentPlayingIndex()
|
|
{
|
|
return downloadList.indexOf(localMediaPlayer.currentPlaying);
|
|
}
|
|
|
|
public long getDownloadListDuration()
|
|
{
|
|
long totalDuration = 0;
|
|
|
|
for (DownloadFile downloadFile : downloadList)
|
|
{
|
|
MusicDirectory.Entry entry = downloadFile.getSong();
|
|
|
|
if (!entry.isDirectory())
|
|
{
|
|
if (entry.getArtist() != null)
|
|
{
|
|
Integer duration = entry.getDuration();
|
|
|
|
if (duration != null)
|
|
{
|
|
totalDuration += duration;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return totalDuration;
|
|
}
|
|
|
|
public synchronized List<DownloadFile> getDownloads()
|
|
{
|
|
List<DownloadFile> temp = new ArrayList<>();
|
|
temp.addAll(downloadList);
|
|
temp.addAll(backgroundDownloadList);
|
|
return temp;
|
|
}
|
|
|
|
public long getDownloadListUpdateRevision()
|
|
{
|
|
return revision;
|
|
}
|
|
|
|
public synchronized void clear()
|
|
{
|
|
downloadList.clear();
|
|
revision++;
|
|
if (currentDownloading != null)
|
|
{
|
|
currentDownloading.cancelDownload();
|
|
currentDownloading = null;
|
|
}
|
|
}
|
|
|
|
private void clearBackground()
|
|
{
|
|
if (currentDownloading != null && backgroundDownloadList.contains(currentDownloading))
|
|
{
|
|
currentDownloading.cancelDownload();
|
|
currentDownloading = null;
|
|
}
|
|
backgroundDownloadList.clear();
|
|
}
|
|
|
|
public synchronized void removeDownloadFile(DownloadFile downloadFile)
|
|
{
|
|
if (downloadFile == currentDownloading)
|
|
{
|
|
currentDownloading.cancelDownload();
|
|
currentDownloading = null;
|
|
}
|
|
|
|
downloadList.remove(downloadFile);
|
|
backgroundDownloadList.remove(downloadFile);
|
|
revision++;
|
|
}
|
|
|
|
public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoPlay, boolean playNext, boolean newPlaylist)
|
|
{
|
|
shufflePlayBuffer.isEnabled = false;
|
|
int offset = 1;
|
|
|
|
if (songs.isEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (newPlaylist)
|
|
{
|
|
downloadList.clear();
|
|
}
|
|
|
|
if (playNext)
|
|
{
|
|
if (autoPlay && getCurrentPlayingIndex() >= 0)
|
|
{
|
|
offset = 0;
|
|
}
|
|
|
|
for (MusicDirectory.Entry song : songs)
|
|
{
|
|
DownloadFile downloadFile = new DownloadFile(context, song, save);
|
|
downloadList.add(getCurrentPlayingIndex() + offset, downloadFile);
|
|
offset++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (MusicDirectory.Entry song : songs)
|
|
{
|
|
DownloadFile downloadFile = new DownloadFile(context, song, save);
|
|
downloadList.add(downloadFile);
|
|
}
|
|
}
|
|
revision++;
|
|
}
|
|
|
|
public synchronized void downloadBackground(List<MusicDirectory.Entry> songs, boolean save)
|
|
{
|
|
for (MusicDirectory.Entry song : songs)
|
|
{
|
|
DownloadFile downloadFile = new DownloadFile(context, song, save);
|
|
backgroundDownloadList.add(downloadFile);
|
|
}
|
|
|
|
revision++;
|
|
|
|
checkDownloads();
|
|
}
|
|
|
|
public synchronized void shuffle()
|
|
{
|
|
Collections.shuffle(downloadList);
|
|
if (localMediaPlayer.currentPlaying != null)
|
|
{
|
|
downloadList.remove(getCurrentPlayingIndex());
|
|
downloadList.add(0, localMediaPlayer.currentPlaying);
|
|
}
|
|
revision++;
|
|
}
|
|
|
|
public synchronized DownloadFile getDownloadFileForSong(MusicDirectory.Entry song)
|
|
{
|
|
for (DownloadFile downloadFile : downloadList)
|
|
{
|
|
if (downloadFile.getSong().equals(song) && ((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && downloadFile.getPartialFile().exists()) || downloadFile.isWorkDone()))
|
|
{
|
|
return downloadFile;
|
|
}
|
|
}
|
|
for (DownloadFile downloadFile : backgroundDownloadList)
|
|
{
|
|
if (downloadFile.getSong().equals(song))
|
|
{
|
|
return downloadFile;
|
|
}
|
|
}
|
|
|
|
DownloadFile downloadFile = downloadFileCache.get(song);
|
|
if (downloadFile == null)
|
|
{
|
|
downloadFile = new DownloadFile(context, song, false);
|
|
downloadFileCache.put(song, downloadFile);
|
|
}
|
|
return downloadFile;
|
|
}
|
|
|
|
private synchronized void cleanup()
|
|
{
|
|
Iterator<DownloadFile> iterator = cleanupCandidates.iterator();
|
|
while (iterator.hasNext())
|
|
{
|
|
DownloadFile downloadFile = iterator.next();
|
|
if (downloadFile != localMediaPlayer.currentPlaying && downloadFile != currentDownloading)
|
|
{
|
|
if (downloadFile.cleanup())
|
|
{
|
|
iterator.remove();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private synchronized void checkShufflePlay(Context context)
|
|
{
|
|
// Get users desired random playlist size
|
|
int listSize = Util.getMaxSongs(context);
|
|
boolean wasEmpty = downloadList.isEmpty();
|
|
|
|
long revisionBefore = revision;
|
|
|
|
// First, ensure that list is at least 20 songs long.
|
|
int size = downloadList.size();
|
|
if (size < listSize)
|
|
{
|
|
for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size))
|
|
{
|
|
DownloadFile downloadFile = new DownloadFile(context, song, false);
|
|
downloadList.add(downloadFile);
|
|
revision++;
|
|
}
|
|
}
|
|
|
|
int currIndex = localMediaPlayer.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(context, song, false));
|
|
downloadList.get(0).cancelDownload();
|
|
downloadList.remove(0);
|
|
revision++;
|
|
}
|
|
}
|
|
|
|
if (revisionBefore != revision)
|
|
{
|
|
jukeboxMediaPlayer.getValue().updatePlaylist();
|
|
}
|
|
|
|
if (wasEmpty && !downloadList.isEmpty())
|
|
{
|
|
if (jukeboxMediaPlayer.getValue().isEnabled())
|
|
{
|
|
jukeboxMediaPlayer.getValue().skip(0, 0);
|
|
localMediaPlayer.setPlayerState(STARTED);
|
|
}
|
|
else
|
|
{
|
|
localMediaPlayer.play(downloadList.get(0));
|
|
}
|
|
}
|
|
}
|
|
}
|