Merge branch 'RefactorContextActions' into 'develop'

Use Coroutines for triggering the download or playback of music through the context menus

See merge request ultrasonic/ultrasonic!998
This commit is contained in:
birdbird 2023-05-09 09:34:15 +00:00
commit fd8afe0231
10 changed files with 329 additions and 475 deletions

View File

@ -1,6 +1,6 @@
[versions] [versions]
# You need to run ./gradlew wrapper after updating the version # You need to run ./gradlew wrapper after updating the version
gradle = "7.6" gradle = "8.1.1"
navigation = "2.5.3" navigation = "2.5.3"
gradlePlugin = "8.0.1" gradlePlugin = "8.0.1"

View File

@ -21,6 +21,7 @@ import org.moire.ultrasonic.domain.GenericEntry
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.subsonic.DownloadAction
import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
@ -129,81 +130,54 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
): Boolean { ): Boolean {
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.menu_play_now -> R.id.menu_play_now ->
downloadHandler.downloadRecursively( downloadHandler.fetchTracksAndAddToController(
fragment, fragment,
item.id, item.id,
save = false,
append = false, append = false,
autoPlay = true, autoPlay = true,
shuffle = false, shuffle = false,
background = false,
playNext = false, playNext = false,
unpin = false,
isArtist = isArtist isArtist = isArtist
) )
R.id.menu_play_next -> R.id.menu_play_next ->
downloadHandler.downloadRecursively( downloadHandler.fetchTracksAndAddToController(
fragment, fragment,
item.id, item.id,
save = false,
append = false, append = false,
autoPlay = true, autoPlay = true,
shuffle = true, shuffle = true,
background = false,
playNext = true, playNext = true,
unpin = false,
isArtist = isArtist isArtist = isArtist
) )
R.id.menu_play_last -> R.id.menu_play_last ->
downloadHandler.downloadRecursively( downloadHandler.fetchTracksAndAddToController(
fragment, fragment,
item.id, item.id,
save = false,
append = true, append = true,
autoPlay = false, autoPlay = false,
shuffle = false, shuffle = false,
background = false,
playNext = false, playNext = false,
unpin = false,
isArtist = isArtist isArtist = isArtist
) )
R.id.menu_pin -> R.id.menu_pin ->
downloadHandler.downloadRecursively( downloadHandler.justDownload(
action = DownloadAction.PIN,
fragment, fragment,
item.id, item.id,
save = true,
append = true,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = isArtist isArtist = isArtist
) )
R.id.menu_unpin -> R.id.menu_unpin ->
downloadHandler.downloadRecursively( downloadHandler.justDownload(
action = DownloadAction.UNPIN,
fragment, fragment,
item.id, item.id,
save = false,
append = false,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = true,
isArtist = isArtist isArtist = isArtist
) )
R.id.menu_download -> R.id.menu_download ->
downloadHandler.downloadRecursively( downloadHandler.justDownload(
action = DownloadAction.DOWNLOAD,
fragment, fragment,
item.id, item.id,
save = false,
append = false,
autoPlay = false,
shuffle = false,
background = true,
playNext = false,
unpin = false,
isArtist = isArtist isArtist = isArtist
) )
else -> return false else -> return false

View File

@ -309,7 +309,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
} }
mediaPlayerController.addToPlaylist( mediaPlayerController.addToPlaylist(
listOf(song), listOf(song),
cachePermanently = false,
autoPlay = false, autoPlay = false,
shuffle = false, shuffle = false,
insertionMode = MediaPlayerController.InsertionMode.APPEND insertionMode = MediaPlayerController.InsertionMode.APPEND
@ -367,40 +366,37 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.song_menu_play_now -> { R.id.song_menu_play_now -> {
songs.add(item) songs.add(item)
downloadHandler.download( downloadHandler.addTracksToMediaController(
fragment = this,
append = false,
save = false,
autoPlay = true,
playNext = false,
shuffle = false,
songs = songs, songs = songs,
append = false,
playNext = false,
autoPlay = true,
shuffle = false,
fragment = this,
playlistName = null playlistName = null
) )
} }
R.id.song_menu_play_next -> { R.id.song_menu_play_next -> {
songs.add(item) songs.add(item)
downloadHandler.download( downloadHandler.addTracksToMediaController(
fragment = this,
append = true,
save = false,
autoPlay = false,
playNext = true,
shuffle = false,
songs = songs, songs = songs,
append = true,
playNext = true,
autoPlay = false,
shuffle = false,
fragment = this,
playlistName = null playlistName = null
) )
} }
R.id.song_menu_play_last -> { R.id.song_menu_play_last -> {
songs.add(item) songs.add(item)
downloadHandler.download( downloadHandler.addTracksToMediaController(
fragment = this,
append = true,
save = false,
autoPlay = false,
playNext = false,
shuffle = false,
songs = songs, songs = songs,
append = true,
playNext = false,
autoPlay = false,
shuffle = false,
fragment = this,
playlistName = null playlistName = null
) )
} }

View File

@ -40,11 +40,10 @@ import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.model.TrackCollectionModel import org.moire.ultrasonic.model.TrackCollectionModel
import org.moire.ultrasonic.service.DownloadService
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign 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.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer import org.moire.ultrasonic.subsonic.VideoPlayer
import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CancellationToken
@ -84,7 +83,6 @@ open class TrackCollectionFragment(
private var shareButton: MenuItem? = null private var shareButton: MenuItem? = null
internal val mediaPlayerController: MediaPlayerController by inject() internal val mediaPlayerController: MediaPlayerController by inject()
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
private val shareHandler: ShareHandler by inject() private val shareHandler: ShareHandler by inject()
internal var cancellationToken: CancellationToken? = null internal var cancellationToken: CancellationToken? = null
@ -211,11 +209,14 @@ open class TrackCollectionFragment(
} }
playNextButton?.setOnClickListener { playNextButton?.setOnClickListener {
downloadHandler.download( downloadHandler.addTracksToMediaController(
this@TrackCollectionFragment, append = true,
save = false, autoPlay = false, playNext = true, shuffle = false,
songs = getSelectedSongs(), 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<Track> = getSelectedSongs() selectedSongs: List<Track> = getSelectedSongs()
) { ) {
if (selectedSongs.isNotEmpty()) { if (selectedSongs.isNotEmpty()) {
downloadHandler.download( downloadHandler.addTracksToMediaController(
this, append, false, !append, playNext = false, songs = selectedSongs,
shuffle = false, songs = selectedSongs, null append = append,
playNext = false,
autoPlay = !append,
shuffle = false,
playlistName = null,
fragment = this
) )
} else { } else {
playAll(false, append) playAll(false, append)
@ -337,31 +343,29 @@ open class TrackCollectionFragment(
} }
val isArtist = navArgs.isArtist val isArtist = navArgs.isArtist
val id = navArgs.id
// Need a valid id to download stuff
val id = navArgs.id ?: return
if (hasSubFolders) { if (hasSubFolders) {
downloadHandler.downloadRecursively( downloadHandler.fetchTracksAndAddToController(
fragment = this, fragment = this,
id = id, id = id,
save = false,
append = append, append = append,
autoPlay = !append, autoPlay = !append,
shuffle = shuffle, shuffle = shuffle,
background = false,
playNext = false, playNext = false,
unpin = false,
isArtist = isArtist isArtist = isArtist
) )
} else { } else {
downloadHandler.download( downloadHandler.addTracksToMediaController(
fragment = this,
append = append,
save = false,
autoPlay = !append,
playNext = false,
shuffle = shuffle,
songs = getAllSongs(), 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) { private fun downloadBackground(save: Boolean, tracks: List<Track> = getSelectedSongs()) {
var songs = getSelectedSongs() var songs = tracks
if (songs.isEmpty()) { if (songs.isEmpty()) {
songs = getAllSongs() songs = getAllSongs()
} }
downloadBackground(save, songs) val action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD
} downloadHandler.justDownload(
action = action,
private fun downloadBackground( fragment = this,
save: Boolean, tracks = songs
songs: List<Track?> )
) {
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()
} }
internal fun delete(songs: List<Track> = getSelectedSongs()) { internal fun delete(songs: List<Track> = getSelectedSongs()) {
Util.toast( downloadHandler.justDownload(
context, action = DownloadAction.DELETE,
resources.getQuantityString( fragment = this,
R.plurals.select_album_n_songs_deleted, songs.size, songs.size tracks = songs
)
) )
DownloadService.delete(songs)
} }
internal fun unpin(songs: List<Track> = getSelectedSongs()) { internal fun unpin(songs: List<Track> = getSelectedSongs()) {
Util.toast( downloadHandler.justDownload(
context, action = DownloadAction.UNPIN,
resources.getQuantityString( fragment = this,
R.plurals.select_album_n_songs_unpinned, songs.size, songs.size tracks = songs
)
) )
DownloadService.unpin(songs)
} }
override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = { override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = {
@ -637,15 +614,14 @@ open class TrackCollectionFragment(
playNow(false, songs) playNow(false, songs)
} }
R.id.song_menu_play_next -> { R.id.song_menu_play_next -> {
downloadHandler.download( downloadHandler.addTracksToMediaController(
fragment = this@TrackCollectionFragment,
append = true,
save = false,
autoPlay = false,
playNext = true,
shuffle = false,
songs = songs, 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 -> { R.id.song_menu_play_last -> {

View File

@ -38,6 +38,7 @@ import org.moire.ultrasonic.domain.Playlist
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.service.OfflineException import org.moire.ultrasonic.service.OfflineException
import org.moire.ultrasonic.subsonic.DownloadAction
import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.BackgroundTask
import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.CacheCleaner
@ -147,45 +148,33 @@ class PlaylistsFragment : Fragment() {
val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.playlist_menu_pin -> { R.id.playlist_menu_pin -> {
downloadHandler.value.downloadPlaylist( downloadHandler.value.justDownload(
this, DownloadAction.PIN,
fragment = this,
id = playlist.id, id = playlist.id,
name = playlist.name, name = playlist.name,
save = true, isShare = false,
append = true, isDirectory = false
autoplay = false,
shuffle = false,
background = true,
playNext = false,
unpin = false
) )
} }
R.id.playlist_menu_unpin -> { R.id.playlist_menu_unpin -> {
downloadHandler.value.downloadPlaylist( downloadHandler.value.justDownload(
this, DownloadAction.UNPIN,
fragment = this,
id = playlist.id, id = playlist.id,
name = playlist.name, name = playlist.name,
save = false, isShare = false,
append = false, isDirectory = false
autoplay = false,
shuffle = false,
background = true,
playNext = false,
unpin = true
) )
} }
R.id.playlist_menu_download -> { R.id.playlist_menu_download -> {
downloadHandler.value.downloadPlaylist( downloadHandler.value.justDownload(
this, DownloadAction.DOWNLOAD,
fragment = this,
id = playlist.id, id = playlist.id,
name = playlist.name, name = playlist.name,
save = false, isShare = false,
append = false, isDirectory = false
autoplay = false,
shuffle = false,
background = true,
playNext = false,
unpin = false
) )
} }
R.id.playlist_menu_play_now -> { R.id.playlist_menu_play_now -> {

View File

@ -28,7 +28,8 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.util.Locale 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.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException 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.fragment.FragmentTitle
import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.service.OfflineException import org.moire.ultrasonic.service.OfflineException
import org.moire.ultrasonic.subsonic.DownloadAction
import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.BackgroundTask
import org.moire.ultrasonic.util.CancellationToken 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. * 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 refreshSharesListView: SwipeRefreshLayout? = null
private var sharesListView: ListView? = null private var sharesListView: ListView? = null
private var emptyTextView: View? = null private var emptyTextView: View? = null
private var shareAdapter: ShareAdapter? = null private var shareAdapter: ShareAdapter? = null
private val downloadHandler = KoinJavaComponent.inject<DownloadHandler>( private val downloadHandler = inject<DownloadHandler>()
DownloadHandler::class.java
)
private var cancellationToken: CancellationToken? = null private var cancellationToken: CancellationToken? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context) Util.applyTheme(this.context)
@ -72,7 +72,6 @@ class SharesFragment : Fragment() {
return inflater.inflate(R.layout.select_share, container, false) return inflater.inflate(R.layout.select_share, container, false)
} }
@Suppress("NAME_SHADOWING")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
cancellationToken = CancellationToken() cancellationToken = CancellationToken()
refreshSharesListView = view.findViewById(R.id.select_share_refresh) refreshSharesListView = view.findViewById(R.id.select_share_refresh)
@ -132,73 +131,55 @@ class SharesFragment : Fragment() {
val share = sharesListView!!.getItemAtPosition(info.position) as Share val share = sharesListView!!.getItemAtPosition(info.position) as Share
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.share_menu_pin -> { R.id.share_menu_pin -> {
downloadHandler.value.downloadShare( downloadHandler.value.justDownload(
this, DownloadAction.PIN,
share.id, fragment = this,
share.name, id = share.id,
save = true, name = share.name,
append = true, isShare = true,
autoplay = false, isDirectory = false
shuffle = false,
background = true,
playNext = false,
unpin = false
) )
} }
R.id.share_menu_unpin -> { R.id.share_menu_unpin -> {
downloadHandler.value.downloadShare( downloadHandler.value.justDownload(
this, DownloadAction.UNPIN,
share.id, fragment = this,
share.name, id = share.id,
save = false, name = share.name,
append = false, isShare = true,
autoplay = false, isDirectory = false
shuffle = false,
background = true,
playNext = false,
unpin = true
) )
} }
R.id.share_menu_download -> { R.id.share_menu_download -> {
downloadHandler.value.downloadShare( downloadHandler.value.justDownload(
this, DownloadAction.DOWNLOAD,
share.id, fragment = this,
share.name, id = share.id,
save = false, name = share.name,
append = false, isShare = true,
autoplay = false, isDirectory = false
shuffle = false,
background = true,
playNext = false,
unpin = false
) )
} }
R.id.share_menu_play_now -> { R.id.share_menu_play_now -> {
downloadHandler.value.downloadShare( downloadHandler.value.fetchTracksAndAddToController(
this, this,
share.id, share.id,
share.name, share.name,
save = false,
append = false, append = false,
autoplay = true, autoPlay = true,
shuffle = false, shuffle = false,
background = false,
playNext = false, playNext = false,
unpin = false
) )
} }
R.id.share_menu_play_shuffled -> { R.id.share_menu_play_shuffled -> {
downloadHandler.value.downloadShare( downloadHandler.value.fetchTracksAndAddToController(
this, this,
share.id, share.id,
share.name, share.name,
save = false,
append = false, append = false,
autoplay = true, autoPlay = true,
shuffle = true, shuffle = true,
background = false,
playNext = false, playNext = false,
unpin = false
) )
} }
R.id.share_menu_delete -> { R.id.share_menu_delete -> {

View File

@ -42,7 +42,6 @@ import org.moire.ultrasonic.playback.PlaybackService
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.setPin
import org.moire.ultrasonic.util.toMediaItem import org.moire.ultrasonic.util.toMediaItem
import org.moire.ultrasonic.util.toTrack import org.moire.ultrasonic.util.toTrack
import timber.log.Timber import timber.log.Timber
@ -314,7 +313,6 @@ class MediaPlayerController(
addToPlaylist( addToPlaylist(
state.songs, state.songs,
cachePermanently = false,
autoPlay = false, autoPlay = false,
shuffle = false, shuffle = false,
insertionMode = insertionMode insertionMode = insertionMode
@ -408,7 +406,6 @@ class MediaPlayerController(
@Synchronized @Synchronized
fun addToPlaylist( fun addToPlaylist(
songs: List<Track>, songs: List<Track>,
cachePermanently: Boolean,
autoPlay: Boolean, autoPlay: Boolean,
shuffle: Boolean, shuffle: Boolean,
insertionMode: InsertionMode insertionMode: InsertionMode
@ -423,7 +420,6 @@ class MediaPlayerController(
val mediaItems: List<MediaItem> = songs.map { val mediaItems: List<MediaItem> = songs.map {
val result = it.toMediaItem() val result = it.toMediaItem()
if (cachePermanently) result.setPin(true)
result result
} }

View File

@ -7,17 +7,11 @@
package org.moire.ultrasonic.subsonic package org.moire.ultrasonic.subsonic
import android.os.Handler
import android.os.Looper
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import java.util.Collections
import java.util.LinkedList import java.util.LinkedList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline 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.DownloadService
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService 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.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.executeTaskWithToast
import timber.log.Timber
/** /**
* Retrieves a list of songs and adds them to the now playing list * Retrieves a list of songs and adds them to the now playing list
@ -39,279 +29,145 @@ import timber.log.Timber
@Suppress("LongParameterList") @Suppress("LongParameterList")
class DownloadHandler( class DownloadHandler(
val mediaPlayerController: MediaPlayerController, val mediaPlayerController: MediaPlayerController,
val networkAndStorageChecker: NetworkAndStorageChecker private val networkAndStorageChecker: NetworkAndStorageChecker
) : CoroutineScope by CoroutineScope(Dispatchers.IO) { ) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
private val maxSongs = 500 private val maxSongs = 500
/** fun justDownload(
* Exception Handler for Coroutines action: DownloadAction,
*/
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
Handler(Looper.getMainLooper()).post {
Timber.w(exception)
}
}
// TODO: Use coroutine here (with proper exception handler)
fun download(
fragment: Fragment, fragment: Fragment,
append: Boolean, id: String? = null,
save: Boolean, name: String? = "",
autoPlay: Boolean, isShare: Boolean = false,
playNext: Boolean, isDirectory: Boolean = true,
shuffle: Boolean, isArtist: Boolean = false,
songs: List<Track>, tracks: List<Track>? = null
playlistName: String?,
) { ) {
val onValid = Runnable { var successString: String? = null
// 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
}
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 // Launch the Job
val job = launch(exceptionHandler) { executeTaskWithToast(fragment, {
val tracksToDownload: List<Track> = 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<Track> = val songs: MutableList<Track> =
getTracksFromServer(isArtist, id, isDirectory, name, isShare) getTracksFromServer(isArtist, id, isDirectory, name, isShare)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
addTracksToMediaController( addTracksToMediaController(
songs, songs = songs,
background, append = append,
unpin, playNext = playNext,
append, autoPlay = autoPlay,
playNext, shuffle = shuffle,
save, playlistName = null,
autoPlay, fragment = fragment
shuffle,
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
)
}
} }
} }) { 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) { _, 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())
)
}
}
} }
private fun addTracksToMediaController( fun addTracksToMediaController(
songs: MutableList<Track>, songs: List<Track>,
background: Boolean,
unpin: Boolean,
append: Boolean, append: Boolean,
playNext: Boolean, playNext: Boolean,
save: Boolean,
autoPlay: Boolean, autoPlay: Boolean,
shuffle: Boolean, shuffle: Boolean,
playlistName: String? = null,
fragment: Fragment fragment: Fragment
) { ) {
if (songs.isEmpty()) return if (songs.isEmpty()) return
if (Settings.shouldSortByDisc) {
Collections.sort(songs, EntryByDiscAndTrackComparator())
}
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
if (!background) {
if (unpin) { val insertionMode = when {
DownloadService.unpin(songs) append -> MediaPlayerController.InsertionMode.APPEND
} else { playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT
val insertionMode = when { else -> MediaPlayerController.InsertionMode.CLEAR
append -> MediaPlayerController.InsertionMode.APPEND }
playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT
else -> MediaPlayerController.InsertionMode.CLEAR if (playlistName != null) {
} mediaPlayerController.suggestedPlaylistName = playlistName
mediaPlayerController.addToPlaylist( }
songs,
save, mediaPlayerController.addToPlaylist(
autoPlay, songs,
shuffle, autoPlay,
insertionMode shuffle,
) insertionMode
if ( )
!append && if (Settings.shouldTransitionOnPlayback && (!append || autoPlay)) {
Settings.shouldTransitionOnPlayback fragment.findNavController().popBackStack(R.id.playerFragment, true)
) { fragment.findNavController().navigate(R.id.playerFragment)
fragment.findNavController().popBackStack(
R.id.playerFragment,
true
)
fragment.findNavController().navigate(R.id.playerFragment)
}
}
} else {
if (unpin) {
DownloadService.unpin(songs)
} else {
DownloadService.download(songs, save)
}
} }
} }
@ -396,3 +252,7 @@ class DownloadHandler(
} }
} }
} }
enum class DownloadAction {
DOWNLOAD, PIN, UNPIN, DELETE
}

View File

@ -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()
}
}
}

View File

@ -148,8 +148,8 @@ object Util {
if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG
} }
toast!!.show() toast!!.show()
} catch (_: Exception) { } catch (all: Exception) {
// Ignore Timber.w(all)
} }
} }