Compare commits

..

8 Commits

Author SHA1 Message Date
birdbird
90ffa32246 Merge branch 'FixIDNull' into 'develop'
Fix the warning 'ID must not be null'

See merge request ultrasonic/ultrasonic!999
2023-05-09 10:08:13 +00:00
birdbird
3d94de9e46 Merge branch 'mergeback' into 'develop'
Mergeback

See merge request ultrasonic/ultrasonic!1000
2023-05-09 10:05:18 +00:00
birdbird
50aa2d0a2d Mergeback 2023-05-09 10:05:18 +00:00
tzugen
0e2171b872
Fix the warning 'ID must not be null' 2023-05-09 11:48:10 +02:00
birdbird
2c3f43f139 Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request ultrasonic/ultrasonic!997
2023-05-09 09:34:35 +00:00
birdbird
fd8afe0231 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
2023-05-09 09:34:15 +00:00
birdbird
cd982814cf Use Coroutines for triggering the download or playback of music through the context menus 2023-05-09 09:34:15 +00:00
gallegonovato
338fb618b9
Translated using Weblate (Spanish)
Currently translated at 100.0% (426 of 426 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/
2023-05-08 17:52:45 +02:00
13 changed files with 345 additions and 483 deletions

View File

@ -0,0 +1,8 @@
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 = "7.6" gradle = "8.1.1"
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 116 versionCode 117
versionName "4.3.3" versionName "4.3.4"
minSdkVersion versions.minSdk minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk

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 = {
@ -597,14 +574,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 (getRandomTracks) { } else if (id == null || 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)
@ -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)
} }
} }

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">Mostrar Álbum</string> <string name="download.menu_show_album">Ir al á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">Mostrar artista</string> <string name="download.menu_show_artist">Ir al 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>