Compare commits

..

No commits in common. "90ffa32246427a31e9ba4f3081fcdff3f14953b1" and "842cb36ecb9eaad2b45686ceb87b7eb844bf4ae5" have entirely different histories.

13 changed files with 473 additions and 335 deletions

View File

@ -1,8 +0,0 @@
Bug fixes
- Fix more exceptions
Changes since 4.2.0
- #827: Make app full compliant Android Auto to publish in Play Store.
- #878: "Play shuffled" option for playlists always begins with the first track.
- #891: Dump config to log file when logging is enabled.
- #854: Remove Videos menu option for servers which don't support it.

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 = "8.1.1" gradle = "7.6"
navigation = "2.5.3" navigation = "2.5.3"
gradlePlugin = "8.0.1" gradlePlugin = "8.0.1"

View File

@ -9,8 +9,8 @@ android {
defaultConfig { defaultConfig {
applicationId "org.moire.ultrasonic" applicationId "org.moire.ultrasonic"
versionCode 117 versionCode 116
versionName "4.3.4" versionName "4.3.3"
minSdkVersion versions.minSdk minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk

View File

@ -21,7 +21,6 @@ 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
@ -130,54 +129,81 @@ 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.fetchTracksAndAddToController( downloadHandler.downloadRecursively(
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.fetchTracksAndAddToController( downloadHandler.downloadRecursively(
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.fetchTracksAndAddToController( downloadHandler.downloadRecursively(
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.justDownload( downloadHandler.downloadRecursively(
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.justDownload( downloadHandler.downloadRecursively(
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.justDownload( downloadHandler.downloadRecursively(
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,6 +309,7 @@ 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
@ -366,37 +367,40 @@ 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.addTracksToMediaController( downloadHandler.download(
songs = songs,
append = false,
playNext = false,
autoPlay = true,
shuffle = false,
fragment = this, fragment = this,
append = false,
save = false,
autoPlay = true,
playNext = false,
shuffle = false,
songs = songs,
playlistName = null playlistName = null
) )
} }
R.id.song_menu_play_next -> { R.id.song_menu_play_next -> {
songs.add(item) songs.add(item)
downloadHandler.addTracksToMediaController( downloadHandler.download(
songs = songs,
append = true,
playNext = true,
autoPlay = false,
shuffle = false,
fragment = this, fragment = this,
append = true,
save = false,
autoPlay = false,
playNext = true,
shuffle = false,
songs = songs,
playlistName = null playlistName = null
) )
} }
R.id.song_menu_play_last -> { R.id.song_menu_play_last -> {
songs.add(item) songs.add(item)
downloadHandler.addTracksToMediaController( downloadHandler.download(
songs = songs,
append = true,
playNext = false,
autoPlay = false,
shuffle = false,
fragment = this, fragment = this,
append = true,
save = false,
autoPlay = false,
playNext = false,
shuffle = false,
songs = songs,
playlistName = null playlistName = null
) )
} }

View File

@ -40,10 +40,11 @@ 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.DownloadAction import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
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
@ -83,6 +84,7 @@ 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
@ -209,14 +211,11 @@ open class TrackCollectionFragment(
} }
playNextButton?.setOnClickListener { playNextButton?.setOnClickListener {
downloadHandler.addTracksToMediaController( downloadHandler.download(
this@TrackCollectionFragment, append = true,
save = false, autoPlay = false, playNext = true, shuffle = false,
songs = getSelectedSongs(), songs = getSelectedSongs(),
append = true, playlistName = navArgs.playlistName
playNext = true,
autoPlay = false,
shuffle = false,
playlistName = navArgs.playlistName,
this@TrackCollectionFragment
) )
} }
@ -305,14 +304,9 @@ open class TrackCollectionFragment(
selectedSongs: List<Track> = getSelectedSongs() selectedSongs: List<Track> = getSelectedSongs()
) { ) {
if (selectedSongs.isNotEmpty()) { if (selectedSongs.isNotEmpty()) {
downloadHandler.addTracksToMediaController( downloadHandler.download(
songs = selectedSongs, this, append, false, !append, playNext = false,
append = append, shuffle = false, songs = selectedSongs, null
playNext = false,
autoPlay = !append,
shuffle = false,
playlistName = null,
fragment = this
) )
} else { } else {
playAll(false, append) playAll(false, append)
@ -343,29 +337,31 @@ 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.fetchTracksAndAddToController( downloadHandler.downloadRecursively(
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.addTracksToMediaController( downloadHandler.download(
songs = getAllSongs(), fragment = this,
append = append, append = append,
playNext = false, save = false,
autoPlay = !append, autoPlay = !append,
playNext = false,
shuffle = shuffle, shuffle = shuffle,
playlistName = navArgs.playlistName, songs = getAllSongs(),
fragment = this playlistName = navArgs.playlistName
) )
} }
} }
@ -420,35 +416,62 @@ open class TrackCollectionFragment(
} }
} }
private fun downloadBackground(save: Boolean, tracks: List<Track> = getSelectedSongs()) { private fun downloadBackground(save: Boolean) {
var songs = tracks var songs = getSelectedSongs()
if (songs.isEmpty()) { if (songs.isEmpty()) {
songs = getAllSongs() songs = getAllSongs()
} }
val action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD downloadBackground(save, songs)
downloadHandler.justDownload( }
action = action,
fragment = this, private fun downloadBackground(
tracks = songs save: Boolean,
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()) {
downloadHandler.justDownload( Util.toast(
action = DownloadAction.DELETE, context,
fragment = this, resources.getQuantityString(
tracks = songs R.plurals.select_album_n_songs_deleted, songs.size, songs.size
) )
)
DownloadService.delete(songs)
} }
internal fun unpin(songs: List<Track> = getSelectedSongs()) { internal fun unpin(songs: List<Track> = getSelectedSongs()) {
downloadHandler.justDownload( Util.toast(
action = DownloadAction.UNPIN, context,
fragment = this, resources.getQuantityString(
tracks = songs R.plurals.select_album_n_songs_unpinned, songs.size, songs.size
) )
)
DownloadService.unpin(songs)
} }
override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = { override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = {
@ -574,14 +597,14 @@ open class TrackCollectionFragment(
} else if (getVideos) { } else if (getVideos) {
setTitle(R.string.main_videos) setTitle(R.string.main_videos)
listModel.getVideos(refresh2) listModel.getVideos(refresh2)
} else if (id == null || getRandomTracks) { } else if (getRandomTracks) {
// There seems to be a bug in ViewPager when resuming the Actitivy that subfragments
// arguments are empty. If we have no id, just show some random tracks
setTitle(R.string.main_songs_random) setTitle(R.string.main_songs_random)
listModel.getRandom(size, append) listModel.getRandom(size, append)
} else { } else {
setTitle(name) setTitle(name)
requireNotNull(id) {
"ID must be set. NavArgs: ${navArgs.toBundle()}"
}
if (ActiveServerProvider.isID3Enabled()) { if (ActiveServerProvider.isID3Enabled()) {
if (isAlbum) { if (isAlbum) {
listModel.getAlbum(refresh2, id, name) listModel.getAlbum(refresh2, id, name)
@ -614,14 +637,15 @@ open class TrackCollectionFragment(
playNow(false, songs) playNow(false, songs)
} }
R.id.song_menu_play_next -> { R.id.song_menu_play_next -> {
downloadHandler.addTracksToMediaController( downloadHandler.download(
songs = songs, fragment = this@TrackCollectionFragment,
append = true, append = true,
playNext = true, save = false,
autoPlay = false, autoPlay = false,
playNext = true,
shuffle = false, shuffle = false,
playlistName = navArgs.playlistName, songs = songs,
fragment = this@TrackCollectionFragment playlistName = navArgs.playlistName
) )
} }
R.id.song_menu_play_last -> { R.id.song_menu_play_last -> {

View File

@ -38,7 +38,6 @@ 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
@ -148,33 +147,45 @@ 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.justDownload( downloadHandler.value.downloadPlaylist(
DownloadAction.PIN, this,
fragment = this,
id = playlist.id, id = playlist.id,
name = playlist.name, name = playlist.name,
isShare = false, save = true,
isDirectory = false append = true,
autoplay = false,
shuffle = false,
background = true,
playNext = false,
unpin = false
) )
} }
R.id.playlist_menu_unpin -> { R.id.playlist_menu_unpin -> {
downloadHandler.value.justDownload( downloadHandler.value.downloadPlaylist(
DownloadAction.UNPIN, this,
fragment = this,
id = playlist.id, id = playlist.id,
name = playlist.name, name = playlist.name,
isShare = false, save = false,
isDirectory = false append = false,
autoplay = false,
shuffle = false,
background = true,
playNext = false,
unpin = true
) )
} }
R.id.playlist_menu_download -> { R.id.playlist_menu_download -> {
downloadHandler.value.justDownload( downloadHandler.value.downloadPlaylist(
DownloadAction.DOWNLOAD, this,
fragment = this,
id = playlist.id, id = playlist.id,
name = playlist.name, name = playlist.name,
isShare = false, save = false,
isDirectory = false append = 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,8 +28,7 @@ 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.core.component.KoinComponent import org.koin.java.KoinJavaComponent
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
@ -37,7 +36,6 @@ 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
@ -52,12 +50,14 @@ 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(), KoinComponent { class SharesFragment : Fragment() {
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 = inject<DownloadHandler>() private val downloadHandler = KoinJavaComponent.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,6 +72,7 @@ class SharesFragment : Fragment(), KoinComponent {
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)
@ -131,55 +132,73 @@ class SharesFragment : Fragment(), KoinComponent {
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.justDownload( downloadHandler.value.downloadShare(
DownloadAction.PIN, this,
fragment = this, share.id,
id = share.id, share.name,
name = share.name, save = true,
isShare = true, append = true,
isDirectory = false autoplay = false,
shuffle = false,
background = true,
playNext = false,
unpin = false
) )
} }
R.id.share_menu_unpin -> { R.id.share_menu_unpin -> {
downloadHandler.value.justDownload( downloadHandler.value.downloadShare(
DownloadAction.UNPIN, this,
fragment = this, share.id,
id = share.id, share.name,
name = share.name, save = false,
isShare = true, append = false,
isDirectory = false autoplay = false,
shuffle = false,
background = true,
playNext = false,
unpin = true
) )
} }
R.id.share_menu_download -> { R.id.share_menu_download -> {
downloadHandler.value.justDownload( downloadHandler.value.downloadShare(
DownloadAction.DOWNLOAD, this,
fragment = this, share.id,
id = share.id, share.name,
name = share.name, save = false,
isShare = true, append = false,
isDirectory = false autoplay = false,
shuffle = false,
background = true,
playNext = false,
unpin = false
) )
} }
R.id.share_menu_play_now -> { R.id.share_menu_play_now -> {
downloadHandler.value.fetchTracksAndAddToController( downloadHandler.value.downloadShare(
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.fetchTracksAndAddToController( downloadHandler.value.downloadShare(
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,6 +42,7 @@ 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
@ -313,6 +314,7 @@ class MediaPlayerController(
addToPlaylist( addToPlaylist(
state.songs, state.songs,
cachePermanently = false,
autoPlay = false, autoPlay = false,
shuffle = false, shuffle = false,
insertionMode = insertionMode insertionMode = insertionMode
@ -406,6 +408,7 @@ 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
@ -420,6 +423,7 @@ 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,11 +7,17 @@
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
@ -20,8 +26,12 @@ 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.executeTaskWithToast import org.moire.ultrasonic.util.Util
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
@ -29,147 +39,281 @@ import org.moire.ultrasonic.util.executeTaskWithToast
@Suppress("LongParameterList") @Suppress("LongParameterList")
class DownloadHandler( class DownloadHandler(
val mediaPlayerController: MediaPlayerController, val mediaPlayerController: MediaPlayerController,
private val networkAndStorageChecker: NetworkAndStorageChecker val networkAndStorageChecker: NetworkAndStorageChecker
) : CoroutineScope by CoroutineScope(Dispatchers.IO) { ) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
private val maxSongs = 500 private val maxSongs = 500
fun justDownload( /**
action: DownloadAction, * 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(
fragment: Fragment, fragment: Fragment,
id: String? = null, append: Boolean,
name: String? = "", save: Boolean,
isShare: Boolean = false, autoPlay: Boolean,
isDirectory: Boolean = true, playNext: Boolean,
isArtist: Boolean = false, shuffle: Boolean,
tracks: List<Track>? = null songs: List<Track>,
playlistName: String?,
) { ) {
var successString: String? = null val onValid = Runnable {
// TODO: The logic here is different than in the controller...
// Launch the Job val insertionMode = when {
executeTaskWithToast(fragment, { playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT
val tracksToDownload: List<Track> = tracks append -> MediaPlayerController.InsertionMode.APPEND
?: getTracksFromServer(isArtist, id!!, isDirectory, name, isShare) else -> MediaPlayerController.InsertionMode.CLEAR
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( networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
R.plurals.select_album_n_songs_downloaded, mediaPlayerController.addToPlaylist(
tracksToDownload.size, songs,
tracksToDownload.size save,
) autoPlay,
DownloadAction.UNPIN -> { shuffle,
fragment.resources.getQuantityString( insertionMode
R.plurals.select_album_n_songs_unpinned,
tracksToDownload.size,
tracksToDownload.size
) )
if (playlistName != null) {
mediaPlayerController.suggestedPlaylistName = playlistName
} }
DownloadAction.PIN -> { 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( fragment.resources.getQuantityString(
R.plurals.select_album_n_songs_pinned, R.plurals.select_album_n_songs_pinned,
tracksToDownload.size, songs.size,
tracksToDownload.size songs.size
) )
} )
DownloadAction.DELETE -> { } else if (playNext) {
Util.toast(
fragment.context,
fragment.resources.getQuantityString( fragment.resources.getQuantityString(
R.plurals.select_album_n_songs_deleted, R.plurals.select_album_n_songs_play_next,
tracksToDownload.size, songs.size,
tracksToDownload.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()
}) { successString }
} }
fun fetchTracksAndAddToController( fun downloadPlaylist(
fragment: Fragment, fragment: Fragment,
id: String, id: String,
name: String? = "", name: String?,
isShare: Boolean = false, save: Boolean,
isDirectory: Boolean = true, 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, append: Boolean,
autoPlay: Boolean, autoPlay: Boolean,
shuffle: Boolean, shuffle: Boolean,
background: Boolean,
playNext: Boolean, playNext: Boolean,
isArtist: Boolean = false 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
) { ) {
var successString: String? = null
// Launch the Job // Launch the Job
executeTaskWithToast(fragment, { val job = launch(exceptionHandler) {
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,
append = append, background,
playNext = playNext, unpin,
autoPlay = autoPlay, append,
shuffle = shuffle, playNext,
playlistName = null, save,
fragment = fragment autoPlay,
) shuffle,
// Play Now doesn't get a Toast :) fragment
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 }
}
fun addTracksToMediaController( // Create the dialog
songs: List<Track>, 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(
songs: MutableList<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) {
DownloadService.unpin(songs)
} else {
val insertionMode = when { val insertionMode = when {
append -> MediaPlayerController.InsertionMode.APPEND append -> MediaPlayerController.InsertionMode.APPEND
playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT
else -> MediaPlayerController.InsertionMode.CLEAR else -> MediaPlayerController.InsertionMode.CLEAR
} }
if (playlistName != null) {
mediaPlayerController.suggestedPlaylistName = playlistName
}
mediaPlayerController.addToPlaylist( mediaPlayerController.addToPlaylist(
songs, songs,
save,
autoPlay, autoPlay,
shuffle, shuffle,
insertionMode insertionMode
) )
if (Settings.shouldTransitionOnPlayback && (!append || autoPlay)) { if (
fragment.findNavController().popBackStack(R.id.playerFragment, true) !append &&
Settings.shouldTransitionOnPlayback
) {
fragment.findNavController().popBackStack(
R.id.playerFragment,
true
)
fragment.findNavController().navigate(R.id.playerFragment) fragment.findNavController().navigate(R.id.playerFragment)
} }
} }
} else {
if (unpin) {
DownloadService.unpin(songs)
} else {
DownloadService.download(songs, save)
}
}
}
private fun getTracksFromServer( private fun getTracksFromServer(
isArtist: Boolean, isArtist: Boolean,
@ -252,7 +396,3 @@ class DownloadHandler(
} }
} }
} }
enum class DownloadAction {
DOWNLOAD, PIN, UNPIN, DELETE
}

View File

@ -1,82 +0,0 @@
/*
* 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 (all: Exception) { } catch (_: Exception) {
Timber.w(all) // Ignore
} }
} }

View File

@ -70,7 +70,7 @@
<string name="download.menu_save">Guardar lista de reproducción</string> <string name="download.menu_save">Guardar lista de reproducción</string>
<string name="download.menu_screen_off">Pantalla apagada</string> <string name="download.menu_screen_off">Pantalla apagada</string>
<string name="download.menu_screen_on">Pantalla encendida</string> <string name="download.menu_screen_on">Pantalla encendida</string>
<string name="download.menu_show_album">Ir al álbum</string> <string name="download.menu_show_album">Mostrar Álbum</string>
<string name="download.menu_shuffle">Aleatorio</string> <string name="download.menu_shuffle">Aleatorio</string>
<string name="download.menu_shuffle_on">Modo aleatorio activado</string> <string name="download.menu_shuffle_on">Modo aleatorio activado</string>
<string name="download.menu_shuffle_off">Modo aleatorio desactivado</string> <string name="download.menu_shuffle_off">Modo aleatorio desactivado</string>
@ -361,7 +361,7 @@
<string name="share_default_greeting">Echa un vistazo a esta música que te comparto desde %s</string> <string name="share_default_greeting">Echa un vistazo a esta música que te comparto desde %s</string>
<string name="share_via">Compartir canciones vía</string> <string name="share_via">Compartir canciones vía</string>
<string name="menu.share">Compartir</string> <string name="menu.share">Compartir</string>
<string name="download.menu_show_artist">Ir al artista</string> <string name="download.menu_show_artist">Mostrar artista</string>
<string name="albumArt">Portadas de álbumes</string> <string name="albumArt">Portadas de álbumes</string>
<string name="common_multiple_years">Múltiples años</string> <string name="common_multiple_years">Múltiples años</string>
<string name="settings.show_confirmation_dialog">Mostrar diálogo de confirmación</string> <string name="settings.show_confirmation_dialog">Mostrar diálogo de confirmación</string>