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.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java deleted file mode 100644 index f768de46..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java +++ /dev/null @@ -1,445 +0,0 @@ -package org.moire.ultrasonic.service; - -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; - -/** - * 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<>(); - public DownloadFile currentDownloading; - - private final ShufflePlayBuffer shufflePlayBuffer; - private final ExternalStorageMonitor externalStorageMonitor; - private final LocalMediaPlayer localMediaPlayer; - - // 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; - } - - public void onCreate() - { - Runnable downloadChecker = () -> { - try - { - checkDownloads(); - } - catch (Throwable x) - { - Timber.e(x,"checkDownloads() failed."); - } - }; - - 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(); - } - - 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(); - } - - 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()) - { - 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() || 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; - } - - public synchronized List getDownloads() - { - List 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 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(song, save); - downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); - offset++; - } - } - else - { - for (MusicDirectory.Entry song : songs) - { - DownloadFile downloadFile = new DownloadFile(song, save); - downloadList.add(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 downloadFile : backgroundDownloadList) - { - if (downloadFile.getSong().equals(song)) - { - return downloadFile; - } - } - - DownloadFile downloadFile = downloadFileCache.get(song); - if (downloadFile == null) - { - downloadFile = new DownloadFile(song, false); - downloadFileCache.put(song, 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() - { - // Get users desired random playlist size - int listSize = Util.getMaxSongs(); - 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(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(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)); - } - } - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt new file mode 100644 index 00000000..56984123 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.kt @@ -0,0 +1,353 @@ +package org.moire.ultrasonic.service + +import java.util.ArrayList +import java.util.PriorityQueue +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.PlayerState +import org.moire.ultrasonic.util.LRUCache +import org.moire.ultrasonic.util.ShufflePlayBuffer +import org.moire.ultrasonic.util.Util.getMaxSongs +import org.moire.ultrasonic.util.Util.getPreloadCount +import org.moire.ultrasonic.util.Util.isExternalStoragePresent +import org.moire.ultrasonic.util.Util.isNetworkConnected +import timber.log.Timber + +/** + * This class is responsible for maintaining the playlist and downloading + * its items from the network to the filesystem. + * + * TODO: Implement LiveData + * TODO: Move away from managing the queue with scheduled checks, instead use callbacks when + * Downloads are finished + */ +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() + + private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject() + + private val downloadFileCache = LRUCache(100) + + private var executorService: ScheduledExecutorService? = null + + var playlistUpdateRevision: Long = 0 + private set + + val downloadChecker = Runnable { + try { + Timber.w("checking Downloads") + checkDownloadsInternal() + } catch (all: Exception) { + Timber.e(all, "checkDownloads() failed.") + } + } + + fun onCreate() { + executorService = Executors.newSingleThreadScheduledExecutor() + executorService!!.scheduleWithFixedDelay( + downloadChecker, CHECK_INTERVAL, CHECK_INTERVAL, 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 and remove them + cleanupActiveDownloads() + + // Check if need to preload more from playlist + val preloadCount = getPreloadCount() + + // Start preloading at the current playing song + var start = currentPlayingIndex + if (start == -1) start = 0 + + val end = (start + preloadCount).coerceAtMost(playlist.size) + + 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) + } + } + + // Fill up active List with waiting tasks + while (activelyDownloading.size < PARALLEL_DOWNLOADS && downloadQueue.size > 0) { + val task = downloadQueue.remove() + activelyDownloading.add(task) + task.download() + + // The next file on the playlist is currently downloading + if (playlist.indexOf(task) == 1) { + localMediaPlayer.setNextPlayerState(PlayerState.DOWNLOADING) + } + } + } + + private fun cleanupActiveDownloads() { + activelyDownloading.retainAll { + when { + it.isDownloading -> true + it.isFailed && it.shouldRetry() -> { + // Add it back to queue + downloadQueue.add(it) + false + } + else -> { + it.cleanup() + false + } + } + } + } + + @get:Synchronized + val currentPlayingIndex: Int + get() = playlist.indexOf(localMediaPlayer.currentPlaying) + + @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() + } + } + } + } + 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() + } + + playlistUpdateRevision++ + } + + @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() + } + } + + @Synchronized + fun clearActiveDownloads() { + // Cancel all active downloads with a low priority + for (download in activelyDownloading) { + download.cancelDownload() + } + } + + @Synchronized + fun removeFromPlaylist(downloadFile: DownloadFile) { + if (activelyDownloading.contains(downloadFile)) { + downloadFile.cancelDownload() + } + playlist.remove(downloadFile) + playlistUpdateRevision++ + } + + @Synchronized + fun addToPlaylist( + songs: List, + save: Boolean, + autoPlay: Boolean, + playNext: Boolean, + newPlaylist: Boolean + ) { + shufflePlayBuffer.isEnabled = false + var offset = 1 + if (songs.isEmpty()) { + return + } + if (newPlaylist) { + playlist.clear() + } + if (playNext) { + if (autoPlay && currentPlayingIndex >= 0) { + offset = 0 + } + 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) + } + } + playlistUpdateRevision++ + 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)) + } + + 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!!) + } + + playlistUpdateRevision++ + } + + @Synchronized + @Suppress("ReturnCount") + fun getDownloadFileForSong(song: MusicDirectory.Entry): DownloadFile { + for (downloadFile in playlist) { + if (downloadFile.song == song) { + return downloadFile + } + } + for (downloadFile in activelyDownloading) { + if (downloadFile.song == song) { + return downloadFile + } + } + for (downloadFile in downloadQueue) { + if (downloadFile.song == song) { + return downloadFile + } + } + var downloadFile = downloadFileCache[song] + if (downloadFile == null) { + downloadFile = DownloadFile(song, false) + downloadFileCache.put(song, downloadFile) + } + return downloadFile + } + + @Synchronized + private fun checkShufflePlay() { + // Get users desired random playlist size + val listSize = getMaxSongs() + val wasEmpty = playlist.isEmpty() + val revisionBefore = playlistUpdateRevision + + // First, ensure that list is at least 20 songs long. + val size = playlist.size + if (size < listSize) { + for (song in shufflePlayBuffer[listSize - size]) { + val downloadFile = DownloadFile(song, false) + playlist.add(downloadFile) + playlistUpdateRevision++ + } + } + + val currIndex = if (localMediaPlayer.currentPlaying == null) 0 else currentPlayingIndex + + // Only shift playlist if playing song #5 or later. + if (currIndex > SHUFFLE_BUFFER_LIMIT) { + val songsToShift = currIndex - 2 + for (song in shufflePlayBuffer[songsToShift]) { + playlist.add(DownloadFile(song, false)) + playlist[0].cancelDownload() + playlist.removeAt(0) + playlistUpdateRevision++ + } + } + if (revisionBefore != playlistUpdateRevision) { + jukeboxMediaPlayer.updatePlaylist() + } + 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 + const val CHECK_INTERVAL = 5L + const val SHUFFLE_BUFFER_LIMIT = 4 + } +} 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..cc82c25d 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 @@ -202,7 +204,6 @@ class DownloadFile( return String.format("DownloadFile (%s)", song) } - @Suppress("TooGenericExceptionCaught") private inner class DownloadTask : CancellableTask() { override fun execute() { var inputStream: InputStream? = null @@ -290,7 +291,7 @@ class DownloadFile( Util.renameFile(partialFile, completeFile) } } - } catch (e: Exception) { + } catch (all: Exception) { Util.close(outputStream) Util.delete(completeFile) Util.delete(saveFile) @@ -299,7 +300,7 @@ class DownloadFile( if (retryCount > 0) { --retryCount } - Timber.w(e, "Failed to download '%s'.", song) + Timber.w(all, "Failed to download '%s'.", song) } } finally { Util.close(inputStream) @@ -337,8 +338,8 @@ class DownloadFile( // Download the largest size that we can display in the UI imageLoaderProvider.getImageLoader().cacheCoverArt(song) } - } catch (e: Exception) { - Timber.e(e, "Failed to get cover art.") + } catch (all: Exception) { + Timber.e(all, "Failed to get cover art.") } } @@ -387,6 +388,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 994f06e8..c3b400b1 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,9 +415,7 @@ class MediaPlayerController( reset() // Cancel current download, if necessary. - if (downloader.currentDownloading != null) { - downloader.currentDownloading.cancelDownload() - } + downloader.clearActiveDownloads() } else { jukeboxMediaPlayer.stopJukeboxService() } @@ -491,24 +495,21 @@ 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 + get() = downloader.playlistUpdateRevision 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..31c7df38 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..7f234a75 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!!)