diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java index 41a4c814..0b350604 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java @@ -4,7 +4,6 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.ImageView; import android.widget.ListView; @@ -25,8 +24,8 @@ import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker; import org.moire.ultrasonic.subsonic.VideoPlayer; import org.moire.ultrasonic.util.CancellationToken; import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.Pair; import org.moire.ultrasonic.util.FragmentBackgroundTask; +import org.moire.ultrasonic.util.Pair; import org.moire.ultrasonic.util.Util; import org.moire.ultrasonic.view.EntryAdapter; @@ -78,37 +77,27 @@ public class BookmarksFragment extends Fragment { refreshAlbumListView = view.findViewById(R.id.select_album_entries_refresh); albumListView = view.findViewById(R.id.select_album_entries_list); - refreshAlbumListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() - { - @Override - public void onRefresh() - { - enableButtons(); - getBookmarks(); - } + refreshAlbumListView.setOnRefreshListener(() -> { + enableButtons(); + getBookmarks(); }); albumListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); - albumListView.setOnItemClickListener(new AdapterView.OnItemClickListener() - { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) + albumListView.setOnItemClickListener((parent, view17, position, id) -> { + if (position >= 0) { - if (position >= 0) - { - MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position); + MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position); - if (entry != null) + if (entry != null) + { + if (entry.isVideo()) { - if (entry.isVideo()) - { - VideoPlayer.Companion.playVideo(getContext(), entry); - } - else - { - enableButtons(); - } + VideoPlayer.Companion.playVideo(getContext(), entry); + } + else + { + enableButtons(); } } } @@ -130,58 +119,24 @@ public class BookmarksFragment extends Fragment { playLastButton.setVisibility(View.GONE); oreButton.setVisibility(View.GONE); - playNowButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(View view) - { - playNow(getSelectedSongs(albumListView)); - } - }); + playNowButton.setOnClickListener(view16 -> playNow(getSelectedSongs(albumListView))); - selectButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(View view) - { - selectAllOrNone(); - } + selectButton.setOnClickListener(view15 -> selectAllOrNone()); + pinButton.setOnClickListener(view14 -> { + downloadBackground(true); + selectAll(false, false); }); - pinButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(View view) - { - downloadBackground(true); - selectAll(false, false); - } + unpinButton.setOnClickListener(view13 -> { + unpin(); + selectAll(false, false); }); - unpinButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(View view) - { - unpin(); - selectAll(false, false); - } + downloadButton.setOnClickListener(view12 -> { + downloadBackground(false); + selectAll(false, false); }); - downloadButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(View view) - { - downloadBackground(false); - selectAll(false, false); - } - }); - deleteButton.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(View view) - { - delete(); - selectAll(false, false); - } + deleteButton.setOnClickListener(view1 -> { + delete(); + selectAll(false, false); }); registerForContextMenu(albumListView); @@ -230,7 +185,8 @@ public class BookmarksFragment extends Fragment { { if (albumListView.isItemChecked(i)) { - songs.add((MusicDirectory.Entry) albumListView.getItemAtPosition(i)); + MusicDirectory.Entry song = (MusicDirectory.Entry) albumListView.getItemAtPosition(i); + if (song != null) songs.add(song); } } } @@ -291,6 +247,7 @@ public class BookmarksFragment extends Fragment { for (MusicDirectory.Entry song : selection) { + if (song == null) continue; DownloadFile downloadFile = mediaPlayerController.getValue().getDownloadFileForSong(song); if (downloadFile.isWorkDone()) { @@ -326,22 +283,17 @@ public class BookmarksFragment extends Fragment { private void downloadBackground(final boolean save, final List songs) { - Runnable onValid = new Runnable() - { - @Override - public void run() - { - networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); - mediaPlayerController.getValue().downloadBackground(songs, save); + Runnable onValid = () -> { + networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); + mediaPlayerController.getValue().downloadBackground(songs, save); - if (save) - { - Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size())); - } - else - { - Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size())); - } + if (save) + { + Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size())); + } + else + { + Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size())); } }; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java index 76761f5b..d99ab93c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java @@ -562,7 +562,7 @@ public class SearchFragment extends Fragment { mediaPlayerController.clear(); } - mediaPlayerController.download(Collections.singletonList(song), false, false, false, false, false); + mediaPlayerController.addToPlaylist(Collections.singletonList(song), false, false, false, false, false); if (true) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt index dd4b0d79..ce4fa733 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt @@ -1,449 +1,428 @@ -package org.moire.ultrasonic.service; +package org.moire.ultrasonic.service -import androidx.annotation.Nullable; - -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 timber.log.Timber; - -import static org.koin.java.KoinJavaComponent.inject; -import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; -import static org.moire.ultrasonic.domain.PlayerState.STARTED; +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.util.Util.isExternalStoragePresent +import org.moire.ultrasonic.util.Util.isNetworkConnected +import org.moire.ultrasonic.util.Util.getPreloadCount +import org.moire.ultrasonic.util.Util.getMaxSongs +import org.moire.ultrasonic.util.ShufflePlayBuffer +import timber.log.Timber +import org.moire.ultrasonic.domain.PlayerState +import org.moire.ultrasonic.util.LRUCache +import java.util.ArrayList +import java.util.PriorityQueue +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit /** * This class is responsible for maintaining the playlist and downloading * its items from the network to the filesystem. */ -public class Downloader -{ - public final List downloadList = new ArrayList<>(); - public final List backgroundDownloadList = new ArrayList<>(); +class Downloader( + private val shufflePlayBuffer: ShufflePlayBuffer, + private val externalStorageMonitor: ExternalStorageMonitor, + private val localMediaPlayer: LocalMediaPlayer +): KoinComponent { + val playList: MutableList = ArrayList() + private val downloadQueue: PriorityQueue = PriorityQueue() + private val activelyDownloading: MutableList = ArrayList() - @Nullable - public DownloadFile currentDownloading; + private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject() + + private val downloadFileCache = LRUCache(100) - private final ShufflePlayBuffer shufflePlayBuffer; - private final ExternalStorageMonitor externalStorageMonitor; - private final LocalMediaPlayer localMediaPlayer; + private var executorService: ScheduledExecutorService? = null + var downloadListUpdateRevision: Long = 0 + private set - // TODO: This is a circular reference, try to remove - private final Lazy jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class); - - private final List cleanupCandidates = new ArrayList<>(); - private final LRUCache downloadFileCache = new LRUCache<>(100); - private ScheduledExecutorService executorService; - private long revision; - - public Downloader(ShufflePlayBuffer shufflePlayBuffer, ExternalStorageMonitor externalStorageMonitor, - LocalMediaPlayer localMediaPlayer) - { - this.shufflePlayBuffer = shufflePlayBuffer; - this.externalStorageMonitor = externalStorageMonitor; - this.localMediaPlayer = localMediaPlayer; + val downloadChecker = Runnable { + try { + Timber.w("checking Downloads") + checkDownloadsInternal() + } catch (all: Exception) { + Timber.e(all, "checkDownloads() failed.") + } } - public void onCreate() - { - Runnable downloadChecker = () -> { - try - { - checkDownloads(); + fun onCreate() { + executorService = Executors.newSingleThreadScheduledExecutor() + executorService!!.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS) + Timber.i("Downloader created") + } + + fun onDestroy() { + stop() + clearPlaylist() + clearBackground() + Timber.i("Downloader destroyed") + } + + fun stop() { + if (executorService != null) executorService!!.shutdown() + Timber.i("Downloader stopped") + } + + fun checkDownloads() { + executorService?.execute(downloadChecker) + } + + @Synchronized + fun checkDownloadsInternal() { + if (!isExternalStoragePresent() || !externalStorageMonitor.isExternalStorageAvailable) { + return + } + if (shufflePlayBuffer.isEnabled) { + checkShufflePlay() + } + if (jukeboxMediaPlayer.isEnabled || !isNetworkConnected()) { + return + } + + // Check the active downloads for failures or completions + activelyDownloading.retainAll { + when { + it.isDownloading -> true + it.isFailed && it.shouldRetry() -> { + // Add it back to queue + downloadQueue.add(it) + false + } + else -> { + it.cleanup() + false + } } - catch (Throwable x) - { - Timber.e(x,"checkDownloads() failed."); + } + + + // Check if need to preload more from playlist + val preloadCount = getPreloadCount() + + // Start preloading at the current playing song + var start = if (localMediaPlayer.currentPlaying == null) 0 else currentPlayingIndex + if (start == -1) start = 0 + + var end = (start + preloadCount).coerceAtMost(playList.size) + + // Playlist also contains played songs!!!! + for (i in start until end) { + val download = playList[i] + + // Set correct priority (the lower the number, the higher the priority) + download.priority = i + + // Add file to queue if not in one of the queues already. + if (!download.isWorkDone && !activelyDownloading.contains(download) && !downloadQueue.contains(download)) { + downloadQueue.add(download) } - }; - - executorService = Executors.newSingleThreadScheduledExecutor(); - executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS); - Timber.i("Downloader created"); - } - - public void onDestroy() - { - stop(); - clear(); - clearBackground(); - Timber.i("Downloader destroyed"); - } - - public void stop() - { - if (executorService != null) executorService.shutdown(); - Timber.i("Downloader stopped"); - } - - public synchronized void checkDownloads() - { - if (!Util.isExternalStoragePresent() || !externalStorageMonitor.isExternalStorageAvailable()) - { - return; } - if (shufflePlayBuffer.isEnabled) - { - checkShufflePlay(); - } + // Fill up active List with waiting tasks + while (activelyDownloading.size < PARALLEL_DOWNLOADS && downloadQueue.size > 0 ) { + val task = downloadQueue.remove() + activelyDownloading.add(task) + task.download() - if (jukeboxMediaPlayer.getValue().isEnabled() || !Util.isNetworkConnected()) - { - 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(); + // The next file on the playlist is currently downloading + // TODO: really necessary? + if (playList.indexOf(task) == 1) { + localMediaPlayer.setNextPlayerState(PlayerState.DOWNLOADING) } - - 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; +// fun oldStuff() { +// // 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 || playList.isEmpty() && backgroundDownloadList.isEmpty()) +// ) { +// cleanup() +// return +// } +// +// // There is a target to download +// currentDownloading = null +// val n = playList.size +// var preloaded = 0 +// if (n != 0) { +// var start = if (localMediaPlayer.currentPlaying == null) 0 else currentPlayingIndex +// if (start == -1) start = 0 +// var i = start +// // Check all DownloadFiles on the playlist +// do { +// val downloadFile = playList[i] +// if (!downloadFile.isWorkDone) { +// if (downloadFile.shouldSave() || preloaded < getPreloadCount()) { +// currentDownloading = downloadFile +// currentDownloading!!.download() +// cleanupCandidates.add(currentDownloading) +// if (i == start + 1) { +// // The next file on the playlist is currently downloading +// localMediaPlayer.setNextPlayerState(PlayerState.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 >= getPreloadCount() || playList.isEmpty()) && backgroundDownloadList.isNotEmpty()) { +// var i = 0 +// while (i < backgroundDownloadList.size) { +// val downloadFile = backgroundDownloadList[i] +// if (downloadFile.isWorkDone && (!downloadFile.shouldSave() || downloadFile.isSaved)) { +// scanMedia(downloadFile.completeFile) +// +// // Don't need to keep list like active song list +// backgroundDownloadList.removeAt(i) +// downloadListUpdateRevision++ +// i-- +// } else if (downloadFile.isFailed && !downloadFile.shouldRetry()) { +// // Don't continue to attempt to download forever +// backgroundDownloadList.removeAt(i) +// downloadListUpdateRevision++ +// i-- +// } else { +// currentDownloading = downloadFile +// currentDownloading!!.download() +// cleanupCandidates.add(currentDownloading) +// break +// } +// i++ +// } +// } +// +// } - if (n != 0) - { - int start = localMediaPlayer.currentPlaying == null ? 0 : getCurrentPlayingIndex(); - if (start == -1) start = 0; + @get:Synchronized + val currentPlayingIndex: Int + get() = playList.indexOf(localMediaPlayer.currentPlaying) - int i = start; - // Check all DownloadFiles on the playlist - do - { - DownloadFile downloadFile = downloadList.get(i); - if (!downloadFile.isWorkDone()) - { - if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount()) - { - currentDownloading = downloadFile; - currentDownloading.download(); - cleanupCandidates.add(currentDownloading); - if (i == (start + 1)) - { - // The next file on the playlist is currently downloading - localMediaPlayer.setNextPlayerState(DOWNLOADING); + @get:Synchronized + val downloadListDuration: Long + get() { + var totalDuration: Long = 0 + for (downloadFile in playList) { + val song = downloadFile.song + if (!song.isDirectory) { + if (song.artist != null) { + if (song.duration != null) { + totalDuration += song.duration!!.toLong() } - 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() || downloadList.isEmpty()) && !backgroundDownloadList.isEmpty()) - { - for (int i = 0; i < backgroundDownloadList.size(); i++) - { - DownloadFile downloadFile = backgroundDownloadList.get(i); - if (downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved())) - { - Util.scanMedia(downloadFile.getCompleteFile()); - - // Don't need to keep list like active song list - backgroundDownloadList.remove(i); - revision++; - i--; - } - else if (downloadFile.isFailed() && !downloadFile.shouldRetry()) { - // Don't continue to attempt to download forever - 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 } - return totalDuration; + @get:Synchronized + val downloads: List + get() { + val temp: MutableList = ArrayList() + temp.addAll(playList) + temp.addAll(activelyDownloading) + temp.addAll(downloadQueue) + return temp.distinct() + } + + @Synchronized + fun clearPlaylist() { + playList.clear() + + // Cancel all active downloads with a high priority + for (download in activelyDownloading) { + if (download.priority < 100) + download.cancelDownload() + } + + downloadListUpdateRevision++ } - public synchronized List getDownloads() - { - List temp = new ArrayList<>(); - temp.addAll(downloadList); - temp.addAll(backgroundDownloadList); - return temp; + @Synchronized + private fun clearBackground() { + // Clear the pending queue + downloadQueue.clear() + + // Cancel all active downloads with a low priority + for (download in activelyDownloading) { + if (download.priority >= 100) + download.cancelDownload() + } + + downloadListUpdateRevision++ } - public long getDownloadListUpdateRevision() - { - return revision; - } - - public synchronized void clear() - { - downloadList.clear(); - revision++; - if (currentDownloading != null) - { - currentDownloading.cancelDownload(); - currentDownloading = null; + @Synchronized + fun clearActiveDownloads() { + // Cancel all active downloads with a low priority + for (download in activelyDownloading) { + download.cancelDownload() } } - private void clearBackground() - { - if (currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) - { - currentDownloading.cancelDownload(); - currentDownloading = null; + @Synchronized + fun removeFromPlaylist(downloadFile: DownloadFile) { + if (activelyDownloading.contains(downloadFile)) { + downloadFile.cancelDownload() } - backgroundDownloadList.clear(); + playList.remove(downloadFile) + downloadListUpdateRevision++ } - public synchronized void removeDownloadFile(DownloadFile downloadFile) - { - if (downloadFile == currentDownloading) - { - currentDownloading.cancelDownload(); - currentDownloading = null; + @Synchronized + fun addToPlaylist( + songs: List, + save: Boolean, + autoPlay: Boolean, + playNext: Boolean, + newPlaylist: Boolean + ) { + shufflePlayBuffer.isEnabled = false + var offset = 1 + if (songs.isEmpty()) { + return } - - downloadList.remove(downloadFile); - backgroundDownloadList.remove(downloadFile); - revision++; - } - - public synchronized void download(List songs, boolean save, boolean autoPlay, boolean playNext, boolean newPlaylist) - { - shufflePlayBuffer.isEnabled = false; - int offset = 1; - - if (songs.isEmpty()) - { - return; + if (newPlaylist) { + playList.clear() } - - if (newPlaylist) - { - downloadList.clear(); - } - - if (playNext) - { - if (autoPlay && getCurrentPlayingIndex() >= 0) - { - offset = 0; + if (playNext) { + if (autoPlay && currentPlayingIndex >= 0) { + offset = 0 } - - for (MusicDirectory.Entry song : songs) - { - DownloadFile downloadFile = new DownloadFile(song, save); - downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); - offset++; + for (song in songs) { + val downloadFile = DownloadFile(song!!, save) + playList.add(currentPlayingIndex + offset, downloadFile) + offset++ + } + } else { + for (song in songs) { + val downloadFile = DownloadFile(song!!, save) + playList.add(downloadFile) } } - else - { - for (MusicDirectory.Entry song : songs) - { - DownloadFile downloadFile = new DownloadFile(song, save); - downloadList.add(downloadFile); + downloadListUpdateRevision++ + //checkDownloads() + } + + @Synchronized + fun downloadBackground(songs: List, save: Boolean) { + + // Because of the priority handling we add the songs in the reverse order they + // were requested, then it is correct in the end. + for (song in songs.asReversed()) { + downloadQueue.add(DownloadFile(song, save)) + } + + downloadListUpdateRevision++ + //checkDownloads() + } + + @Synchronized + fun shuffle() { + playList.shuffle() + + // Move the current song to the top.. + if (localMediaPlayer.currentPlaying != null) { + playList.remove(localMediaPlayer.currentPlaying) + playList.add(0, localMediaPlayer.currentPlaying!!) + } + + downloadListUpdateRevision++ + } + + @Synchronized + fun getDownloadFileForSong(song: MusicDirectory.Entry): DownloadFile { + for (downloadFile in playList) { + if (downloadFile.song == song) { + return downloadFile } } - revision++; - } - - public synchronized void downloadBackground(List songs, boolean save) - { - for (MusicDirectory.Entry song : songs) - { - DownloadFile downloadFile = new DownloadFile(song, save); - backgroundDownloadList.add(downloadFile); - } - - revision++; - - checkDownloads(); - } - - public synchronized void shuffle() - { - Collections.shuffle(downloadList); - if (localMediaPlayer.currentPlaying != null) - { - downloadList.remove(localMediaPlayer.currentPlaying); - 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 in activelyDownloading) { + if (downloadFile.song == song) { + return downloadFile } } - for (DownloadFile downloadFile : backgroundDownloadList) - { - if (downloadFile.getSong().equals(song)) - { - return downloadFile; + for (downloadFile in downloadQueue) { + if (downloadFile.song == song) { + return downloadFile } } - - DownloadFile downloadFile = downloadFileCache.get(song); - if (downloadFile == null) - { - downloadFile = new DownloadFile(song, false); - downloadFileCache.put(song, downloadFile); + var downloadFile = downloadFileCache[song] + if (downloadFile == null) { + downloadFile = DownloadFile(song, false) + downloadFileCache.put(song, downloadFile) } - return downloadFile; + return downloadFile } - private synchronized void cleanup() - { - Iterator 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() - { + @Synchronized + private fun checkShufflePlay() { // Get users desired random playlist size - int listSize = Util.getMaxSongs(); - boolean wasEmpty = downloadList.isEmpty(); - - long revisionBefore = revision; + val listSize = getMaxSongs() + val wasEmpty = playList.isEmpty() + val revisionBefore = downloadListUpdateRevision // 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(song, false); - downloadList.add(downloadFile); - revision++; + val size = playList.size + if (size < listSize) { + for (song in shufflePlayBuffer[listSize - size]) { + val downloadFile = DownloadFile(song, false) + playList.add(downloadFile) + downloadListUpdateRevision++ } } - - int currIndex = localMediaPlayer.currentPlaying == null ? 0 : getCurrentPlayingIndex(); + + val currIndex = if (localMediaPlayer.currentPlaying == null) 0 else currentPlayingIndex // 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(song, false)); - downloadList.get(0).cancelDownload(); - downloadList.remove(0); - revision++; + if (currIndex > 4) { + val songsToShift = currIndex - 2 + for (song in shufflePlayBuffer[songsToShift]) { + playList.add(DownloadFile(song, false)) + playList[0].cancelDownload() + playList.removeAt(0) + downloadListUpdateRevision++ } } - - if (revisionBefore != revision) - { - jukeboxMediaPlayer.getValue().updatePlaylist(); + if (revisionBefore != downloadListUpdateRevision) { + jukeboxMediaPlayer.updatePlaylist() } - - if (wasEmpty && !downloadList.isEmpty()) - { - if (jukeboxMediaPlayer.getValue().isEnabled()) - { - jukeboxMediaPlayer.getValue().skip(0, 0); - localMediaPlayer.setPlayerState(STARTED); - } - else - { - localMediaPlayer.play(downloadList.get(0)); + if (wasEmpty && playList.isNotEmpty()) { + if (jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.skip(0, 0) + localMediaPlayer.setPlayerState(PlayerState.STARTED) + } else { + localMediaPlayer.play(playList[0]) } } } + companion object { + const val PARALLEL_DOWNLOADS = 3 + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 718f34a1..4dca6c5f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -456,7 +456,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon requireActivity().invalidateOptionsMenu() } - // Scroll to current playing/downloading. + // Scroll to current playing. private fun scrollToCurrent() { val adapter = playlistView.adapter if (adapter != null) { @@ -467,13 +467,6 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon return } } - val currentDownloading = mediaPlayerController.currentDownloading - for (i in 0 until count) { - if (currentDownloading == playlistView.getItemAtPosition(i)) { - playlistView.smoothScrollToPositionFromTop(i, 40) - return - } - } } } @@ -643,7 +636,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon return true } R.id.menu_remove -> { - mediaPlayerController.remove(song!!) + mediaPlayerController.removeFromPlaylist(song!!) onDownloadListChanged() return true } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index f370ab9f..35aefe19 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -517,6 +517,7 @@ class TrackCollectionFragment : Fragment() { var pinnedCount = 0 for (song in selection) { + if (song == null) continue val downloadFile = mediaPlayerController.getDownloadFileForSong(song) if (downloadFile.isWorkDone) { deleteEnabled = true diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt index 5bdfe45b..c3fc3b95 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AutoMediaBrowserService.kt @@ -1066,7 +1066,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } private fun playSongs(songs: List?) { - mediaPlayerController.download( + mediaPlayerController.addToPlaylist( songs, save = false, autoPlay = true, @@ -1077,7 +1077,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() { } private fun playSong(song: MusicDirectory.Entry) { - mediaPlayerController.download( + mediaPlayerController.addToPlaylist( listOf(song), save = false, autoPlay = false, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index f8e88257..6b4c03b9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -40,7 +40,7 @@ import timber.log.Timber class DownloadFile( val song: MusicDirectory.Entry, private val save: Boolean -) : KoinComponent { +) : KoinComponent, Comparable { val partialFile: File val completeFile: File private val saveFile: File = FileUtil.getSongFile(song) @@ -50,6 +50,8 @@ class DownloadFile( private val desiredBitRate: Int = Util.getMaxBitRate() + var priority = 100 + @Volatile private var isPlaying = false @@ -387,6 +389,10 @@ class DownloadFile( } } + override fun compareTo(other: DownloadFile): Int { + return priority.compareTo(other.priority) + } + companion object { const val MAX_RETRIES = 5 } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index beb71844..61324184 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -74,7 +74,7 @@ class MediaPlayerController( autoPlay: Boolean, newPlaylist: Boolean ) { - download( + addToPlaylist( songs, save = false, autoPlay = false, @@ -167,7 +167,7 @@ class MediaPlayerController( @Synchronized @Suppress("LongParameterList") - fun download( + fun addToPlaylist( songs: List?, save: Boolean, autoPlay: Boolean, @@ -175,10 +175,12 @@ class MediaPlayerController( shuffle: Boolean, newPlaylist: Boolean ) { - downloader.download(songs, save, autoPlay, playNext, newPlaylist) + if (songs == null) return + val filteredSongs = songs.filterNotNull() + downloader.addToPlaylist(filteredSongs, save, autoPlay, playNext, newPlaylist) jukeboxMediaPlayer.updatePlaylist() if (shuffle) shuffle() - val isLastTrack = (downloader.downloadList.size - 1 == downloader.currentPlayingIndex) + val isLastTrack = (downloader.playList.size - 1 == downloader.currentPlayingIndex) if (!playNext && !autoPlay && isLastTrack) { val mediaPlayerService = runningInstance @@ -188,15 +190,15 @@ class MediaPlayerController( if (autoPlay) { play(0) } else { - if (localMediaPlayer.currentPlaying == null && downloader.downloadList.size > 0) { - localMediaPlayer.currentPlaying = downloader.downloadList[0] - downloader.downloadList[0].setPlaying(true) + if (localMediaPlayer.currentPlaying == null && downloader.playList.size > 0) { + localMediaPlayer.currentPlaying = downloader.playList[0] + downloader.playList[0].setPlaying(true) } downloader.checkDownloads() } downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, + downloader.playList, downloader.currentPlayingIndex, playerPosition ) @@ -204,9 +206,11 @@ class MediaPlayerController( @Synchronized fun downloadBackground(songs: List?, save: Boolean) { - downloader.downloadBackground(songs, save) + if (songs == null) return + val filteredSongs = songs.filterNotNull() + downloader.downloadBackground(filteredSongs, save) downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, + downloader.playList, downloader.currentPlayingIndex, playerPosition ) @@ -237,7 +241,7 @@ class MediaPlayerController( fun shuffle() { downloader.shuffle() downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, + downloader.playList, downloader.currentPlayingIndex, playerPosition ) @@ -267,10 +271,10 @@ class MediaPlayerController( mediaPlayerService.clear(serialize) } else { // If no MediaPlayerService is available, just empty the playlist - downloader.clear() + downloader.clearPlaylist() if (serialize) { downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, + downloader.playList, downloader.currentPlayingIndex, playerPosition ) } @@ -281,7 +285,7 @@ class MediaPlayerController( @Synchronized fun clearIncomplete() { reset() - val iterator = downloader.downloadList.iterator() + val iterator = downloader.playList.iterator() while (iterator.hasNext()) { val downloadFile = iterator.next() if (!downloadFile.isCompleteFileAvailable) { @@ -290,7 +294,7 @@ class MediaPlayerController( } downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, + downloader.playList, downloader.currentPlayingIndex, playerPosition ) @@ -299,15 +303,15 @@ class MediaPlayerController( } @Synchronized - fun remove(downloadFile: DownloadFile) { + fun removeFromPlaylist(downloadFile: DownloadFile) { if (downloadFile == localMediaPlayer.currentPlaying) { reset() currentPlaying = null } - downloader.removeDownloadFile(downloadFile) + downloader.removeFromPlaylist(downloadFile) downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, + downloader.playList, downloader.currentPlayingIndex, playerPosition ) @@ -321,15 +325,17 @@ class MediaPlayerController( } @Synchronized + // TODO: Make it require not null fun delete(songs: List) { - for (song in songs) { + for (song in songs.filterNotNull()) { downloader.getDownloadFileForSong(song).delete() } } @Synchronized + // TODO: Make it require not null fun unpin(songs: List) { - for (song in songs) { + for (song in songs.filterNotNull()) { downloader.getDownloadFileForSong(song).unpin() } } @@ -357,12 +363,12 @@ class MediaPlayerController( when (repeatMode) { RepeatMode.SINGLE, RepeatMode.OFF -> { // Play next if exists - if (index + 1 >= 0 && index + 1 < downloader.downloadList.size) { + if (index + 1 >= 0 && index + 1 < downloader.playList.size) { play(index + 1) } } RepeatMode.ALL -> { - play((index + 1) % downloader.downloadList.size) + play((index + 1) % downloader.playList.size) } else -> { } @@ -409,7 +415,7 @@ class MediaPlayerController( reset() // Cancel current download, if necessary. - downloader.currentDownloading?.cancelDownload() + downloader.clearActiveDownloads() } else { jukeboxMediaPlayer.stopJukeboxService() } @@ -489,16 +495,13 @@ class MediaPlayerController( } val playlistSize: Int - get() = downloader.downloadList.size + get() = downloader.playList.size val currentPlayingNumberOnPlaylist: Int get() = downloader.currentPlayingIndex - val currentDownloading: DownloadFile? - get() = downloader.currentDownloading - val playList: List - get() = downloader.downloadList + get() = downloader.playList val playListUpdateRevision: Long get() = downloader.downloadListUpdateRevision @@ -506,7 +509,7 @@ class MediaPlayerController( val playListDuration: Long get() = downloader.downloadListDuration - fun getDownloadFileForSong(song: MusicDirectory.Entry?): DownloadFile { + fun getDownloadFileForSong(song: MusicDirectory.Entry): DownloadFile { return downloader.getDownloadFileForSong(song) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 3b9a1800..19572a2d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -76,7 +76,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { // Work-around: Serialize again, as the restore() method creates a // serialization without current playing info. downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, + downloader.playList, downloader.currentPlayingIndex, mediaPlayerController.playerPosition ) @@ -94,7 +94,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { if (!created) return downloadQueueSerializer.serializeDownloadQueueNow( - downloader.downloadList, + downloader.playList, downloader.currentPlayingIndex, mediaPlayerController.playerPosition ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index 4e6c14d7..318e7b08 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -88,7 +88,7 @@ class MediaPlayerService : Service() { localMediaPlayer.onPrepared = { downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, + downloader.playList, downloader.currentPlayingIndex, playerPosition ) @@ -189,7 +189,7 @@ class MediaPlayerService : Service() { @Synchronized fun setCurrentPlaying(currentPlayingIndex: Int) { try { - localMediaPlayer.setCurrentPlaying(downloader.downloadList[currentPlayingIndex]) + localMediaPlayer.setCurrentPlaying(downloader.playList[currentPlayingIndex]) } catch (ignored: IndexOutOfBoundsException) { } } @@ -208,7 +208,7 @@ class MediaPlayerService : Service() { if (index != -1) { when (repeatMode) { RepeatMode.OFF -> index += 1 - RepeatMode.ALL -> index = (index + 1) % downloader.downloadList.size + RepeatMode.ALL -> index = (index + 1) % downloader.playList.size RepeatMode.SINGLE -> { } else -> { @@ -217,8 +217,8 @@ class MediaPlayerService : Service() { } localMediaPlayer.clearNextPlaying(false) - if (index < downloader.downloadList.size && index != -1) { - localMediaPlayer.setNextPlaying(downloader.downloadList[index]) + if (index < downloader.playList.size && index != -1) { + localMediaPlayer.setNextPlaying(downloader.playList[index]) } else { localMediaPlayer.clearNextPlaying(true) } @@ -271,7 +271,7 @@ class MediaPlayerService : Service() { @Synchronized fun play(index: Int, start: Boolean) { Timber.v("play requested for %d", index) - if (index < 0 || index >= downloader.downloadList.size) { + if (index < 0 || index >= downloader.playList.size) { resetPlayback() } else { setCurrentPlaying(index) @@ -280,7 +280,7 @@ class MediaPlayerService : Service() { jukeboxMediaPlayer.skip(index, 0) localMediaPlayer.setPlayerState(PlayerState.STARTED) } else { - localMediaPlayer.play(downloader.downloadList[index]) + localMediaPlayer.play(downloader.playList[index]) } } downloader.checkDownloads() @@ -293,7 +293,7 @@ class MediaPlayerService : Service() { localMediaPlayer.reset() localMediaPlayer.setCurrentPlaying(null) downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, + downloader.playList, downloader.currentPlayingIndex, playerPosition ) } @@ -395,7 +395,7 @@ class MediaPlayerService : Service() { if (playerState === PlayerState.PAUSED) { downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, downloader.currentPlayingIndex, playerPosition + downloader.playList, downloader.currentPlayingIndex, playerPosition ) } @@ -408,8 +408,8 @@ class MediaPlayerService : Service() { Util.broadcastPlaybackStatusChange(context, playerState) Util.broadcastA2dpPlayStatusChange( context, playerState, song, - downloader.downloadList.size + downloader.backgroundDownloadList.size, - downloader.downloadList.indexOf(currentPlaying) + 1, playerPosition + downloader.playList.size, + downloader.playList.indexOf(currentPlaying) + 1, playerPosition ) // Update widget @@ -455,7 +455,7 @@ class MediaPlayerService : Service() { if (index != -1) { when (repeatMode) { RepeatMode.OFF -> { - if (index + 1 < 0 || index + 1 >= downloader.downloadList.size) { + if (index + 1 < 0 || index + 1 >= downloader.playList.size) { if (Util.getShouldClearPlaylist()) { clear(true) jukeboxMediaPlayer.updatePlaylist() @@ -466,7 +466,7 @@ class MediaPlayerService : Service() { } } RepeatMode.ALL -> { - play((index + 1) % downloader.downloadList.size) + play((index + 1) % downloader.playList.size) } RepeatMode.SINGLE -> play(index) else -> { @@ -480,12 +480,12 @@ class MediaPlayerService : Service() { @Synchronized fun clear(serialize: Boolean) { localMediaPlayer.reset() - downloader.clear() + downloader.clearPlaylist() localMediaPlayer.setCurrentPlaying(null) setNextPlaying() if (serialize) { downloadQueueSerializer.serializeDownloadQueue( - downloader.downloadList, + downloader.playList, downloader.currentPlayingIndex, playerPosition ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index b30490ac..a0f21f2c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -39,7 +39,7 @@ class DownloadHandler( mediaPlayerController.clear() } networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.download( + mediaPlayerController.addToPlaylist( songs, save, autoPlay, @@ -297,7 +297,7 @@ class DownloadHandler( if (unpin) { mediaPlayerController.unpin(songs) } else { - mediaPlayerController.download( + mediaPlayerController.addToPlaylist( songs, save, autoPlay, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt index 54fcf3a1..16dd3a3a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt @@ -218,10 +218,13 @@ class SongView(context: Context) : UpdateView(context), Checkable, KoinComponent override fun updateBackground() {} + @Synchronized public override fun update() { updateBackground() - downloadFile = mediaPlayerController.getDownloadFileForSong(entry) + val song = entry ?: return + + downloadFile = mediaPlayerController.getDownloadFileForSong(song) updateDownloadStatus(downloadFile!!)