diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce6ce6f2..dc011630 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] # You need to run ./gradlew wrapper after updating the version -gradle = "7.6" +gradle = "8.1.1" navigation = "2.5.3" gradlePlugin = "8.0.1" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 8b0f1241..80a78416 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -21,6 +21,7 @@ import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign +import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.util.Settings @@ -129,81 +130,54 @@ abstract class EntryListFragment : MultiListFragment() { ): Boolean { when (menuItem.itemId) { R.id.menu_play_now -> - downloadHandler.downloadRecursively( + downloadHandler.fetchTracksAndAddToController( fragment, item.id, - save = false, append = false, autoPlay = true, shuffle = false, - background = false, playNext = false, - unpin = false, isArtist = isArtist ) R.id.menu_play_next -> - downloadHandler.downloadRecursively( + downloadHandler.fetchTracksAndAddToController( fragment, item.id, - save = false, append = false, autoPlay = true, shuffle = true, - background = false, playNext = true, - unpin = false, isArtist = isArtist ) R.id.menu_play_last -> - downloadHandler.downloadRecursively( + downloadHandler.fetchTracksAndAddToController( fragment, item.id, - save = false, append = true, autoPlay = false, shuffle = false, - background = false, playNext = false, - unpin = false, isArtist = isArtist ) R.id.menu_pin -> - downloadHandler.downloadRecursively( + downloadHandler.justDownload( + action = DownloadAction.PIN, fragment, item.id, - save = true, - append = true, - autoPlay = false, - shuffle = false, - background = false, - playNext = false, - unpin = false, isArtist = isArtist ) R.id.menu_unpin -> - downloadHandler.downloadRecursively( + downloadHandler.justDownload( + action = DownloadAction.UNPIN, fragment, item.id, - save = false, - append = false, - autoPlay = false, - shuffle = false, - background = false, - playNext = false, - unpin = true, isArtist = isArtist ) R.id.menu_download -> - downloadHandler.downloadRecursively( + downloadHandler.justDownload( + action = DownloadAction.DOWNLOAD, fragment, item.id, - save = false, - append = false, - autoPlay = false, - shuffle = false, - background = true, - playNext = false, - unpin = false, isArtist = isArtist ) else -> return false diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 323f8d05..9dfcc1ef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -309,7 +309,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { } mediaPlayerController.addToPlaylist( listOf(song), - cachePermanently = false, autoPlay = false, shuffle = false, insertionMode = MediaPlayerController.InsertionMode.APPEND @@ -367,40 +366,37 @@ class SearchFragment : MultiListFragment(), KoinComponent { when (menuItem.itemId) { R.id.song_menu_play_now -> { songs.add(item) - downloadHandler.download( - fragment = this, - append = false, - save = false, - autoPlay = true, - playNext = false, - shuffle = false, + downloadHandler.addTracksToMediaController( songs = songs, + append = false, + playNext = false, + autoPlay = true, + shuffle = false, + fragment = this, playlistName = null ) } R.id.song_menu_play_next -> { songs.add(item) - downloadHandler.download( - fragment = this, - append = true, - save = false, - autoPlay = false, - playNext = true, - shuffle = false, + downloadHandler.addTracksToMediaController( songs = songs, + append = true, + playNext = true, + autoPlay = false, + shuffle = false, + fragment = this, playlistName = null ) } R.id.song_menu_play_last -> { songs.add(item) - downloadHandler.download( - fragment = this, - append = true, - save = false, - autoPlay = false, - playNext = false, - shuffle = false, + downloadHandler.addTracksToMediaController( songs = songs, + append = true, + playNext = false, + autoPlay = false, + shuffle = false, + fragment = this, playlistName = null ) } 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 6664f17e..85e466dd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -40,11 +40,10 @@ import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.model.TrackCollectionModel -import org.moire.ultrasonic.service.DownloadService import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign -import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker +import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.VideoPlayer import org.moire.ultrasonic.util.CancellationToken @@ -84,7 +83,6 @@ open class TrackCollectionFragment( private var shareButton: MenuItem? = null internal val mediaPlayerController: MediaPlayerController by inject() - private val networkAndStorageChecker: NetworkAndStorageChecker by inject() private val shareHandler: ShareHandler by inject() internal var cancellationToken: CancellationToken? = null @@ -211,11 +209,14 @@ open class TrackCollectionFragment( } playNextButton?.setOnClickListener { - downloadHandler.download( - this@TrackCollectionFragment, append = true, - save = false, autoPlay = false, playNext = true, shuffle = false, + downloadHandler.addTracksToMediaController( songs = getSelectedSongs(), - playlistName = navArgs.playlistName + append = true, + playNext = true, + autoPlay = false, + shuffle = false, + playlistName = navArgs.playlistName, + this@TrackCollectionFragment ) } @@ -304,9 +305,14 @@ open class TrackCollectionFragment( selectedSongs: List = getSelectedSongs() ) { if (selectedSongs.isNotEmpty()) { - downloadHandler.download( - this, append, false, !append, playNext = false, - shuffle = false, songs = selectedSongs, null + downloadHandler.addTracksToMediaController( + songs = selectedSongs, + append = append, + playNext = false, + autoPlay = !append, + shuffle = false, + playlistName = null, + fragment = this ) } else { playAll(false, append) @@ -337,31 +343,29 @@ open class TrackCollectionFragment( } val isArtist = navArgs.isArtist - val id = navArgs.id + + // Need a valid id to download stuff + val id = navArgs.id ?: return if (hasSubFolders) { - downloadHandler.downloadRecursively( + downloadHandler.fetchTracksAndAddToController( fragment = this, id = id, - save = false, append = append, autoPlay = !append, shuffle = shuffle, - background = false, playNext = false, - unpin = false, isArtist = isArtist ) } else { - downloadHandler.download( - fragment = this, - append = append, - save = false, - autoPlay = !append, - playNext = false, - shuffle = shuffle, + downloadHandler.addTracksToMediaController( songs = getAllSongs(), - playlistName = navArgs.playlistName + append = append, + playNext = false, + autoPlay = !append, + shuffle = shuffle, + playlistName = navArgs.playlistName, + fragment = this ) } } @@ -416,62 +420,35 @@ open class TrackCollectionFragment( } } - private fun downloadBackground(save: Boolean) { - var songs = getSelectedSongs() + private fun downloadBackground(save: Boolean, tracks: List = getSelectedSongs()) { + var songs = tracks if (songs.isEmpty()) { songs = getAllSongs() } - downloadBackground(save, songs) - } - - private fun downloadBackground( - save: Boolean, - songs: List - ) { - val onValid = Runnable { - networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - DownloadService.download(songs.filterNotNull(), save) - - if (save) { - Util.toast( - context, - resources.getQuantityString( - R.plurals.select_album_n_songs_pinned, songs.size, songs.size - ) - ) - } else { - Util.toast( - context, - resources.getQuantityString( - R.plurals.select_album_n_songs_downloaded, songs.size, songs.size - ) - ) - } - } - onValid.run() + val action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD + downloadHandler.justDownload( + action = action, + fragment = this, + tracks = songs + ) } internal fun delete(songs: List = getSelectedSongs()) { - Util.toast( - context, - resources.getQuantityString( - R.plurals.select_album_n_songs_deleted, songs.size, songs.size - ) + downloadHandler.justDownload( + action = DownloadAction.DELETE, + fragment = this, + tracks = songs ) - - DownloadService.delete(songs) } internal fun unpin(songs: List = getSelectedSongs()) { - Util.toast( - context, - resources.getQuantityString( - R.plurals.select_album_n_songs_unpinned, songs.size, songs.size - ) + downloadHandler.justDownload( + action = DownloadAction.UNPIN, + fragment = this, + tracks = songs ) - DownloadService.unpin(songs) } override val defaultObserver: (List) -> Unit = { @@ -637,15 +614,14 @@ open class TrackCollectionFragment( playNow(false, songs) } R.id.song_menu_play_next -> { - downloadHandler.download( - fragment = this@TrackCollectionFragment, - append = true, - save = false, - autoPlay = false, - playNext = true, - shuffle = false, + downloadHandler.addTracksToMediaController( songs = songs, - playlistName = navArgs.playlistName + append = true, + playNext = true, + autoPlay = false, + shuffle = false, + playlistName = navArgs.playlistName, + fragment = this@TrackCollectionFragment ) } R.id.song_menu_play_last -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt index ff2f8a73..3a077b45 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt @@ -38,6 +38,7 @@ import org.moire.ultrasonic.domain.Playlist import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.OfflineException +import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.CacheCleaner @@ -147,45 +148,33 @@ class PlaylistsFragment : Fragment() { val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist when (menuItem.itemId) { R.id.playlist_menu_pin -> { - downloadHandler.value.downloadPlaylist( - this, + downloadHandler.value.justDownload( + DownloadAction.PIN, + fragment = this, id = playlist.id, name = playlist.name, - save = true, - append = true, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = false + isShare = false, + isDirectory = false ) } R.id.playlist_menu_unpin -> { - downloadHandler.value.downloadPlaylist( - this, + downloadHandler.value.justDownload( + DownloadAction.UNPIN, + fragment = this, id = playlist.id, name = playlist.name, - save = false, - append = false, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = true + isShare = false, + isDirectory = false ) } R.id.playlist_menu_download -> { - downloadHandler.value.downloadPlaylist( - this, + downloadHandler.value.justDownload( + DownloadAction.DOWNLOAD, + fragment = this, id = playlist.id, name = playlist.name, - save = false, - append = false, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = false + isShare = false, + isDirectory = false ) } R.id.playlist_menu_play_now -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt index eae31252..49c33749 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt @@ -28,7 +28,8 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import java.util.Locale -import org.koin.java.KoinJavaComponent +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException @@ -36,6 +37,7 @@ import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.fragment.FragmentTitle import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.OfflineException +import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.CancellationToken @@ -50,14 +52,12 @@ import org.moire.ultrasonic.view.ShareAdapter * * TODO: This file has been converted from Java, but not modernized yet. */ -class SharesFragment : Fragment() { +class SharesFragment : Fragment(), KoinComponent { private var refreshSharesListView: SwipeRefreshLayout? = null private var sharesListView: ListView? = null private var emptyTextView: View? = null private var shareAdapter: ShareAdapter? = null - private val downloadHandler = KoinJavaComponent.inject( - DownloadHandler::class.java - ) + private val downloadHandler = inject() private var cancellationToken: CancellationToken? = null override fun onCreate(savedInstanceState: Bundle?) { Util.applyTheme(this.context) @@ -72,7 +72,6 @@ class SharesFragment : Fragment() { return inflater.inflate(R.layout.select_share, container, false) } - @Suppress("NAME_SHADOWING") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { cancellationToken = CancellationToken() refreshSharesListView = view.findViewById(R.id.select_share_refresh) @@ -132,73 +131,55 @@ class SharesFragment : Fragment() { val share = sharesListView!!.getItemAtPosition(info.position) as Share when (menuItem.itemId) { R.id.share_menu_pin -> { - downloadHandler.value.downloadShare( - this, - share.id, - share.name, - save = true, - append = true, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = false + downloadHandler.value.justDownload( + DownloadAction.PIN, + fragment = this, + id = share.id, + name = share.name, + isShare = true, + isDirectory = false ) } R.id.share_menu_unpin -> { - downloadHandler.value.downloadShare( - this, - share.id, - share.name, - save = false, - append = false, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = true + downloadHandler.value.justDownload( + DownloadAction.UNPIN, + fragment = this, + id = share.id, + name = share.name, + isShare = true, + isDirectory = false ) } R.id.share_menu_download -> { - downloadHandler.value.downloadShare( - this, - share.id, - share.name, - save = false, - append = false, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = false + downloadHandler.value.justDownload( + DownloadAction.DOWNLOAD, + fragment = this, + id = share.id, + name = share.name, + isShare = true, + isDirectory = false ) } R.id.share_menu_play_now -> { - downloadHandler.value.downloadShare( + downloadHandler.value.fetchTracksAndAddToController( this, share.id, share.name, - save = false, append = false, - autoplay = true, + autoPlay = true, shuffle = false, - background = false, playNext = false, - unpin = false ) } R.id.share_menu_play_shuffled -> { - downloadHandler.value.downloadShare( + downloadHandler.value.fetchTracksAndAddToController( this, share.id, share.name, - save = false, append = false, - autoplay = true, + autoPlay = true, shuffle = true, - background = false, playNext = false, - unpin = false ) } R.id.share_menu_delete -> { 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 611a2c4c..2be0f26f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -42,7 +42,6 @@ import org.moire.ultrasonic.playback.PlaybackService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util -import org.moire.ultrasonic.util.setPin import org.moire.ultrasonic.util.toMediaItem import org.moire.ultrasonic.util.toTrack import timber.log.Timber @@ -314,7 +313,6 @@ class MediaPlayerController( addToPlaylist( state.songs, - cachePermanently = false, autoPlay = false, shuffle = false, insertionMode = insertionMode @@ -408,7 +406,6 @@ class MediaPlayerController( @Synchronized fun addToPlaylist( songs: List, - cachePermanently: Boolean, autoPlay: Boolean, shuffle: Boolean, insertionMode: InsertionMode @@ -423,7 +420,6 @@ class MediaPlayerController( val mediaItems: List = songs.map { val result = it.toMediaItem() - if (cachePermanently) result.setPin(true) result } 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 ce14c811..c327207b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -7,17 +7,11 @@ package org.moire.ultrasonic.subsonic -import android.os.Handler -import android.os.Looper import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController -import java.util.Collections import java.util.LinkedList -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline @@ -26,12 +20,8 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.DownloadService import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.CommunicationError -import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator -import org.moire.ultrasonic.util.InfoDialog import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.Util -import timber.log.Timber +import org.moire.ultrasonic.util.executeTaskWithToast /** * Retrieves a list of songs and adds them to the now playing list @@ -39,279 +29,145 @@ import timber.log.Timber @Suppress("LongParameterList") class DownloadHandler( val mediaPlayerController: MediaPlayerController, - val networkAndStorageChecker: NetworkAndStorageChecker + private val networkAndStorageChecker: NetworkAndStorageChecker ) : CoroutineScope by CoroutineScope(Dispatchers.IO) { private val maxSongs = 500 - /** - * Exception Handler for Coroutines - */ - val exceptionHandler = CoroutineExceptionHandler { _, exception -> - Handler(Looper.getMainLooper()).post { - Timber.w(exception) - } - } - - // TODO: Use coroutine here (with proper exception handler) - fun download( + fun justDownload( + action: DownloadAction, fragment: Fragment, - append: Boolean, - save: Boolean, - autoPlay: Boolean, - playNext: Boolean, - shuffle: Boolean, - songs: List, - playlistName: String?, + id: String? = null, + name: String? = "", + isShare: Boolean = false, + isDirectory: Boolean = true, + isArtist: Boolean = false, + tracks: List? = null ) { - val onValid = Runnable { - // TODO: The logic here is different than in the controller... - val insertionMode = when { - playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT - append -> MediaPlayerController.InsertionMode.APPEND - else -> MediaPlayerController.InsertionMode.CLEAR - } + var successString: String? = null - networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.addToPlaylist( - songs, - save, - autoPlay, - shuffle, - insertionMode - ) - - if (playlistName != null) { - mediaPlayerController.suggestedPlaylistName = playlistName - } - if (autoPlay) { - if (Settings.shouldTransitionOnPlayback) { - fragment.findNavController().popBackStack(R.id.playerFragment, true) - fragment.findNavController().navigate(R.id.playerFragment) - } - } else if (save) { - Util.toast( - fragment.context, - fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_pinned, - songs.size, - songs.size - ) - ) - } else if (playNext) { - Util.toast( - fragment.context, - fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_play_next, - songs.size, - songs.size - ) - ) - } else if (append) { - Util.toast( - fragment.context, - fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_added, - songs.size, - songs.size - ) - ) - } - } - onValid.run() - } - - fun downloadPlaylist( - fragment: Fragment, - id: String, - name: String?, - save: Boolean, - append: Boolean, - autoplay: Boolean, - shuffle: Boolean, - background: Boolean, - playNext: Boolean, - unpin: Boolean - ) { - downloadRecursively( - fragment, - id, - name, - isShare = false, - isDirectory = false, - save = save, - append = append, - autoPlay = autoplay, - shuffle = shuffle, - background = background, - playNext = playNext, - unpin = unpin, - isArtist = false - ) - } - - fun downloadShare( - fragment: Fragment, - id: String, - name: String?, - save: Boolean, - append: Boolean, - autoplay: Boolean, - shuffle: Boolean, - background: Boolean, - playNext: Boolean, - unpin: Boolean - ) { - downloadRecursively( - fragment, - id, - name, - isShare = true, - isDirectory = false, - save = save, - append = append, - autoPlay = autoplay, - shuffle = shuffle, - background = background, - playNext = playNext, - unpin = unpin, - isArtist = false - ) - } - - fun downloadRecursively( - fragment: Fragment, - id: String?, - save: Boolean, - append: Boolean, - autoPlay: Boolean, - shuffle: Boolean, - background: Boolean, - playNext: Boolean, - unpin: Boolean, - isArtist: Boolean - ) { - if (id.isNullOrEmpty()) return - downloadRecursively( - fragment, - id, - "", - isShare = false, - isDirectory = true, - save = save, - append = append, - autoPlay = autoPlay, - shuffle = shuffle, - background = background, - playNext = playNext, - unpin = unpin, - isArtist = isArtist - ) - } - - private fun downloadRecursively( - fragment: Fragment, - id: String, - name: String?, - isShare: Boolean, - isDirectory: Boolean, - save: Boolean, - append: Boolean, - autoPlay: Boolean, - shuffle: Boolean, - background: Boolean, - playNext: Boolean, - unpin: Boolean, - isArtist: Boolean - ) { // Launch the Job - val job = launch(exceptionHandler) { + executeTaskWithToast(fragment, { + val tracksToDownload: List = tracks + ?: getTracksFromServer(isArtist, id!!, isDirectory, name, isShare) + + withContext(Dispatchers.Main) { + // If we are just downloading tracks we don't need to add them to the controller + when (action) { + DownloadAction.DOWNLOAD -> DownloadService.download(tracksToDownload, false) + DownloadAction.PIN -> DownloadService.download(tracksToDownload, true) + DownloadAction.UNPIN -> DownloadService.unpin(tracksToDownload) + DownloadAction.DELETE -> DownloadService.delete(tracksToDownload) + } + successString = when (action) { + DownloadAction.DOWNLOAD -> fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_downloaded, + tracksToDownload.size, + tracksToDownload.size + ) + DownloadAction.UNPIN -> { + fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_unpinned, + tracksToDownload.size, + tracksToDownload.size + ) + } + DownloadAction.PIN -> { + fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_pinned, + tracksToDownload.size, + tracksToDownload.size + ) + } + DownloadAction.DELETE -> { + fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_deleted, + tracksToDownload.size, + tracksToDownload.size + ) + } + } + } + }) { successString } + } + + fun fetchTracksAndAddToController( + fragment: Fragment, + id: String, + name: String? = "", + isShare: Boolean = false, + isDirectory: Boolean = true, + append: Boolean, + autoPlay: Boolean, + shuffle: Boolean, + playNext: Boolean, + isArtist: Boolean = false + ) { + var successString: String? = null + // Launch the Job + executeTaskWithToast(fragment, { val songs: MutableList = getTracksFromServer(isArtist, id, isDirectory, name, isShare) withContext(Dispatchers.Main) { addTracksToMediaController( - songs, - background, - unpin, - append, - playNext, - save, - autoPlay, - shuffle, - fragment + songs = songs, + append = append, + playNext = playNext, + autoPlay = autoPlay, + shuffle = shuffle, + playlistName = null, + fragment = fragment ) + // Play Now doesn't get a Toast :) + if (playNext) { + successString = fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_play_next, + songs.size, + songs.size + ) + } else if (append) { + successString = fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_added, + songs.size, + songs.size + ) + } } - } - - // Create the dialog - val builder = InfoDialog.Builder(fragment.requireContext()) - builder.setTitle(R.string.background_task_wait) - builder.setMessage(R.string.background_task_loading) - builder.setOnCancelListener { job.cancel() } - builder.setPositiveButton(R.string.common_cancel) { _, i -> job.cancel() } - val dialog = builder.create() - dialog.show() - - job.invokeOnCompletion { - dialog.dismiss() - if (it != null && it !is CancellationException) { - Util.toast( - fragment.requireContext(), - CommunicationError.getErrorMessage(it, fragment.requireContext()) - ) - } - } + }) { successString } } - private fun addTracksToMediaController( - songs: MutableList, - background: Boolean, - unpin: Boolean, + fun addTracksToMediaController( + songs: List, append: Boolean, playNext: Boolean, - save: Boolean, autoPlay: Boolean, shuffle: Boolean, + playlistName: String? = null, fragment: Fragment ) { if (songs.isEmpty()) return - if (Settings.shouldSortByDisc) { - Collections.sort(songs, EntryByDiscAndTrackComparator()) - } + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - if (!background) { - if (unpin) { - DownloadService.unpin(songs) - } else { - val insertionMode = when { - append -> MediaPlayerController.InsertionMode.APPEND - playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT - else -> MediaPlayerController.InsertionMode.CLEAR - } - mediaPlayerController.addToPlaylist( - songs, - save, - autoPlay, - shuffle, - insertionMode - ) - if ( - !append && - Settings.shouldTransitionOnPlayback - ) { - fragment.findNavController().popBackStack( - R.id.playerFragment, - true - ) - fragment.findNavController().navigate(R.id.playerFragment) - } - } - } else { - if (unpin) { - DownloadService.unpin(songs) - } else { - DownloadService.download(songs, save) - } + + val insertionMode = when { + append -> MediaPlayerController.InsertionMode.APPEND + playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT + else -> MediaPlayerController.InsertionMode.CLEAR + } + + if (playlistName != null) { + mediaPlayerController.suggestedPlaylistName = playlistName + } + + mediaPlayerController.addToPlaylist( + songs, + autoPlay, + shuffle, + insertionMode + ) + if (Settings.shouldTransitionOnPlayback && (!append || autoPlay)) { + fragment.findNavController().popBackStack(R.id.playerFragment, true) + fragment.findNavController().navigate(R.id.playerFragment) } } @@ -396,3 +252,7 @@ class DownloadHandler( } } } + +enum class DownloadAction { + DOWNLOAD, PIN, UNPIN, DELETE +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt new file mode 100644 index 00000000..c0a95bd9 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt @@ -0,0 +1,82 @@ +/* + * CoroutinePatterns.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.util + +import android.os.Handler +import android.os.Looper +import androidx.fragment.app.Fragment +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.moire.ultrasonic.R +import timber.log.Timber + +object CoroutinePatterns { + val loggingExceptionHandler by lazy { + CoroutineExceptionHandler { _, exception -> + Handler(Looper.getMainLooper()).post { + Timber.w(exception) + } + } + } +} + +fun CoroutineScope.executeTaskWithToast( + fragment: Fragment, + task: suspend CoroutineScope.() -> Unit, + successString: () -> String? +): Job { + // Launch the Job + val job = launch(CoroutinePatterns.loggingExceptionHandler, block = task) + + // Setup a handler when the job is done + job.invokeOnCompletion { + val toastString = if (it != null && it !is CancellationException) { + CommunicationError.getErrorMessage(it, fragment.context) + } else { + successString() + } + + // Return early if nothing to post + if (toastString == null) return@invokeOnCompletion + + launch(Dispatchers.Main) { + Util.toast(fragment.context, toastString) + } + } + + return job +} + +fun CoroutineScope.executeTaskWithModalDialog( + fragment: Fragment, + task: suspend CoroutineScope.() -> Unit, + successString: () -> String +) { + // Create the job + val job = executeTaskWithToast(fragment, task, successString) + + // Create the dialog + val builder = InfoDialog.Builder(fragment.requireContext()) + builder.setTitle(R.string.background_task_wait) + builder.setMessage(R.string.background_task_loading) + builder.setOnCancelListener { job.cancel() } + builder.setPositiveButton(R.string.common_cancel) { _, _ -> job.cancel() } + val dialog = builder.create() + dialog.show() + + // Add additional handler to close the dialog + job.invokeOnCompletion { + launch(Dispatchers.Main) { + dialog.dismiss() + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index b9f2368f..40739ee7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -148,8 +148,8 @@ object Util { if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG } toast!!.show() - } catch (_: Exception) { - // Ignore + } catch (all: Exception) { + Timber.w(all) } }