From 17260878ac3257c51bb0de5f860d9de06b996086 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Sat, 14 Oct 2023 19:09:26 +0000 Subject: [PATCH] Refactor Koin, Scopes & Lifecycles --- .idea/codeStyles/Project.xml | 5 - ultrasonic/lint-baseline.xml | 44 --- ultrasonic/src/main/AndroidManifest.xml | 14 +- .../ultrasonic/activity/NavigationActivity.kt | 20 +- .../ultrasonic/adapters/ArtistRowBinder.kt | 29 +- .../ultrasonic/adapters/TrackViewHolder.kt | 5 - .../ultrasonic/data/ActiveServerProvider.kt | 49 ++-- .../{playback => data}/CachedDataSource.kt | 4 +- .../moire/ultrasonic/di/ApplicationModule.kt | 5 +- .../moire/ultrasonic/di/MediaPlayerModule.kt | 14 +- .../moire/ultrasonic/di/MusicServiceModule.kt | 8 - .../ultrasonic/fragment/BookmarksFragment.kt | 19 +- .../ultrasonic/fragment/EntryListFragment.kt | 73 +---- .../moire/ultrasonic/fragment/MainFragment.kt | 8 +- .../ultrasonic/fragment/MultiListFragment.kt | 6 +- .../ultrasonic/fragment/NowPlayingFragment.kt | 4 +- .../ultrasonic/fragment/PlayerFragment.kt | 62 ++--- .../ultrasonic/fragment/SearchFragment.kt | 138 ++-------- .../fragment/ServerSelectorFragment.kt | 4 +- .../ultrasonic/fragment/SettingsFragment.kt | 4 +- .../fragment/TrackCollectionFragment.kt | 208 +++++--------- .../fragment/legacy/PlaylistsFragment.kt | 33 +-- .../fragment/legacy/SharesFragment.kt | 49 ++-- .../ultrasonic/imageloader/ImageLoader.kt | 8 +- .../provider/AlbumArtContentProvider.kt | 4 +- .../provider/UltrasonicAppWidgetProvider.kt | 10 +- .../receiver/MediaButtonIntentReceiver.kt | 60 ---- .../receiver/UltrasonicIntentReceiver.kt | 11 +- .../ultrasonic/service/CachedMusicService.kt | 4 + .../ultrasonic/service/DownloadService.kt | 70 +++-- .../moire/ultrasonic/service/DownloadTask.kt | 2 +- .../MediaLibrarySessionCallback.kt} | 39 ++- .../service/MediaPlayerLifecycleSupport.kt | 26 +- .../ultrasonic/service/MediaPlayerManager.kt | 145 +++++++--- .../moire/ultrasonic/service/MusicService.kt | 2 + .../ultrasonic/service/OfflineMusicService.kt | 4 + .../{playback => service}/PlaybackService.kt | 15 +- .../moire/ultrasonic/service/PlaybackState.kt | 11 + .../service/PlaybackStateSerializer.kt | 17 +- .../ultrasonic/service/RESTMusicService.kt | 7 +- .../org/moire/ultrasonic/service/RxBus.kt | 5 +- .../ultrasonic/subsonic/DownloadHandler.kt | 260 ------------------ .../subsonic/ImageLoaderProvider.kt | 4 +- .../subsonic/NetworkAndStorageChecker.kt | 8 +- .../moire/ultrasonic/subsonic/ShareHandler.kt | 212 +++++++------- .../moire/ultrasonic/subsonic/VideoPlayer.kt | 4 +- .../org/moire/ultrasonic/util/CacheCleaner.kt | 12 +- .../org/moire/ultrasonic/util/Constants.kt | 1 + .../moire/ultrasonic/util/ContextMenuUtil.kt | 141 ++++++++++ .../ultrasonic/util/CoroutinePatterns.kt | 77 +++--- .../org/moire/ultrasonic/util/DownloadUtil.kt | 198 +++++++++++++ .../org/moire/ultrasonic/util/Settings.kt | 4 - .../org/moire/ultrasonic/util/Storage.kt | 2 +- .../moire/ultrasonic/util/TimeSpanPicker.kt | 2 +- .../kotlin/org/moire/ultrasonic/util/Util.kt | 60 +++- ultrasonic/src/main/res/values-cs/strings.xml | 4 - ultrasonic/src/main/res/values-de/strings.xml | 4 - ultrasonic/src/main/res/values-es/strings.xml | 4 - ultrasonic/src/main/res/values-fr/strings.xml | 4 - ultrasonic/src/main/res/values-gl/strings.xml | 2 - ultrasonic/src/main/res/values-hu/strings.xml | 4 - ultrasonic/src/main/res/values-it/strings.xml | 4 - ultrasonic/src/main/res/values-ja/strings.xml | 4 - .../src/main/res/values-nb-rNO/strings.xml | 4 - ultrasonic/src/main/res/values-nl/strings.xml | 4 - ultrasonic/src/main/res/values-pl/strings.xml | 4 - .../src/main/res/values-pt-rBR/strings.xml | 4 - ultrasonic/src/main/res/values-pt/strings.xml | 4 - ultrasonic/src/main/res/values-ru/strings.xml | 4 - .../src/main/res/values-zh-rCN/strings.xml | 4 - .../src/main/res/values-zh-rTW/strings.xml | 2 - .../src/main/res/values/setting_keys.xml | 1 - ultrasonic/src/main/res/values/strings.xml | 4 - ultrasonic/src/main/res/xml/settings.xml | 6 - 74 files changed, 1082 insertions(+), 1219 deletions(-) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{playback => data}/CachedDataSource.kt (98%) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{playback/AutoMediaBrowserCallback.kt => service/MediaLibrarySessionCallback.kt} (97%) rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/{playback => service}/PlaybackService.kt (97%) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index f6631329..7643783a 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,11 +1,6 @@ - - diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index f77f8b2f..45e1a6d3 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -70,50 +70,6 @@ column="1"/> - - - - - - - - - - - - - - - - - + + + + + - - - - - { + scoped { MediaPlayerManager(get(), get()) } + scoped { MediaPlayerLifecycleSupport(get(), get(), get(), get()) } + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index 5dd8fb25..6f723c89 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -3,7 +3,6 @@ package org.moire.ultrasonic.di import kotlin.math.abs import okhttp3.logging.HttpLoggingInterceptor -import org.koin.android.ext.koin.androidContext import org.koin.core.qualifier.named import org.koin.dsl.module import org.moire.ultrasonic.BuildConfig @@ -16,9 +15,6 @@ import org.moire.ultrasonic.service.CachedMusicService import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.OfflineMusicService import org.moire.ultrasonic.service.RESTMusicService -import org.moire.ultrasonic.subsonic.DownloadHandler -import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker -import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.util.Constants /** @@ -68,8 +64,4 @@ val musicServiceModule = module { single(named(OFFLINE_MUSIC_SERVICE)) { OfflineMusicService() } - - single { DownloadHandler(get(), get()) } - single { NetworkAndStorageChecker(androidContext()) } - single { ShareHandler(androidContext()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index b959742e..1e7038a9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -17,7 +17,7 @@ import org.moire.ultrasonic.adapters.BaseAdapter import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle -import org.moire.ultrasonic.service.PlaybackState +import org.moire.ultrasonic.service.MediaPlayerManager /** * Lists the Bookmarks available on the server @@ -61,22 +61,21 @@ class BookmarksFragment : TrackCollectionFragment() { } /** - * Custom playback function which uses the restore functionality. A bit of a hack.. + * Play the selected tracks at the bookmarked position */ private fun playNow(songs: List) { if (songs.isNotEmpty()) { - val state = PlaybackState( + mediaPlayerManager.addToPlaylist( songs = songs, - currentPlayingIndex = 0, - currentPlayingPosition = songs[0].bookmarkPosition + autoPlay = false, + shuffle = false, + insertionMode = MediaPlayerManager.InsertionMode.CLEAR ) - mediaPlayerManager.restore( - state = state, - autoPlay = true, - newPlaylist = true - ) + mediaPlayerManager.seekTo(0, songs[0].bookmarkPosition) + mediaPlayerManager.prepare() + mediaPlayerManager.play() } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 075b3e31..1072790e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -11,10 +11,10 @@ import android.os.Bundle import android.view.MenuItem import android.view.View import androidx.core.view.isVisible -import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import io.reactivex.rxjava3.disposables.CompositeDisposable -import org.moire.ultrasonic.R +import org.koin.core.component.KoinScopeComponent +import org.koin.core.component.inject import org.moire.ultrasonic.adapters.FolderSelectorBinder import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Artist @@ -23,17 +23,17 @@ import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign -import org.moire.ultrasonic.subsonic.DownloadAction -import org.moire.ultrasonic.subsonic.DownloadHandler +import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu /** * An extension of the MultiListFragment, with a few helper functions geared * towards the display of MusicDirectory.Entries. * @param T: The type of data which will be used (must extend GenericEntry) */ -abstract class EntryListFragment : MultiListFragment() { +abstract class EntryListFragment : MultiListFragment(), KoinScopeComponent { private var rxBusSubscription: CompositeDisposable = CompositeDisposable() + private val mediaPlayerManager: MediaPlayerManager by inject() /** * Whether to show the folder selector @@ -46,7 +46,7 @@ abstract class EntryListFragment : MultiListFragment() { override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean { val isArtist = (item is Artist) - return handleContextMenu(menuItem, item, isArtist, downloadHandler, this) + return handleContextMenu(menuItem, item, isArtist, mediaPlayerManager, this) } override fun onItemClick(item: T) { @@ -119,65 +119,4 @@ abstract class EntryListFragment : MultiListFragment() { header } - - companion object { - @Suppress("LongMethod") - internal fun handleContextMenu( - menuItem: MenuItem, - item: Identifiable, - isArtist: Boolean, - downloadHandler: DownloadHandler, - fragment: Fragment - ): Boolean { - when (menuItem.itemId) { - R.id.menu_play_now -> - downloadHandler.fetchTracksAndAddToController( - fragment, - item.id, - insertionMode = MediaPlayerManager.InsertionMode.CLEAR, - autoPlay = true, - isArtist = isArtist - ) - R.id.menu_play_next -> - downloadHandler.fetchTracksAndAddToController( - fragment, - item.id, - insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT, - autoPlay = true, - isArtist = isArtist - ) - R.id.menu_play_last -> - downloadHandler.fetchTracksAndAddToController( - fragment, - item.id, - insertionMode = MediaPlayerManager.InsertionMode.APPEND, - autoPlay = false, - isArtist = isArtist - ) - R.id.menu_pin -> - downloadHandler.justDownload( - action = DownloadAction.PIN, - fragment, - item.id, - isArtist = isArtist - ) - R.id.menu_unpin -> - downloadHandler.justDownload( - action = DownloadAction.UNPIN, - fragment, - item.id, - isArtist = isArtist - ) - R.id.menu_download -> - downloadHandler.justDownload( - action = DownloadAction.DOWNLOAD, - fragment, - item.id, - isArtist = isArtist - ) - else -> return false - } - return true - } - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt index e64d1e80..6ef9e248 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt @@ -20,7 +20,11 @@ import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import java.lang.ref.SoftReference -import org.koin.core.component.KoinComponent +import kotlin.collections.HashMap +import kotlin.collections.hashMapOf +import kotlin.collections.set +import org.koin.androidx.scope.ScopeFragment +import org.koin.core.component.KoinScopeComponent import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.models.AlbumListType @@ -34,7 +38,7 @@ import org.moire.ultrasonic.view.SortOrder import org.moire.ultrasonic.view.ViewCapabilities import timber.log.Timber -class MainFragment : Fragment(), KoinComponent { +class MainFragment : ScopeFragment(), KoinScopeComponent { private var filterButtonBar: FilterButtonBar? = null private var layoutType: LayoutType = LayoutType.COVER diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index 6cc94b1d..0d7cca69 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -17,7 +17,6 @@ import android.view.ViewGroup import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible -import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -26,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlinx.coroutines.CoroutineExceptionHandler import org.koin.android.ext.android.inject +import org.koin.androidx.scope.ScopeFragment import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.BaseAdapter @@ -33,7 +33,6 @@ import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.model.GenericListModel import org.moire.ultrasonic.model.ServerSettingsModel -import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Util @@ -42,11 +41,10 @@ import org.moire.ultrasonic.util.Util * An abstract Model, which can be extended to display a list of items of type T from the API * @param T: The type of data which will be used (must extend GenericEntry) */ -abstract class MultiListFragment : Fragment() { +abstract class MultiListFragment : ScopeFragment() { internal val activeServerProvider: ActiveServerProvider by inject() internal val serverSettingsModel: ServerSettingsModel by viewModel() internal val imageLoaderProvider: ImageLoaderProvider by inject() - protected val downloadHandler: DownloadHandler by inject() protected var refreshListView: SwipeRefreshLayout? = null internal var listView: RecyclerView? = null internal lateinit var viewManager: LinearLayoutManager diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index 70eaf028..28cdb042 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -15,7 +15,6 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView -import androidx.fragment.app.Fragment import androidx.navigation.Navigation import androidx.navigation.fragment.findNavController import com.google.android.material.button.MaterialButton @@ -23,6 +22,7 @@ import io.reactivex.rxjava3.disposables.Disposable import java.lang.Exception import kotlin.math.abs import org.koin.android.ext.android.inject +import org.koin.androidx.scope.ScopeFragment import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.service.MediaPlayerManager @@ -37,7 +37,7 @@ import timber.log.Timber /** * Contains the mini-now playing information box displayed at the bottom of the screen */ -class NowPlayingFragment : Fragment() { +class NowPlayingFragment : ScopeFragment() { private var downX = 0f private var downY = 0f diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 52394da5..2e849731 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -45,7 +45,6 @@ import androidx.core.content.res.ResourcesCompat import androidx.core.view.MenuHost import androidx.core.view.MenuProvider import androidx.core.view.isVisible -import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem @@ -79,12 +78,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import org.koin.android.ext.android.inject -import org.koin.core.component.KoinComponent +import org.koin.androidx.scope.ScopeFragment +import org.koin.core.component.KoinScopeComponent import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.BaseAdapter import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.api.subsonic.models.AlbumListType -import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags @@ -106,6 +105,7 @@ import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.ConfirmationDialog import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.util.Util.toast import org.moire.ultrasonic.util.toTrack import org.moire.ultrasonic.view.AutoRepeatButton import timber.log.Timber @@ -116,9 +116,9 @@ import timber.log.Timber */ @Suppress("LargeClass", "TooManyFunctions", "MagicNumber") class PlayerFragment : - Fragment(), + ScopeFragment(), GestureDetector.OnGestureListener, - KoinComponent, + KoinScopeComponent, CoroutineScope by CoroutineScope(Dispatchers.Main) { // Settings @@ -356,14 +356,14 @@ class PlayerFragment : onPlaylistChanged() when (newRepeat) { - 0 -> Util.toast( - context, R.string.download_repeat_off + 0 -> toast( + R.string.download_repeat_off ) - 1 -> Util.toast( - context, R.string.download_repeat_single + 1 -> toast( + R.string.download_repeat_single ) - 2 -> Util.toast( - context, R.string.download_repeat_all + 2 -> toast( + R.string.download_repeat_all ) else -> { } @@ -410,7 +410,7 @@ class PlayerFragment : // Query the Jukebox state in an IO Context ioScope.launch(CommunicationError.getHandler(context)) { try { - jukeboxAvailable = mediaPlayerManager.isJukeboxAvailable + jukeboxAvailable = getMusicService().isJukeboxAvailable() } catch (all: Exception) { Timber.e(all) } @@ -457,9 +457,9 @@ class PlayerFragment : val isEnabled = mediaPlayerManager.toggleShuffle() if (isEnabled) { - Util.toast(activity, R.string.download_menu_shuffle_on) + toast(R.string.download_menu_shuffle_on) } else { - Util.toast(activity, R.string.download_menu_shuffle_off) + toast(R.string.download_menu_shuffle_off) } updateShuffleButtonState(isEnabled) @@ -579,8 +579,7 @@ class PlayerFragment : equalizerMenuItem.isVisible = isEqualizerAvailable } - val mediaPlayerController = mediaPlayerManager - val track = mediaPlayerController.currentMediaItem?.toTrack() + val track = mediaPlayerManager.currentMediaItem?.toTrack() if (track != null) { currentSong = track @@ -600,7 +599,7 @@ class PlayerFragment : goToArtist.isVisible = false } - if (mediaPlayerController.keepScreenOn) { + if (mediaPlayerManager.keepScreenOn) { screenOption?.setTitle(R.string.download_menu_screen_off) } else { screenOption?.setTitle(R.string.download_menu_screen_on) @@ -609,7 +608,7 @@ class PlayerFragment : if (jukeboxOption != null) { jukeboxOption.isEnabled = jukeboxAvailable jukeboxOption.isVisible = jukeboxAvailable - if (mediaPlayerController.isJukeboxEnabled) { + if (mediaPlayerManager.isJukeboxEnabled) { jukeboxOption.setTitle(R.string.download_menu_jukebox_off) } else { jukeboxOption.setTitle(R.string.download_menu_jukebox_on) @@ -707,8 +706,7 @@ class PlayerFragment : R.id.menu_item_jukebox -> { val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled - Util.toast( - context, + toast( if (jukeboxEnabled) R.string.download_jukebox_on else R.string.download_jukebox_off, false @@ -760,7 +758,7 @@ class PlayerFragment : R.string.download_bookmark_set_at_position, bookmarkTime ) - Util.toast(context, msg) + toast(msg) return true } R.id.menu_item_bookmark_delete -> { @@ -776,13 +774,12 @@ class PlayerFragment : Timber.e(all) } }.start() - Util.toast(context, R.string.download_bookmark_removed) + toast(R.string.download_bookmark_removed) return true } R.id.menu_item_share -> { - val mediaPlayerController = mediaPlayerManager val tracks: MutableList = ArrayList() - val playlist = mediaPlayerController.playlist + val playlist = mediaPlayerManager.playlist for (item in playlist) { val playlistEntry = item.toTrack() tracks.add(playlistEntry) @@ -790,8 +787,6 @@ class PlayerFragment : shareHandler.createShare( this, tracks = tracks, - swipe = null, - cancellationToken = cancellationToken, ) return true } @@ -804,8 +799,6 @@ class PlayerFragment : shareHandler.createShare( this, tracks, - swipe = null, - cancellationToken = cancellationToken ) return true } @@ -822,7 +815,7 @@ class PlayerFragment : } private fun savePlaylistInBackground(playlistName: String) { - Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName)) + toast(resources.getString(R.string.download_playlist_saving, playlistName)) mediaPlayerManager.suggestedPlaylistName = playlistName // The playlist can be acquired only from the main thread @@ -835,7 +828,7 @@ class PlayerFragment : musicService.createPlaylist(null, playlistName, entries) }.invokeOnCompletion { if (it == null || it is CancellationException) { - Util.toast(UApp.applicationContext(), R.string.download_playlist_done) + toast(R.string.download_playlist_done) } else { Timber.e(it, "Exception has occurred in savePlaylistInBackground") val msg = String.format( @@ -844,7 +837,7 @@ class PlayerFragment : resources.getString(R.string.download_playlist_error), CommunicationError.getErrorMessage(it) ) - Util.toast(UApp.applicationContext(), msg) + toast(msg) } } } @@ -958,7 +951,7 @@ class PlayerFragment : item?.mediaMetadata?.title ) - Util.toast(context, songRemoved) + toast(songRemoved) // Remove the item from the playlist mediaPlayerManager.removeFromPlaylist(pos) @@ -1059,15 +1052,14 @@ class PlayerFragment : } private fun onPlaylistChanged() { - val mediaPlayerController = mediaPlayerManager // Try to display playlist in play order - val list = mediaPlayerController.playlistInPlayOrder + val list = mediaPlayerManager.playlistInPlayOrder emptyTextView.setText(R.string.playlist_empty) viewAdapter.submitList(list.map(MediaItem::toTrack)) progressIndicator.isVisible = false emptyView.isVisible = list.isEmpty() - updateRepeatButtonState(mediaPlayerController.repeatMode) + updateRepeatButtonState(mediaPlayerManager.repeatMode) } private fun onTrackChanged() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 40d71cf3..048d3961 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -17,7 +17,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent +import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.AlbumRowDelegate @@ -36,13 +36,12 @@ import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.model.SearchListModel -import org.moire.ultrasonic.service.DownloadService import org.moire.ultrasonic.service.MediaPlayerManager -import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker -import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CommunicationError +import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu +import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenuTracks import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.toast @@ -51,15 +50,12 @@ import org.moire.ultrasonic.util.Util.toast * Initiates a search on the media library and displays the results */ -class SearchFragment : MultiListFragment(), KoinComponent { +class SearchFragment : MultiListFragment(), KoinScopeComponent { private var searchResult: SearchResult? = null private var searchRefresh: SwipeRefreshLayout? = null private val mediaPlayerManager: MediaPlayerManager by inject() - private val shareHandler: ShareHandler by inject() - private val networkAndStorageChecker: NetworkAndStorageChecker by inject() - private var cancellationToken: CancellationToken? = null private val navArgs by navArgs() @@ -137,18 +133,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { super.onDestroyView() } - private fun downloadBackground(save: Boolean, songs: List) { - val onValid = Runnable { - networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - DownloadService.download( - songs.filterNotNull(), - save = save, - updateSaveFlag = true - ) - } - onValid.run() - } - private fun search(query: String, autoplay: Boolean) { listModel.viewModelScope.launch(CommunicationError.getHandler(context)) { refreshListView?.isRefreshing = true @@ -253,7 +237,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { insertionMode = MediaPlayerManager.InsertionMode.APPEND ) mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1) - toast(context, resources.getQuantityString(R.plurals.n_songs_added_to_end, 1, 1)) + toast(resources.getQuantityString(R.plurals.n_songs_added_to_end, 1, 1)) } private fun onVideoSelected(track: Track) { @@ -288,103 +272,23 @@ class SearchFragment : MultiListFragment(), KoinComponent { @Suppress("LongMethod") override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean { - val isArtist = (item is Artist) - - val found = EntryListFragment.handleContextMenu( - menuItem, - item, - isArtist, - downloadHandler, - this - ) - - if (found || item !is Track) return true - - val songs = mutableListOf() - - when (menuItem.itemId) { - R.id.song_menu_play_now -> { - songs.add(item) - downloadHandler.addTracksToMediaController( - songs = songs, - insertionMode = MediaPlayerManager.InsertionMode.CLEAR, - autoPlay = true, - shuffle = false, - fragment = this, - playlistName = null - ) - } - R.id.song_menu_play_next -> { - songs.add(item) - downloadHandler.addTracksToMediaController( - songs = songs, - insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT, - autoPlay = false, - shuffle = false, - fragment = this, - playlistName = null - ) - } - R.id.song_menu_play_last -> { - songs.add(item) - downloadHandler.addTracksToMediaController( - songs = songs, - insertionMode = MediaPlayerManager.InsertionMode.APPEND, - autoPlay = false, - shuffle = false, - fragment = this, - playlistName = null - ) - } - R.id.song_menu_pin -> { - songs.add(item) - toast( - context, - resources.getQuantityString( - R.plurals.n_songs_pinned, - songs.size, - songs.size - ) - ) - downloadBackground(true, songs) - } - R.id.song_menu_download -> { - songs.add(item) - toast( - context, - resources.getQuantityString( - R.plurals.n_songs_to_be_downloaded, - songs.size, - songs.size - ) - ) - downloadBackground(false, songs) - } - R.id.song_menu_unpin -> { - songs.add(item) - toast( - context, - resources.getQuantityString( - R.plurals.n_songs_unpinned, - songs.size, - songs.size - ) - ) - DownloadService.unpin(songs) - } - R.id.song_menu_share -> { - songs.add(item) - shareHandler.createShare( - fragment = this, - tracks = songs, - swipe = searchRefresh, - cancellationToken = cancellationToken!!, - additionalId = null - ) - } + // Here the Item could be a track or an album or an artist + if (item is Track) { + return handleContextMenuTracks( + menuItem = menuItem, + tracks = listOf(item), + mediaPlayerManager = mediaPlayerManager, + fragment = this + ) + } else { + return handleContextMenu( + menuItem = menuItem, + item = item, + isArtist = item is Artist, + mediaPlayerManager = mediaPlayerManager, + fragment = this + ) } - - return true } companion object { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index 5f79b53f..50b6399d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -64,7 +64,7 @@ class ServerSelectorFragment : Fragment() { listView?.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ -> val server = parent.getItemAtPosition(position) as ServerSetting - ActiveServerProvider.setActiveServerById(server.id) + activeServerProvider.setActiveServerById(server.id) findNavController().popBackStack(R.id.mainFragment, false) } @@ -99,7 +99,7 @@ class ServerSelectorFragment : Fragment() { val activeServerId = ActiveServerProvider.getActiveServerId() // If the currently active server is deleted, go offline - if (id == activeServerId) ActiveServerProvider.setActiveServerById(OFFLINE_DB_ID) + if (id == activeServerId) activeServerProvider.setActiveServerById(OFFLINE_DB_ID) serverSettingsModel.deleteItemById(id) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index 9e654186..36a8c264 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -300,7 +300,7 @@ class SettingsFragment : SearchSuggestionProvider.MODE ) suggestions.clearHistory() - toast(activity, R.string.settings_search_history_cleared) + toast(R.string.settings_search_history_cleared) false } } @@ -332,7 +332,7 @@ class SettingsFragment : Timber.w("Failed to delete %s", nomediaDir) } } - toast(activity, R.string.settings_hide_media_toast, false) + toast(R.string.settings_hide_media_toast, false) } private fun setCacheLocation(path: String) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 108016dc..ee8193a9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -46,14 +46,16 @@ import org.moire.ultrasonic.model.TrackCollectionModel import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign -import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.VideoPlayer -import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.ConfirmationDialog +import org.moire.ultrasonic.util.ContextMenuUtil +import org.moire.ultrasonic.util.DownloadAction +import org.moire.ultrasonic.util.DownloadUtil import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.util.Util.navigateToCurrent +import org.moire.ultrasonic.util.Util.toast import org.moire.ultrasonic.view.SortOrder import org.moire.ultrasonic.view.ViewCapabilities import timber.log.Timber @@ -86,7 +88,6 @@ open class TrackCollectionFragment( internal val mediaPlayerManager: MediaPlayerManager by inject() private val shareHandler: ShareHandler by inject() - internal var cancellationToken: CancellationToken? = null override val listModel: TrackCollectionModel by viewModels() private val rxBusSubscription: CompositeDisposable = CompositeDisposable() @@ -102,7 +103,6 @@ open class TrackCollectionFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - cancellationToken = CancellationToken() albumButtons = view.findViewById(R.id.menu_album) @@ -211,19 +211,23 @@ open class TrackCollectionFragment( } playNowButton?.setOnClickListener { - playNow(MediaPlayerManager.InsertionMode.CLEAR, toast = true) + playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.CLEAR) } playNextButton?.setOnClickListener { - playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, toast = true) + playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.AFTER_CURRENT) } playLastButton!!.setOnClickListener { - playNow(MediaPlayerManager.InsertionMode.APPEND, toast = true) + playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.APPEND) } pinButton?.setOnClickListener { - downloadBackground(true) + downloadSelectedOrAllTracks(true) + } + + downloadButton?.setOnClickListener { + downloadSelectedOrAllTracks(false) } unpinButton?.setOnClickListener { @@ -231,26 +235,22 @@ open class TrackCollectionFragment( ConfirmationDialog.Builder(requireContext()) .setMessage(R.string.common_unpin_selection_confirmation) .setPositiveButton(R.string.common_unpin) { _, _ -> - unpin() + unpinSelectedTracks() }.show() } else { - unpin() + unpinSelectedTracks() } } - downloadButton?.setOnClickListener { - downloadBackground(false) - } - deleteButton?.setOnClickListener { if (Settings.showConfirmationDialog) { ConfirmationDialog.Builder(requireContext()) .setMessage(R.string.common_delete_selection_confirmation) .setPositiveButton(R.string.common_delete) { _, _ -> - delete() + deleteSelectedTracks() }.show() } else { - delete() + deleteSelectedTracks() } } } @@ -283,9 +283,9 @@ open class TrackCollectionFragment( return true } else if (item.itemId == R.id.menu_item_share) { shareHandler.createShare( - this@TrackCollectionFragment, getSelectedTracks(), - refreshListView, cancellationToken!!, - navArgs.id + fragment = this@TrackCollectionFragment, + tracks = getSelectedTracks(), + additionalId = navArgs.id ) return true } @@ -294,46 +294,10 @@ open class TrackCollectionFragment( } override fun onDestroyView() { - cancellationToken!!.cancel() rxBusSubscription.dispose() super.onDestroyView() } - private fun playNow( - insertionMode: MediaPlayerManager.InsertionMode, - selectedTracks: List = getSelectedTracks(), - toast: Boolean = false - ) { - if (selectedTracks.isNotEmpty()) { - downloadHandler.addTracksToMediaController( - songs = selectedTracks, - insertionMode = insertionMode, - autoPlay = (insertionMode == MediaPlayerManager.InsertionMode.CLEAR), - playlistName = null, - fragment = this - ) - } else { - playAll(false, insertionMode) - } - - if (toast) { - val stringInt = when (insertionMode) { - MediaPlayerManager.InsertionMode.CLEAR -> - R.plurals.n_songs_added_play_now - MediaPlayerManager.InsertionMode.AFTER_CURRENT -> - R.plurals.n_songs_added_after_current - MediaPlayerManager.InsertionMode.APPEND -> - R.plurals.n_songs_added_to_end - } - val msg = resources.getQuantityString( - stringInt, - selectedTracks.size, - selectedTracks.size - ) - Util.toast(requireContext(), msg) - } - } - /** * Get the size of the underlying list */ @@ -364,25 +328,65 @@ open class TrackCollectionFragment( // Need a valid id to recurse sub directories stuff if (hasSubFolders && navArgs.id != null) { - downloadHandler.fetchTracksAndAddToController( + mediaPlayerManager.playTracksAndToast( fragment = this, - id = navArgs.id!!, insertionMode = insertionMode, - autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND), + id = navArgs.id!!, shuffle = shuffle, isArtist = isArtist ) } else { - downloadHandler.addTracksToMediaController( + mediaPlayerManager.suggestedPlaylistName = navArgs.playlistName + mediaPlayerManager.addToPlaylist( songs = getAllSongs(), insertionMode = insertionMode, autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND), - shuffle = shuffle, - playlistName = navArgs.playlistName, - fragment = this + shuffle = shuffle ) + if (insertionMode == MediaPlayerManager.InsertionMode.CLEAR) { + navigateToCurrent() + } } } + private fun unpinSelectedTracks() { + DownloadUtil.justDownload( + action = DownloadAction.UNPIN, + fragment = this, + tracks = getSelectedTracks() + ) + } + + private fun downloadSelectedOrAllTracks(save: Boolean) { + var tracks = getSelectedTracks() + if (tracks.isEmpty()) tracks = getAllSongs() + + DownloadUtil.justDownload( + action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD, + fragment = this, + tracks = tracks + ) + } + + private fun playSelectedOrAllTracks( + insertionMode: MediaPlayerManager.InsertionMode + ) { + var tracks = getSelectedTracks() + if (tracks.isEmpty()) tracks = getAllSongs() + + mediaPlayerManager.playTracksAndToast( + fragment = this, + insertionMode = insertionMode, + tracks = tracks + ) + } + + private fun deleteSelectedTracks() { + DownloadUtil.justDownload( + action = DownloadAction.DELETE, + fragment = this, + tracks = getSelectedTracks() + ) + } @Suppress("UNCHECKED_CAST") private fun getAllSongs(): List { @@ -403,7 +407,7 @@ open class TrackCollectionFragment( // Display toast: N tracks selected val toastResId = R.string.select_album_n_selected - Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0))) + toast(getString(toastResId, selectedCount.coerceAtLeast(0))) } @Synchronized @@ -431,37 +435,6 @@ open class TrackCollectionFragment( } } - private fun downloadBackground(save: Boolean, tracks: List = getSelectedTracks()) { - var songs = tracks - - if (songs.isEmpty()) { - songs = getAllSongs() - } - - val action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD - downloadHandler.justDownload( - action = action, - fragment = this, - tracks = songs - ) - } - - internal fun delete(songs: List = getSelectedTracks()) { - downloadHandler.justDownload( - action = DownloadAction.DELETE, - fragment = this, - tracks = songs - ) - } - - internal fun unpin(songs: List = getSelectedTracks()) { - downloadHandler.justDownload( - action = DownloadAction.UNPIN, - fragment = this, - tracks = songs - ) - } - override val defaultObserver: (List) -> Unit = { Timber.i("Received list") @@ -606,48 +579,19 @@ open class TrackCollectionFragment( private fun displayRandom() = (sortOrder == SortOrder.RANDOM) || navArgs.getRandom - @Suppress("LongMethod") override fun onContextMenuItemSelected( menuItem: MenuItem, item: MusicDirectory.Child ): Boolean { - val songs = getClickedSong(item) - when (menuItem.itemId) { - R.id.song_menu_play_now -> { - playNow(MediaPlayerManager.InsertionMode.CLEAR, songs, true) - } - R.id.song_menu_play_next -> { - playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, songs, true) - } - R.id.song_menu_play_last -> { - playNow(MediaPlayerManager.InsertionMode.APPEND, songs, true) - } - R.id.song_menu_pin -> { - downloadBackground(true, songs) - } - R.id.song_menu_unpin -> { - unpin(songs) - } - R.id.song_menu_download -> { - downloadBackground(false, songs) - } - R.id.song_menu_share -> { - if (item is Track) { - shareHandler.createShare( - this, - tracks = listOf(item), - swipe = refreshListView, - cancellationToken = cancellationToken!!, - additionalId = navArgs.id - ) - } - } - else -> { - return super.onContextItemSelected(menuItem) - } - } - return true + val tracks = getClickedSong(item) + + return ContextMenuUtil.handleContextMenuTracks( + menuItem = menuItem, + tracks = tracks, + mediaPlayerManager = mediaPlayerManager, + fragment = this + ) } private fun getClickedSong(item: MusicDirectory.Child): List { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt index ebffc3cc..91d74642 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt @@ -25,11 +25,11 @@ import android.widget.CheckBox import android.widget.EditText import android.widget.ListView import android.widget.TextView -import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import java.util.Locale -import org.koin.core.component.KoinComponent +import org.koin.androidx.scope.ScopeFragment +import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R @@ -39,12 +39,12 @@ import org.moire.ultrasonic.domain.Playlist import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.OfflineException -import org.moire.ultrasonic.subsonic.DownloadAction -import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.ConfirmationDialog +import org.moire.ultrasonic.util.DownloadAction +import org.moire.ultrasonic.util.DownloadUtil import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.InfoDialog import org.moire.ultrasonic.util.LoadingTask @@ -56,14 +56,12 @@ import org.moire.ultrasonic.util.Util.toast * * TODO: This file has been converted from Java, but not modernized yet. */ -class PlaylistsFragment : Fragment(), KoinComponent { +class PlaylistsFragment : ScopeFragment(), KoinScopeComponent { private var refreshPlaylistsListView: SwipeRefreshLayout? = null private var playlistsListView: ListView? = null private var emptyTextView: View? = null private var playlistAdapter: ArrayAdapter? = null - private val downloadHandler by inject() - private var cancellationToken: CancellationToken? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -115,7 +113,8 @@ class PlaylistsFragment : Fragment(), KoinComponent { override fun doInBackground(): List { val musicService = getMusicService() val playlists = musicService.getPlaylists(refresh) - if (!isOffline()) CacheCleaner().cleanPlaylists(playlists) + val cacheCleaner: CacheCleaner by inject() + if (!isOffline()) cacheCleaner.cleanPlaylists(playlists) return playlists } @@ -147,8 +146,8 @@ class PlaylistsFragment : Fragment(), KoinComponent { val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist when (menuItem.itemId) { R.id.playlist_menu_pin -> { - downloadHandler.justDownload( - DownloadAction.PIN, + DownloadUtil.justDownload( + action = DownloadAction.PIN, fragment = this, id = playlist.id, name = playlist.name, @@ -157,8 +156,8 @@ class PlaylistsFragment : Fragment(), KoinComponent { ) } R.id.playlist_menu_unpin -> { - downloadHandler.justDownload( - DownloadAction.UNPIN, + DownloadUtil.justDownload( + action = DownloadAction.UNPIN, fragment = this, id = playlist.id, name = playlist.name, @@ -167,8 +166,8 @@ class PlaylistsFragment : Fragment(), KoinComponent { ) } R.id.playlist_menu_download -> { - downloadHandler.justDownload( - DownloadAction.DOWNLOAD, + DownloadUtil.justDownload( + action = DownloadAction.DOWNLOAD, fragment = this, id = playlist.id, name = playlist.name, @@ -227,7 +226,6 @@ class PlaylistsFragment : Fragment(), KoinComponent { playlistAdapter!!.remove(playlist) playlistAdapter!!.notifyDataSetChanged() toast( - context, resources.getString(R.string.menu_deleted_playlist, playlist.name) ) } @@ -246,7 +244,7 @@ class PlaylistsFragment : Fragment(), KoinComponent { ), getErrorMessage(error) ) - toast(context, msg, false) + toast(msg, false) } }.execute() }.setNegativeButton(R.string.common_cancel, null).show() @@ -310,7 +308,6 @@ class PlaylistsFragment : Fragment(), KoinComponent { override fun done(result: Any?) { load(true) toast( - context, resources.getString(R.string.playlist_updated_info, playlist.name) ) } @@ -329,7 +326,7 @@ class PlaylistsFragment : Fragment(), KoinComponent { ), getErrorMessage(error) ) - toast(context, msg, false) + toast(msg, false) } }.execute() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt index 32bb6176..981cb3b9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt @@ -24,11 +24,11 @@ import android.widget.CheckBox import android.widget.EditText import android.widget.ListView import android.widget.TextView -import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import java.util.Locale -import org.koin.core.component.KoinComponent +import org.koin.androidx.scope.ScopeFragment +import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R @@ -38,14 +38,15 @@ import org.moire.ultrasonic.fragment.FragmentTitle import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.OfflineException -import org.moire.ultrasonic.subsonic.DownloadAction -import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.CancellationToken +import org.moire.ultrasonic.util.DownloadAction +import org.moire.ultrasonic.util.DownloadUtil import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.LoadingTask import org.moire.ultrasonic.util.TimeSpanPicker import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.util.Util.toast import org.moire.ultrasonic.view.ShareAdapter /** @@ -53,12 +54,12 @@ import org.moire.ultrasonic.view.ShareAdapter * * TODO: This file has been converted from Java, but not modernized yet. */ -class SharesFragment : Fragment(), KoinComponent { +class SharesFragment : ScopeFragment(), KoinScopeComponent { private var refreshSharesListView: SwipeRefreshLayout? = null private var sharesListView: ListView? = null private var emptyTextView: View? = null private var shareAdapter: ShareAdapter? = null - private val downloadHandler: DownloadHandler by inject() + private val mediaPlayerManager: MediaPlayerManager by inject() private var cancellationToken: CancellationToken? = null override fun onCreate(savedInstanceState: Bundle?) { Util.applyTheme(this.context) @@ -133,8 +134,8 @@ class SharesFragment : Fragment(), KoinComponent { val share = sharesListView!!.getItemAtPosition(info.position) as Share when (menuItem.itemId) { R.id.share_menu_pin -> { - downloadHandler.justDownload( - DownloadAction.PIN, + DownloadUtil.justDownload( + action = DownloadAction.PIN, fragment = this, id = share.id, name = share.name, @@ -143,8 +144,8 @@ class SharesFragment : Fragment(), KoinComponent { ) } R.id.share_menu_unpin -> { - downloadHandler.justDownload( - DownloadAction.UNPIN, + DownloadUtil.justDownload( + action = DownloadAction.UNPIN, fragment = this, id = share.id, name = share.name, @@ -153,8 +154,8 @@ class SharesFragment : Fragment(), KoinComponent { ) } R.id.share_menu_download -> { - downloadHandler.justDownload( - DownloadAction.DOWNLOAD, + DownloadUtil.justDownload( + action = DownloadAction.DOWNLOAD, fragment = this, id = share.id, name = share.name, @@ -163,22 +164,20 @@ class SharesFragment : Fragment(), KoinComponent { ) } R.id.share_menu_play_now -> { - downloadHandler.fetchTracksAndAddToController( + mediaPlayerManager.playTracksAndToast( this, - share.id, - share.name, insertionMode = MediaPlayerManager.InsertionMode.CLEAR, - autoPlay = true, + id = share.id, + name = share.name, shuffle = false ) } R.id.share_menu_play_shuffled -> { - downloadHandler.fetchTracksAndAddToController( + mediaPlayerManager.playTracksAndToast( this, - share.id, - share.name, insertionMode = MediaPlayerManager.InsertionMode.CLEAR, - autoPlay = true, + id = share.id, + name = share.name, shuffle = true, ) } @@ -214,8 +213,7 @@ class SharesFragment : Fragment(), KoinComponent { override fun done(result: Any?) { shareAdapter!!.remove(share) shareAdapter!!.notifyDataSetChanged() - Util.toast( - context, + toast( resources.getString(R.string.menu_deleted_share, share.name) ) } @@ -237,7 +235,7 @@ class SharesFragment : Fragment(), KoinComponent { getErrorMessage(error) ) } - Util.toast(context, msg, false) + toast(msg, false) } }.execute() }.setNegativeButton(R.string.common_cancel, null).show() @@ -315,8 +313,7 @@ class SharesFragment : Fragment(), KoinComponent { override fun done(result: Any?) { load(true) - Util.toast( - context, + toast( resources.getString(R.string.playlist_updated_info, share.name) ) } @@ -338,7 +335,7 @@ class SharesFragment : Fragment(), KoinComponent { getErrorMessage(error) ) } - Util.toast(context, msg, false) + toast(msg, false) } }.execute() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index 06bc30ff..a63417ca 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -190,17 +190,17 @@ class ImageLoader( if (artist.coverArt == null) return val key = FileUtil.getArtistArtKey(artist.name, false) val file = FileUtil.getAlbumArtFile(key) - cacheCoverArt(artist.coverArt!!, file) + downloadCoverArt(artist.coverArt!!, file) } /** * Download a cover art file of a Track and cache it on disk */ - fun cacheCoverArt(track: Track) { - cacheCoverArt(track.coverArt!!, FileUtil.getAlbumArtFile(track)) + fun downloadCoverArt(track: Track) { + downloadCoverArt(track.coverArt!!, FileUtil.getAlbumArtFile(track)) } - fun cacheCoverArt(id: String, file: String) = launch { + fun downloadCoverArt(id: String, file: String) = launch { if (id.isBlank()) return@launch withContext(Dispatchers.IO) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/AlbumArtContentProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/AlbumArtContentProvider.kt index 51b3ed52..026fb22f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/AlbumArtContentProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/AlbumArtContentProvider.kt @@ -56,8 +56,10 @@ class AlbumArtContentProvider : ContentProvider(), KoinComponent { val albumArtFile = FileUtil.getAlbumArtFile(parts[1]) Timber.d("AlbumArtContentProvider openFile id: %s; file: %s", parts[0], albumArtFile) + + // TODO: Check if the dependency on the image loader could be removed. imageLoaderProvider.executeOn { - it.cacheCoverArt(parts[0], albumArtFile) + it.downloadCoverArt(parts[0], albumArtFile) } val file = File(albumArtFile) if (!file.exists()) return null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt index 3a422a2b..193bbe52 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt @@ -23,7 +23,7 @@ import android.widget.RemoteViews import org.moire.ultrasonic.R import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver +import org.moire.ultrasonic.receiver.UltrasonicIntentReceiver import org.moire.ultrasonic.util.Constants import timber.log.Timber @@ -233,7 +233,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() { // Emulate media button clicks. intent = Intent(Constants.CMD_PROCESS_KEYCODE) - intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java) + intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java) intent.putExtra( Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) @@ -241,12 +241,12 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() { flags = 0 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // needed starting Android 12 (S = 31) - flags = flags or PendingIntent.FLAG_IMMUTABLE + flags = PendingIntent.FLAG_IMMUTABLE } pendingIntent = PendingIntent.getBroadcast(context, 11, intent, flags) views.setOnClickPendingIntent(R.id.control_play, pendingIntent) intent = Intent(Constants.CMD_PROCESS_KEYCODE) - intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java) + intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java) intent.putExtra( Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT) @@ -254,7 +254,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() { pendingIntent = PendingIntent.getBroadcast(context, 12, intent, flags) views.setOnClickPendingIntent(R.id.control_next, pendingIntent) intent = Intent(Constants.CMD_PROCESS_KEYCODE) - intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java) + intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java) intent.putExtra( Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt deleted file mode 100644 index 6e40062f..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * MediaButtonIntentReceiver.kt - * Copyright (C) 2009-2022 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ - -package org.moire.ultrasonic.receiver - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.Parcelable -import java.lang.Exception -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport -import org.moire.ultrasonic.util.Constants -import org.moire.ultrasonic.util.Settings -import timber.log.Timber - -/** - * This class is used to receive commands from the widget - */ -class MediaButtonIntentReceiver : BroadcastReceiver(), KoinComponent { - private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() - - override fun onReceive(context: Context, intent: Intent) { - val intentAction = intent.action - - // If media button are turned off and we received a media button, exit - if (!Settings.mediaButtonsEnabled && Intent.ACTION_MEDIA_BUTTON == intentAction) return - - // Only process media buttons and CMD_PROCESS_KEYCODE, which is received from the widgets - if (Intent.ACTION_MEDIA_BUTTON != intentAction && - Constants.CMD_PROCESS_KEYCODE != intentAction - ) return - val extras = intent.extras ?: return - - val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - extras.getParcelable(Intent.EXTRA_KEY_EVENT, Parcelable::class.java) - } else { - @Suppress("DEPRECATION") - extras.get(Intent.EXTRA_KEY_EVENT) as Parcelable? - } - - Timber.i("Got MEDIA_BUTTON key event: %s", event) - try { - val serviceIntent = Intent(Constants.CMD_PROCESS_KEYCODE) - serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event) - lifecycleSupport.receiveIntent(serviceIntent) - if (isOrderedBroadcast) { - abortBroadcast() - } - } catch (ignored: Exception) { - // Ignored. - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.kt index 9f75fdfd..86929b26 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.kt @@ -10,20 +10,19 @@ package org.moire.ultrasonic.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import org.koin.java.KoinJavaComponent.inject +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import timber.log.Timber -class UltrasonicIntentReceiver : BroadcastReceiver() { - private val lifecycleSupport = inject( - MediaPlayerLifecycleSupport::class.java - ) +class UltrasonicIntentReceiver : BroadcastReceiver(), KoinComponent { + private val lifecycleSupport by inject() override fun onReceive(context: Context, intent: Intent) { val intentAction = intent.action Timber.i("Received Ultrasonic Intent: %s", intentAction) try { - lifecycleSupport.value.receiveIntent(intent) + lifecycleSupport.receiveIntent(intent) if (isOrderedBroadcast) { abortBroadcast() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index d6312be7..017c0151 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -315,6 +315,10 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, return musicService.getStreamUrl(id, maxBitRate, format) } + override fun isJukeboxAvailable(): Boolean { + return musicService.isJukeboxAvailable() + } + @Throws(Exception::class) override fun updateJukeboxPlaylist(ids: List?): JukeboxStatus { return musicService.updateJukeboxPlaylist(ids) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index dff3d5b3..8eeb60ba 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -29,8 +29,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import org.koin.java.KoinJavaComponent.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.domain.Track @@ -61,6 +63,7 @@ private const val CHECK_INTERVAL = 5000L class DownloadService : Service(), KoinComponent { private var scope: CoroutineScope? = null private val storageMonitor: ExternalStorageMonitor by inject() + private val cacheCleaner: CacheCleaner by inject() private val binder: IBinder = SimpleServiceBinder(this) private var isInForeground = false @@ -153,7 +156,7 @@ class DownloadService : Service(), KoinComponent { // Stop Executor service when done downloading if (activeDownloads.isEmpty()) { - CacheCleaner().cleanSpace() + cacheCleaner.cleanSpace() stopSelf() } @@ -279,7 +282,17 @@ class DownloadService : Service(), KoinComponent { updateSaveFlag: Boolean = false ) { CoroutineScope(Dispatchers.IO).launch { + downloadAsync(tracks, save, isHighPriority, updateSaveFlag) + } + } + suspend fun downloadAsync( + tracks: List, + save: Boolean = false, + isHighPriority: Boolean = false, + updateSaveFlag: Boolean = false + ) { + withContext(Dispatchers.IO) { // Remove tracks which are already downloaded and update the save flag // if needed var filteredTracks = if (updateSaveFlag) { @@ -384,8 +397,14 @@ class DownloadService : Service(), KoinComponent { failedList.clear() } - fun delete(track: Track) { + private fun delete(track: Track) { CoroutineScope(Dispatchers.IO).launch { + deleteAsync(track) + } + } + + private suspend fun deleteAsync(track: Track) { + withContext(Dispatchers.IO) { downloadQueue.get(track.id)?.let { downloadQueue.remove(it) } failedList[track.id]?.let { downloadQueue.remove(it) } cancelDownload(track) @@ -394,7 +413,8 @@ class DownloadService : Service(), KoinComponent { Storage.delete(track.getCompleteFile()) Storage.delete(track.getPinnedFile()) postState(track, DownloadState.IDLE) - CacheCleaner().cleanDatabaseSelective(track) + val cacheCleaner: CacheCleaner by inject(CacheCleaner::class.java) + cacheCleaner.cleanDatabaseSelective(track) Util.scanMedia(track.getPinnedFile()) } } @@ -409,22 +429,38 @@ class DownloadService : Service(), KoinComponent { tracks.forEach(::delete) } - fun unpin(track: Track) { - // Update Pinned flag of items in progress - downloadQueue.get(track.id)?.pinned = false - activeDownloads[track.id]?.downloadTrack?.pinned = false - failedList[track.id]?.pinned = false + suspend fun unpinAsync(tracks: List) { + tracks.forEach { unpinAsync(it) } + } - val pinnedFile = track.getPinnedFile() - if (!Storage.isPathExists(pinnedFile)) return - val file = Storage.getFromPath(track.getPinnedFile()) ?: return - try { - Storage.rename(file, track.getCompleteFile()) - } catch (ignored: FileAlreadyExistsException) { - // Play console has revealed a crash when for some reason both files exist - Storage.delete(file.path) + suspend fun deleteAsync(tracks: List) { + tracks.forEach { deleteAsync(it) } + } + + private fun unpin(track: Track) { + CoroutineScope(Dispatchers.IO).launch { + unpinAsync(track) + } + } + + private suspend fun unpinAsync(track: Track) { + withContext(Dispatchers.IO) { + // Update Pinned flag of items in progress + downloadQueue.get(track.id)?.pinned = false + activeDownloads[track.id]?.downloadTrack?.pinned = false + failedList[track.id]?.pinned = false + + val pinnedFile = track.getPinnedFile() + if (!Storage.isPathExists(pinnedFile)) return@withContext + val file = Storage.getFromPath(track.getPinnedFile()) ?: return@withContext + try { + Storage.rename(file, track.getCompleteFile()) + } catch (ignored: FileAlreadyExistsException) { + // Play console has revealed a crash when for some reason both files exist + Storage.delete(file.path) + } + postState(track, DownloadState.DONE) } - postState(track, DownloadState.DONE) } @Suppress("ReturnCount") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt index e36975b8..483220b2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt @@ -256,7 +256,7 @@ class DownloadTask( // Download the largest size that we can display in the UI imageLoaderProvider.executeOn { imageLoader -> - imageLoader.cacheCoverArt(this) + imageLoader.downloadCoverArt(this) // Cache small copies of the Artist picture directArtist?.let { imageLoader.cacheArtistPicture(it) } compilationArtist?.let { imageLoader.cacheArtistPicture(it) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaLibrarySessionCallback.kt similarity index 97% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaLibrarySessionCallback.kt index ee203b41..e2525785 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaLibrarySessionCallback.kt @@ -1,13 +1,12 @@ /* - * AutoMediaBrowserCallback.kt - * Copyright (C) 2009-2022 Ultrasonic developers + * MediaLibrarySessionCallback.kt + * Copyright (C) 2009-2023 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ -package org.moire.ultrasonic.playback +package org.moire.ultrasonic.service -import android.content.Context import android.os.Build import android.os.Bundle import androidx.car.app.connection.CarConnection @@ -32,11 +31,14 @@ import androidx.media3.session.SessionResult.RESULT_SUCCESS import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.SettableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.guava.await import kotlinx.coroutines.guava.future +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.R @@ -48,8 +50,6 @@ import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.service.MusicServiceFactory -import org.moire.ultrasonic.service.RatingManager import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.buildMediaItem import org.moire.ultrasonic.util.toMediaItem @@ -98,10 +98,12 @@ const val PLAY_COMMAND = "play " * MediaBrowserService implementation for e.g. Android Auto */ @Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember") -class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callback, KoinComponent { +class MediaLibrarySessionCallback : + MediaLibraryService.MediaLibrarySession.Callback, + KoinComponent { - private val applicationContext: Context by inject() private val activeServerProvider: ActiveServerProvider by inject() + private val playbackStateSerializer: PlaybackStateSerializer by inject() private val serviceJob = SupervisorJob() private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) @@ -243,6 +245,25 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac ) } + @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) + override fun onPlaybackResumption( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo + ): ListenableFuture { + val result = SettableFuture.create() + serviceScope.launch { + val state = playbackStateSerializer.deserializeNow() + if (state != null) { + result.set(state.toMediaItemsWithStartPosition()) + withContext(Dispatchers.Main) { + mediaSession.player.shuffleModeEnabled = state.shufflePlay + mediaSession.player.repeatMode = state.repeatMode + } + } + } + return result + } + private fun configureRepeatMode(player: Player) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Timber.d("Car app library available, observing CarConnection") @@ -251,7 +272,7 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac var lastCarConnectionType = -1 - CarConnection(applicationContext).type.observeForever { + CarConnection(UApp.applicationContext()).type.observeForever { if (lastCarConnectionType == it) return@observeForever diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 8c64d9a0..939c51ee 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -16,8 +16,6 @@ import android.os.Build import android.view.KeyEvent import io.reactivex.rxjava3.disposables.CompositeDisposable import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp.Companion.applicationContext import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.CacheCleaner @@ -29,11 +27,13 @@ import timber.log.Timber /** * This class is responsible for handling received events for the Media Player implementation */ -class MediaPlayerLifecycleSupport : KoinComponent { +class MediaPlayerLifecycleSupport( + val mediaPlayerManager: MediaPlayerManager, + private val playbackStateSerializer: PlaybackStateSerializer, + val imageLoaderProvider: ImageLoaderProvider, + private val cacheCleaner: CacheCleaner +) : KoinComponent { private lateinit var ratingManager: RatingManager - private val playbackStateSerializer by inject() - private val mediaPlayerManager by inject() - private val imageLoaderProvider: ImageLoaderProvider by inject() private var created = false private var headsetEventReceiver: BroadcastReceiver? = null @@ -70,7 +70,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { registerHeadsetReceiver() - CacheCleaner().clean() + cacheCleaner.clean() created = true ratingManager = RatingManager.instance Timber.i("LifecycleSupport created") @@ -78,15 +78,10 @@ class MediaPlayerLifecycleSupport : KoinComponent { private fun restoreLastSession(autoPlay: Boolean, afterRestore: Runnable?) { playbackStateSerializer.deserialize { + if (it == null) return@deserialize null + Timber.i("Restoring %s songs", it.songs.size) - Timber.i("Restoring %s songs", it!!.songs.size) - - mediaPlayerManager.restore( - it, - autoPlay, - false - ) - + mediaPlayerManager.restore(it, autoPlay) afterRestore?.run() } } @@ -99,7 +94,6 @@ class MediaPlayerLifecycleSupport : KoinComponent { applicationContext().unregisterReceiver(headsetEventReceiver) imageLoaderProvider.clearImageLoader() - UApp.instance!!.shutdownKoin() created = false Timber.i("LifecycleSupport destroyed") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt index d07b4c52..17eedd37 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt @@ -7,10 +7,11 @@ package org.moire.ultrasonic.service import android.content.ComponentName -import android.content.Context import android.os.Handler import android.os.Looper import androidx.annotation.IntRange +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.C import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem @@ -29,17 +30,20 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent -import org.koin.core.component.inject +import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp -import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider.Companion.OFFLINE_DB_ID import org.moire.ultrasonic.data.RatingUpdate import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.playback.PlaybackService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.util.DownloadUtil import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.util.Util.navigateToCurrent +import org.moire.ultrasonic.util.Util.toast +import org.moire.ultrasonic.util.launchWithToast import org.moire.ultrasonic.util.toMediaItem import org.moire.ultrasonic.util.toTrack import timber.log.Timber @@ -56,12 +60,9 @@ private const val VOLUME_DELTA = 0.05f @Suppress("TooManyFunctions") class MediaPlayerManager( private val playbackStateSerializer: PlaybackStateSerializer, - private val externalStorageMonitor: ExternalStorageMonitor, - val context: Context + private val externalStorageMonitor: ExternalStorageMonitor ) : KoinComponent { - private val activeServerProvider: ActiveServerProvider by inject() - private var created = false var suggestedPlaylistName: String? = null var keepScreenOn = false @@ -73,8 +74,10 @@ class MediaPlayerManager( private var mainScope = CoroutineScope(Dispatchers.Main) - private var sessionToken = - SessionToken(context, ComponentName(context, PlaybackService::class.java)) + private var sessionToken = SessionToken( + UApp.applicationContext(), + ComponentName(UApp.applicationContext(), PlaybackService::class.java) + ) private var mediaControllerFuture: ListenableFuture? = null @@ -145,12 +148,11 @@ class MediaPlayerManager( Timber.w(error.toString()) if (!isJukeboxEnabled) return - val context = UApp.applicationContext() mainScope.launch { - Util.toast( - context, + toast( error.errorCode, - false + false, + UApp.applicationContext() ) } isJukeboxEnabled = false @@ -199,7 +201,7 @@ class MediaPlayerManager( } rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { - val jukebox = activeServerProvider.getActiveServer().jukeboxByDefault + val jukebox = it.jukeboxByDefault // Remove all songs when changing servers before turning on Jukebox. // Jukebox wouldn't find the songs on the new server. if (jukebox) controller?.clearMediaItems() @@ -246,10 +248,10 @@ class MediaPlayerManager( private fun createMediaController(onCreated: () -> Unit) { mediaControllerFuture = MediaController.Builder( - context, + UApp.applicationContext(), sessionToken ) - // Specify mainThread explicitely + // Specify mainThread explicitly .setApplicationLooper(Looper.getMainLooper()) .buildAsync() @@ -320,17 +322,15 @@ class MediaPlayerManager( externalStorageMonitor.onDestroy() DownloadService.requestStop() created = false - Timber.i("MediaPlayerController destroyed") + Timber.i("MediaPlayerManager destroyed") } @Synchronized fun restore( state: PlaybackState, - autoPlay: Boolean, - newPlaylist: Boolean + autoPlay: Boolean ) { - val insertionMode = if (newPlaylist) InsertionMode.CLEAR - else InsertionMode.APPEND + val insertionMode = InsertionMode.APPEND addToPlaylist( state.songs, @@ -474,6 +474,80 @@ class MediaPlayerManager( } } + private suspend fun addToPlaylistAsync( + songs: List, + autoPlay: Boolean, + shuffle: Boolean, + insertionMode: InsertionMode + ) { + withContext(Dispatchers.Main) { + addToPlaylist( + songs = songs, + autoPlay = autoPlay, + shuffle = shuffle, + insertionMode = insertionMode + ) + } + } + + @Suppress("LongParameterList") + fun playTracksAndToast( + fragment: Fragment, + insertionMode: InsertionMode, + tracks: List = listOf(), + id: String? = null, + name: String? = "", + isShare: Boolean = false, + isDirectory: Boolean = true, + shuffle: Boolean = false, + isArtist: Boolean = false + ) { + val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope + + scope.launchWithToast { + + val list: List = + tracks.ifEmpty { + requireNotNull(id) + DownloadUtil.getTracksFromServerAsync(isArtist, id, isDirectory, name, isShare) + } + + addToPlaylistAsync( + songs = list, + insertionMode = insertionMode, + autoPlay = (insertionMode == InsertionMode.CLEAR), + shuffle = shuffle, + ) + + if (insertionMode == InsertionMode.CLEAR) { + fragment.navigateToCurrent() + } + + when (insertionMode) { + InsertionMode.AFTER_CURRENT -> + quantize(R.plurals.n_songs_added_after_current, list) + + InsertionMode.APPEND -> + quantize(R.plurals.n_songs_added_to_end, list) + + InsertionMode.CLEAR -> { + if (Settings.shouldTransitionOnPlayback) + null + else + quantize(R.plurals.n_songs_added_play_now, list) + } + } + } + } + + private fun quantize(resId: Int, tracks: List): String { + return UApp.applicationContext().resources.getQuantityString( + resId, + tracks.size, + tracks.size + ) + } + @set:Synchronized var isShufflePlayEnabled: Boolean get() = controller?.shuffleModeEnabled == true @@ -649,21 +723,6 @@ class MediaPlayerManager( Timber.i("MediaPlayerController released") } - /** - * This function calls the music service directly and - * therefore can't be called from the main thread - */ - val isJukeboxAvailable: Boolean - get() { - try { - val username = activeServerProvider.getActiveServer().userName - return getMusicService().getUser(username).jukeboxRole - } catch (all: Exception) { - Timber.w(all, "Error getting user information") - } - return false - } - fun adjustVolume(up: Boolean) { val delta = if (up) VOLUME_DELTA else -VOLUME_DELTA var gain = controller?.volume ?: return @@ -676,7 +735,7 @@ class MediaPlayerManager( /* * Sets the rating of the current track */ - fun setRating(rating: Rating) { + private fun setRating(rating: Rating) { if (controller is MediaController) { (controller as MediaController).setRating(rating) } @@ -724,7 +783,7 @@ class MediaPlayerManager( val currentMediaItemIndex: Int get() = controller?.currentMediaItemIndex ?: -1 - fun getCurrentShuffleIndex(): Int { + private fun getCurrentShuffleIndex(): Int { val currentMediaItemIndex = controller?.currentMediaItemIndex ?: return -1 return getShuffledIndexOf(currentMediaItemIndex) } @@ -768,9 +827,8 @@ class MediaPlayerManager( * in the shuffled timeline. * @return The index of the item in the shuffled timeline, or [C.INDEX_UNSET] if not found. */ - fun getShuffledIndexOf(searchPosition: Int): Int { - return getWindowIndexWhere(false) { - _, windowIndex -> + private fun getShuffledIndexOf(searchPosition: Int): Int { + return getWindowIndexWhere(false) { _, windowIndex -> windowIndex == searchPosition } } @@ -784,8 +842,7 @@ class MediaPlayerManager( * @return the index of the item in the unshuffled timeline, or [C.INDEX_UNSET] if not found. */ fun getUnshuffledIndexOf(shufflePosition: Int): Int { - return getWindowIndexWhere(true) { - count, _ -> + return getWindowIndexWhere(true) { count, _ -> count == shufflePosition } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index 2d37a648..e80ea8ac 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -138,6 +138,8 @@ interface MusicService { @Throws(Exception::class) fun getStreamUrl(id: String, maxBitRate: Int?, format: String?): String? + fun isJukeboxAvailable(): Boolean + @Throws(Exception::class) fun updateJukeboxPlaylist(ids: List?): JukeboxStatus diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index 039e1de1..d9bd3100 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -335,6 +335,10 @@ class OfflineMusicService : MusicService, KoinComponent { } } + override fun isJukeboxAvailable(): Boolean { + return false + } + @Throws(Exception::class) override fun updateJukeboxPlaylist(ids: List?): JukeboxStatus { throw OfflineException("Jukebox not available in offline mode") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackService.kt similarity index 97% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackService.kt index 9ec36400..34f6acc5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackService.kt @@ -1,10 +1,10 @@ /* * PlaybackService.kt - * Copyright (C) 2009-2022 Ultrasonic developers + * Copyright (C) 2009-2023 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ -package org.moire.ultrasonic.playback +package org.moire.ultrasonic.service import android.annotation.SuppressLint import android.app.PendingIntent @@ -42,15 +42,11 @@ import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.CachedDataSource import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider -import org.moire.ultrasonic.service.DownloadService -import org.moire.ultrasonic.service.JukeboxMediaPlayer -import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.service.RxBus -import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util @@ -68,7 +64,7 @@ class PlaybackService : private var equalizer: EqualizerController? = null private val activeServerProvider: ActiveServerProvider by inject() - private lateinit var librarySessionCallback: AutoMediaBrowserCallback + private lateinit var librarySessionCallback: MediaLibrarySessionCallback private var rxBusSubscription = CompositeDisposable() @@ -115,6 +111,7 @@ class PlaybackService : isStarted = false stopForegroundRemoveNotification() stopSelf() + instance = null } private val resolver: ResolvingDataSource.Resolver = ResolvingDataSource.Resolver { @@ -142,7 +139,7 @@ class PlaybackService : actualBackend = desiredBackend // Create browser interface - librarySessionCallback = AutoMediaBrowserCallback() + librarySessionCallback = MediaLibrarySessionCallback() // This will need to use the AutoCalls mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackState.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackState.kt index d57a5b86..3d81d21b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackState.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackState.kt @@ -1,7 +1,9 @@ package org.moire.ultrasonic.service +import androidx.media3.session.MediaSession import java.io.Serializable import org.moire.ultrasonic.domain.Track +import org.moire.ultrasonic.util.toMediaItem /** * Represents the state of the Media Player implementation @@ -17,3 +19,12 @@ data class PlaybackState( private const val serialVersionUID = -293487987L } } + +@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +fun PlaybackState.toMediaItemsWithStartPosition(): MediaSession.MediaItemsWithStartPosition { + return MediaSession.MediaItemsWithStartPosition( + songs.map { it.toMediaItem() }, + currentPlayingIndex, + currentPlayingPosition.toLong(), + ) +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt index bad9dc2b..c1a7fa85 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt @@ -24,6 +24,8 @@ import timber.log.Timber * This class is responsible for the serialization / deserialization * of the playlist and the player state (e.g. current playing number and play position) * to the filesystem. + * + * TODO: Should use: MediaItemsWithStartPosition */ class PlaybackStateSerializer : KoinComponent { @@ -56,7 +58,7 @@ class PlaybackStateSerializer : KoinComponent { } } - fun serializeNow( + private fun serializeNow( tracks: Iterable, currentPlayingIndex: Int, currentPlayingPosition: Int, @@ -85,7 +87,10 @@ class PlaybackStateSerializer : KoinComponent { if (isDeserializing.get()) return ioScope.launch { try { - deserializeNow(afterDeserialized) + val state = deserializeNow() + mainScope.launch { + afterDeserialized(state) + } isSetup.set(true) } catch (all: Exception) { Timber.e(all, "Had a problem deserializing:") @@ -95,11 +100,11 @@ class PlaybackStateSerializer : KoinComponent { } } - private fun deserializeNow(afterDeserialized: (PlaybackState?) -> Unit?) { + fun deserializeNow(): PlaybackState? { val state = FileUtil.deserialize( context, Constants.FILENAME_PLAYLIST_SER - ) ?: return + ) ?: return null Timber.i( "Deserialized currentPlayingIndex: %d, currentPlayingPosition: %d, shuffle: %b", @@ -108,9 +113,7 @@ class PlaybackStateSerializer : KoinComponent { state.shufflePlay ) - mainScope.launch { - afterDeserialized(state) - } + return state } companion object { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index a31ea7c2..f6071cb6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -51,7 +51,7 @@ import timber.log.Timber */ @Suppress("LargeClass") open class RESTMusicService( - val subsonicAPIClient: SubsonicAPIClient, + private val subsonicAPIClient: SubsonicAPIClient, private val activeServerProvider: ActiveServerProvider ) : MusicService { @@ -504,6 +504,11 @@ open class RESTMusicService( builder.build() } + override fun isJukeboxAvailable(): Boolean { + val username = activeServerProvider.getActiveServer().userName + return getUser(username).jukeboxRole + } + @Throws(Exception::class) override fun updateJukeboxPlaylist( ids: List? diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 8524fdff..ceadd14e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -8,6 +8,7 @@ import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.subjects.PublishSubject import java.util.concurrent.TimeUnit import org.moire.ultrasonic.data.RatingUpdate +import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.domain.Track class RxBus { @@ -32,9 +33,9 @@ class RxBus { var activeServerChangingObservable: Observable = activeServerChangingPublisher - var activeServerChangedPublisher: PublishSubject = + var activeServerChangedPublisher: PublishSubject = PublishSubject.create() - var activeServerChangedObservable: Observable = + var activeServerChangedObservable: Observable = activeServerChangedPublisher.observeOn(mainThread()) val themeChangedEventPublisher: PublishSubject = diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt deleted file mode 100644 index a0b4c156..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ /dev/null @@ -1,260 +0,0 @@ -/* - * DownloadHandler.kt - * Copyright (C) 2009-2022 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ - -package org.moire.ultrasonic.subsonic - -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import java.util.LinkedList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.moire.ultrasonic.R -import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags -import org.moire.ultrasonic.domain.MusicDirectory -import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.service.DownloadService -import org.moire.ultrasonic.service.MediaPlayerManager -import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.executeTaskWithToast - -/** - * Retrieves a list of songs and adds them to the now playing list - */ -@Suppress("LongParameterList") -class DownloadHandler( - val mediaPlayerManager: MediaPlayerManager, - private val networkAndStorageChecker: NetworkAndStorageChecker -) : CoroutineScope by CoroutineScope(Dispatchers.IO) { - private val maxSongs = 500 - - fun justDownload( - action: DownloadAction, - fragment: Fragment, - id: String? = null, - name: String? = "", - isShare: Boolean = false, - isDirectory: Boolean = true, - isArtist: Boolean = false, - tracks: List? = null - ) { - var successString: String? = null - - // Launch the Job - executeTaskWithToast({ - val tracksToDownload: List = tracks - ?: getTracksFromServer(isArtist, id!!, isDirectory, name, isShare) - - withContext(Dispatchers.Main) { - // If we are just downloading tracks we don't need to add them to the controller - when (action) { - DownloadAction.DOWNLOAD -> DownloadService.download( - tracksToDownload, - save = false, - updateSaveFlag = true - ) - DownloadAction.PIN -> DownloadService.download( - tracksToDownload, - save = true, - updateSaveFlag = true - ) - DownloadAction.UNPIN -> DownloadService.unpin(tracksToDownload) - DownloadAction.DELETE -> DownloadService.delete(tracksToDownload) - } - successString = when (action) { - DownloadAction.DOWNLOAD -> fragment.resources.getQuantityString( - R.plurals.n_songs_to_be_downloaded, - tracksToDownload.size, - tracksToDownload.size - ) - DownloadAction.UNPIN -> { - fragment.resources.getQuantityString( - R.plurals.n_songs_unpinned, - tracksToDownload.size, - tracksToDownload.size - ) - } - DownloadAction.PIN -> { - fragment.resources.getQuantityString( - R.plurals.n_songs_pinned, - tracksToDownload.size, - tracksToDownload.size - ) - } - DownloadAction.DELETE -> { - fragment.resources.getQuantityString( - R.plurals.n_songs_deleted, - tracksToDownload.size, - tracksToDownload.size - ) - } - } - } - }) { successString } - } - - fun fetchTracksAndAddToController( - fragment: Fragment, - id: String, - name: String? = "", - isShare: Boolean = false, - isDirectory: Boolean = true, - insertionMode: MediaPlayerManager.InsertionMode, - autoPlay: Boolean, - shuffle: Boolean = false, - isArtist: Boolean = false - ) { - var successString: String? = null - // Launch the Job - executeTaskWithToast({ - val songs: MutableList = - getTracksFromServer(isArtist, id, isDirectory, name, isShare) - - withContext(Dispatchers.Main) { - addTracksToMediaController( - songs = songs, - insertionMode = insertionMode, - autoPlay = autoPlay, - shuffle = shuffle, - playlistName = null, - fragment = fragment - ) - - // Play Now doesn't get a Toast :) - successString = when (insertionMode) { - MediaPlayerManager.InsertionMode.AFTER_CURRENT -> - fragment.resources.getQuantityString( - R.plurals.n_songs_added_after_current, - songs.size, - songs.size - ) - MediaPlayerManager.InsertionMode.APPEND -> - fragment.resources.getQuantityString( - R.plurals.n_songs_added_to_end, - songs.size, - songs.size - ) - else -> null - } - } - }) { successString } - } - - fun addTracksToMediaController( - songs: List, - insertionMode: MediaPlayerManager.InsertionMode, - autoPlay: Boolean, - shuffle: Boolean = false, - playlistName: String? = null, - fragment: Fragment - ) { - if (songs.isEmpty()) return - - networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - - if (playlistName != null) { - mediaPlayerManager.suggestedPlaylistName = playlistName - } - - mediaPlayerManager.addToPlaylist( - songs, - autoPlay, - shuffle, - insertionMode - ) - - if (Settings.shouldTransitionOnPlayback && - insertionMode == MediaPlayerManager.InsertionMode.CLEAR - ) { - fragment.findNavController().popBackStack(R.id.playerFragment, true) - fragment.findNavController().navigate(R.id.playerFragment) - } - } - - private fun getTracksFromServer( - isArtist: Boolean, - id: String, - isDirectory: Boolean, - name: String?, - isShare: Boolean - ): MutableList { - val musicService = getMusicService() - val songs: MutableList = LinkedList() - val root: MusicDirectory - if (shouldUseId3Tags() && isArtist) { - return getSongsForArtist(id) - } else { - if (isDirectory) { - root = if (shouldUseId3Tags()) - musicService.getAlbumAsDir(id, name, false) - else - musicService.getMusicDirectory(id, name, false) - } else if (isShare) { - root = MusicDirectory() - val shares = musicService.getShares(true) - // Filter the received shares by the given id, and get their entries - val entries = shares.filter { it.id == id }.flatMap { it.getEntries() } - root.addAll(entries) - } else { - root = musicService.getPlaylist(id, name!!) - } - getSongsRecursively(root, songs) - } - return songs - } - - @Suppress("DestructuringDeclarationWithTooManyEntries") - @Throws(Exception::class) - private fun getSongsRecursively( - parent: MusicDirectory, - songs: MutableList - ) { - if (songs.size > maxSongs) { - return - } - for (song in parent.getTracks()) { - if (!song.isVideo) { - songs.add(song) - } - } - val musicService = getMusicService() - for ((id1, _, _, title) in parent.getAlbums()) { - val root: MusicDirectory = if (shouldUseId3Tags()) - musicService.getAlbumAsDir(id1, title, false) - else - musicService.getMusicDirectory(id1, title, false) - getSongsRecursively(root, songs) - } - } - - @Throws(Exception::class) - private fun getSongsForArtist( - id: String - ): MutableList { - val songs: MutableList = LinkedList() - val musicService = getMusicService() - val artist = musicService.getAlbumsOfArtist(id, "", false) - for ((id1) in artist) { - val albumDirectory = musicService.getAlbumAsDir( - id1, - "", - false - ) - for (song in albumDirectory.getTracks()) { - if (!song.isVideo) { - songs.add(song) - } - } - } - return songs - } -} - -enum class DownloadAction { - DOWNLOAD, PIN, UNPIN, DELETE -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt index 95a887a9..daf0eb98 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt @@ -1,6 +1,5 @@ package org.moire.ultrasonic.subsonic -import android.content.Context import androidx.core.content.res.ResourcesCompat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -20,8 +19,7 @@ import timber.log.Timber /** * Handles the lifetime of the Image Loader */ -class -ImageLoaderProvider(val context: Context) : +class ImageLoaderProvider : KoinComponent, CoroutineScope by CoroutineScope(Dispatchers.IO) { private var imageLoader: ImageLoader? = null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/NetworkAndStorageChecker.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/NetworkAndStorageChecker.kt index 22e73d33..016f4921 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/NetworkAndStorageChecker.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/NetworkAndStorageChecker.kt @@ -1,19 +1,19 @@ package org.moire.ultrasonic.subsonic -import android.content.Context import org.moire.ultrasonic.R +import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.util.Util /** * Utility class for checking the availability of the network and storage */ -class NetworkAndStorageChecker(val context: Context) { +class NetworkAndStorageChecker { fun warnIfNetworkOrStorageUnavailable() { if (!Util.isExternalStoragePresent()) { - Util.toast(context, R.string.select_album_no_sdcard) + Util.toast(R.string.select_album_no_sdcard, true, UApp.applicationContext()) } else if (!isOffline() && !Util.hasUsableNetwork()) { - Util.toast(context, R.string.select_album_no_network) + Util.toast(R.string.select_album_no_network, true, UApp.applicationContext()) } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt index c9855d89..2dd7f18f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt @@ -8,7 +8,6 @@ package org.moire.ultrasonic.subsonic import android.annotation.SuppressLint -import android.content.Context import android.content.Intent import android.view.LayoutInflater import android.view.View @@ -17,27 +16,27 @@ import android.widget.EditText import android.widget.TextView import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.lifecycle.lifecycleScope import java.util.Locale import java.util.regex.Pattern -import kotlin.collections.ArrayList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.BackgroundTask -import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.ConfirmationDialog -import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.ShareDetails import org.moire.ultrasonic.util.TimeSpanPicker +import org.moire.ultrasonic.util.Util.getString import org.moire.ultrasonic.util.Util.ifNotNull /** * This class handles sharing items in the media library */ -class ShareHandler(val context: Context) { +class ShareHandler { private var shareDescription: EditText? = null private var timeSpanPicker: TimeSpanPicker? = null private var shareOnServerCheckBox: CheckBox? = null @@ -51,21 +50,26 @@ class ShareHandler(val context: Context) { fun share( fragment: Fragment, shareDetails: ShareDetails, - swipe: SwipeRefreshLayout?, - cancellationToken: CancellationToken, additionalId: String? ) { - val task: BackgroundTask = object : FragmentBackgroundTask( - fragment.requireActivity(), - true, - swipe, - cancellationToken - ) { - @Throws(Throwable::class) - override fun doInBackground(): Share? { + val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope + scope.launch { + val share = createShareOnServer(shareDetails, additionalId) + startActivityForShare(share, shareDetails, fragment) + } + } + + private suspend fun createShareOnServer( + shareDetails: ShareDetails, + additionalId: String? + ): Share? { + return withContext(Dispatchers.IO) { + return@withContext try { + val ids: MutableList = ArrayList() - if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) return null + if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) + return@withContext null if (shareDetails.Entries.isEmpty()) { additionalId.ifNotNull { ids.add(it) @@ -86,78 +90,80 @@ class ShareHandler(val context: Context) { val shares = musicService.createShare(ids, shareDetails.Description, timeInMillis) - return shares[0] - } - - override fun done(result: Share?) { - - val intent = Intent(Intent.ACTION_SEND) - intent.type = "text/plain" - - if (result != null) { - // Created a share, send the URL - intent.putExtra( - Intent.EXTRA_TEXT, - String.format( - Locale.ROOT, "%s\n\n%s", Settings.shareGreeting, result.url - ) - ) - } else { - // Sending only text details - val textBuilder = StringBuilder() - textBuilder.appendLine(Settings.shareGreeting) - - if (!shareDetails.Entries[0].title.isNullOrEmpty()) - textBuilder.append(context.resources.getString(R.string.common_title)) - .append(": ").appendLine(shareDetails.Entries[0].title) - if (!shareDetails.Entries[0].artist.isNullOrEmpty()) - textBuilder.append(context.resources.getString(R.string.common_artist)) - .append(": ").appendLine(shareDetails.Entries[0].artist) - if (!shareDetails.Entries[0].album.isNullOrEmpty()) - textBuilder.append(context.resources.getString(R.string.common_album)) - .append(": ").append(shareDetails.Entries[0].album) - - intent.putExtra(Intent.EXTRA_TEXT, textBuilder.toString()) - } - - fragment.activity?.startActivity( - Intent.createChooser( - intent, - context.resources.getString(R.string.share_via) - ) - ) + // Return the share + shares[0] + } catch (ignored: Exception) { + null } } - task.execute() + } + + private suspend fun startActivityForShare( + result: Share?, + shareDetails: ShareDetails, + fragment: Fragment + ) { + return withContext(Dispatchers.Main) { + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + + if (result != null) { + // Created a share, send the URL + intent.putExtra( + Intent.EXTRA_TEXT, + String.format( + Locale.ROOT, "%s\n\n%s", Settings.shareGreeting, result.url + ) + ) + } else { + // Sending only text details + val textBuilder = StringBuilder() + textBuilder.appendLine(Settings.shareGreeting) + + if (!shareDetails.Entries[0].title.isNullOrEmpty()) + textBuilder.append(getString(R.string.common_title)) + .append(": ").appendLine(shareDetails.Entries[0].title) + if (!shareDetails.Entries[0].artist.isNullOrEmpty()) + textBuilder.append(getString(R.string.common_artist)) + .append(": ").appendLine(shareDetails.Entries[0].artist) + if (!shareDetails.Entries[0].album.isNullOrEmpty()) + textBuilder.append(getString(R.string.common_album)) + .append(": ").append(shareDetails.Entries[0].album) + + intent.putExtra(Intent.EXTRA_TEXT, textBuilder.toString()) + } + + fragment.activity?.startActivity( + Intent.createChooser( + intent, + getString(R.string.share_via) + ) + ) + } } fun createShare( fragment: Fragment, tracks: List?, - swipe: SwipeRefreshLayout?, - cancellationToken: CancellationToken, additionalId: String? = null ) { val askForDetails = Settings.shouldAskForShareDetails val shareDetails = ShareDetails() shareDetails.Entries = tracks if (askForDetails) { - showDialog(fragment, shareDetails, swipe, cancellationToken, additionalId) + showDialog(fragment, shareDetails, additionalId) } else { shareDetails.Description = Settings.defaultShareDescription shareDetails.Expiration = System.currentTimeMillis() + Settings.defaultShareExpirationInMillis - share(fragment, shareDetails, swipe, cancellationToken, additionalId) + share(fragment, shareDetails, additionalId) } } - @Suppress("LongMethod") @SuppressLint("InflateParams") private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, - swipe: SwipeRefreshLayout?, - cancellationToken: CancellationToken, additionalId: String? ) { val layout = LayoutInflater.from(fragment.context).inflate(R.layout.share_details, null) @@ -175,18 +181,57 @@ class ShareHandler(val context: Context) { textViewExpiration = layout.findViewById(R.id.textViewExpiration) as TextView } + // Handle the visibility based on shareDetails.Entries size if (shareDetails.Entries.size == 1) { - // For single songs the sharing may be done by text only shareOnServerCheckBox?.setOnCheckedChangeListener { _, _ -> updateVisibility() } - shareOnServerCheckBox?.isChecked = Settings.shareOnServer } else { shareOnServerCheckBox?.isVisible = false } + updateVisibility() + // Set up the dialog builder + val builder = makeDialogBuilder(fragment, shareDetails, additionalId, layout) + + // Initialize UI components with default values + setupDefaultValues() + + builder.create() + builder.show() + } + + private fun setupDefaultValues() { + val defaultDescription = Settings.defaultShareDescription + val timeSpan = Settings.defaultShareExpiration + val split = pattern.split(timeSpan) + if (split.size == 2) { + val timeSpanAmount = split[0].toInt() + val timeSpanType = split[1] + if (timeSpanAmount > 0) { + noExpirationCheckBox!!.isChecked = false + timeSpanPicker!!.isEnabled = true + timeSpanPicker!!.setTimeSpanAmount(timeSpanAmount.toString()) + timeSpanPicker!!.timeSpanType = timeSpanType + } else { + noExpirationCheckBox!!.isChecked = true + timeSpanPicker!!.isEnabled = false + } + } else { + noExpirationCheckBox!!.isChecked = true + timeSpanPicker!!.isEnabled = false + } + shareDescription!!.setText(defaultDescription) + } + + private fun makeDialogBuilder( + fragment: Fragment, + shareDetails: ShareDetails, + additionalId: String?, + layout: View? + ): ConfirmationDialog.Builder { val builder = ConfirmationDialog.Builder(fragment.requireContext()) builder.setTitle(R.string.share_set_share_options) @@ -214,7 +259,7 @@ class ShareHandler(val context: Context) { Settings.shareOnServer = shareDetails.ShareOnServer } - share(fragment, shareDetails, swipe, cancellationToken, additionalId) + share(fragment, shareDetails, additionalId) } builder.setNegativeButton(R.string.common_cancel) { dialog, _ -> @@ -224,35 +269,12 @@ class ShareHandler(val context: Context) { builder.setView(layout) builder.setCancelable(true) - timeSpanPicker!!.setTimeSpanDisableText(context.resources.getString(R.string.no_expiration)) + // Set up the timeSpanPicker + timeSpanPicker!!.setTimeSpanDisableText(getString(R.string.no_expiration)) noExpirationCheckBox!!.setOnCheckedChangeListener { _, b -> timeSpanPicker!!.isEnabled = !b } - - val defaultDescription = Settings.defaultShareDescription - val timeSpan = Settings.defaultShareExpiration - - val split = pattern.split(timeSpan) - if (split.size == 2) { - val timeSpanAmount = split[0].toInt() - val timeSpanType = split[1] - if (timeSpanAmount > 0) { - noExpirationCheckBox!!.isChecked = false - timeSpanPicker!!.isEnabled = true - timeSpanPicker!!.setTimeSpanAmount(timeSpanAmount.toString()) - timeSpanPicker!!.timeSpanType = timeSpanType - } else { - noExpirationCheckBox!!.isChecked = true - timeSpanPicker!!.isEnabled = false - } - } else { - noExpirationCheckBox!!.isChecked = true - timeSpanPicker!!.isEnabled = false - } - - shareDescription!!.setText(defaultDescription) - builder.create() - builder.show() + return builder } private fun updateVisibility() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt index a91fcea9..fa97f749 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt @@ -16,7 +16,7 @@ class VideoPlayer { companion object { fun playVideo(context: Context, track: Track?) { if (!Util.hasUsableNetwork() || track == null) { - Util.toast(context, R.string.select_album_no_network) + Util.toast(R.string.select_album_no_network, true, context) return } try { @@ -32,7 +32,7 @@ class VideoPlayer { ) context.startActivity(intent) } catch (all: Exception) { - Util.toast(context, all.toString(), false) + Util.toast(all.toString(), false, context) } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt index 83939ab8..6f460fe3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt @@ -11,7 +11,6 @@ import android.system.Os import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.guava.future import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -19,7 +18,7 @@ import org.koin.java.KoinJavaComponent.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Playlist import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.service.MediaPlayerManager +import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.util.FileUtil.getAlbumArtFile import org.moire.ultrasonic.util.FileUtil.getCompleteFile import org.moire.ultrasonic.util.FileUtil.getPartialFile @@ -38,7 +37,6 @@ import timber.log.Timber */ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinComponent { - private var mainScope = CoroutineScope(Dispatchers.Main) private val activeServerProvider by inject() private fun exceptionHandler(tag: String): CoroutineExceptionHandler { @@ -235,16 +233,14 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinCompo private fun findFilesToNotDelete(): Set { val filesToNotDelete: MutableSet = HashSet(5) - val mediaPlayerManager: MediaPlayerManager by inject() - val playlist = mainScope.future { mediaPlayerManager.playlist }.get() - for (item in playlist) { - val track = item.toTrack() + // We just take the last published playlist from RX + val playlist = RxBus.playlistObservable.blockingLast() + for (track in playlist) { filesToNotDelete.add(track.getPartialFile()) filesToNotDelete.add(track.getCompleteFile()) filesToNotDelete.add(track.getPinnedFile()) } - filesToNotDelete.add(musicDirectory.path) return filesToNotDelete } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index fceee797..051ca4f4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -37,4 +37,5 @@ object Constants { const val FILENAME_PLAYLIST_SER = "downloadstate.ser" const val ALBUM_ART_FILE = "folder.jpeg" const val RESULT_CLOSE_ALL = 1337 + const val MAX_SONGS_RECURSIVE = 500 } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt new file mode 100644 index 00000000..220ca6c1 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt @@ -0,0 +1,141 @@ +/* + * ContextMenuUtil.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.util + +import android.view.MenuItem +import androidx.fragment.app.Fragment +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.moire.ultrasonic.R +import org.moire.ultrasonic.domain.Identifiable +import org.moire.ultrasonic.domain.Track +import org.moire.ultrasonic.service.MediaPlayerManager +import org.moire.ultrasonic.subsonic.ShareHandler + +object ContextMenuUtil : KoinComponent { + + /* + * Callback for menu items of collections (albums, artists etc) + */ + fun handleContextMenu( + menuItem: MenuItem, + item: Identifiable, + isArtist: Boolean, + mediaPlayerManager: MediaPlayerManager, + fragment: Fragment + ): Boolean { + when (menuItem.itemId) { + R.id.menu_play_now -> + mediaPlayerManager.playTracksAndToast( + fragment = fragment, + insertionMode = MediaPlayerManager.InsertionMode.CLEAR, + id = item.id, + isArtist = isArtist + ) + R.id.menu_play_next -> + mediaPlayerManager.playTracksAndToast( + fragment = fragment, + insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT, + id = item.id, + isArtist = isArtist + ) + R.id.menu_play_last -> + mediaPlayerManager.playTracksAndToast( + fragment = fragment, + insertionMode = MediaPlayerManager.InsertionMode.APPEND, + id = item.id, + isArtist = isArtist + ) + R.id.menu_pin -> + DownloadUtil.justDownload( + action = DownloadAction.PIN, + fragment = fragment, + id = item.id, + isArtist = isArtist + ) + R.id.menu_unpin -> + DownloadUtil.justDownload( + action = DownloadAction.UNPIN, + fragment = fragment, + id = item.id, + isArtist = isArtist + ) + R.id.menu_download -> + DownloadUtil.justDownload( + action = DownloadAction.DOWNLOAD, + fragment = fragment, + id = item.id, + isArtist = isArtist + ) + else -> return false + } + return true + } + + fun handleContextMenuTracks( + menuItem: MenuItem, + tracks: List, + mediaPlayerManager: MediaPlayerManager, + fragment: Fragment + ): Boolean { + when (menuItem.itemId) { + R.id.song_menu_play_now -> { + mediaPlayerManager.playTracksAndToast( + fragment = fragment, + insertionMode = MediaPlayerManager.InsertionMode.CLEAR, + tracks = tracks + ) + } + R.id.song_menu_play_next -> { + mediaPlayerManager.playTracksAndToast( + fragment = fragment, + insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT, + tracks = tracks + ) + } + R.id.song_menu_play_last -> { + mediaPlayerManager.playTracksAndToast( + fragment = fragment, + insertionMode = MediaPlayerManager.InsertionMode.APPEND, + tracks = tracks + ) + } + R.id.song_menu_pin -> { + DownloadUtil.justDownload( + action = DownloadAction.PIN, + fragment = fragment, + tracks = tracks + ) + } + R.id.song_menu_unpin -> { + DownloadUtil.justDownload( + action = DownloadAction.UNPIN, + fragment = fragment, + tracks = tracks + ) + } + R.id.song_menu_download -> { + DownloadUtil.justDownload( + action = DownloadAction.DOWNLOAD, + fragment = fragment, + tracks = tracks + ) + } + R.id.song_menu_share -> { + val shareHandler: ShareHandler by inject() + shareHandler.createShare( + fragment = fragment, + tracks = tracks, + additionalId = null + ) + } + else -> return false + } + return true + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt index 8e0d1154..08b9f07c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt @@ -9,14 +9,12 @@ 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.async import kotlinx.coroutines.launch -import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import timber.log.Timber @@ -30,53 +28,50 @@ object CoroutinePatterns { } } -fun CoroutineScope.executeTaskWithToast( - task: suspend CoroutineScope.() -> Unit, - successString: () -> String? -): Job { +fun CoroutineScope.launchWithToast( + block: suspend CoroutineScope.() -> String? +) { // Launch the Job - val job = launch(CoroutinePatterns.loggingExceptionHandler, block = task) + val deferred = async(CoroutinePatterns.loggingExceptionHandler, block = block) // Setup a handler when the job is done - job.invokeOnCompletion { + deferred.invokeOnCompletion { val toastString = if (it != null && it !is CancellationException) { CommunicationError.getErrorMessage(it) } else { - successString() + null } - // Return early if nothing to post - if (toastString == null) return@invokeOnCompletion - launch(Dispatchers.Main) { - Util.toast(UApp.applicationContext(), toastString) - } - } - - return job -} - -fun CoroutineScope.executeTaskWithModalDialog( - fragment: Fragment, - task: suspend CoroutineScope.() -> Unit, - successString: () -> String -) { - // Create the job - val job = executeTaskWithToast(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() + val successString = toastString ?: deferred.await() + if (successString != null) { + Util.toast(successString, UApp.applicationContext()) + } } } } + +// Unused, kept commented for eventual later use +// fun CoroutineScope.executeTaskWithModalDialog( +// fragment: Fragment, +// task: suspend CoroutineScope.() -> String? +// ) { +// // Create the job +// val job = launchWithToast(task) +// +// // Create the dialog +// val builder = InfoDialog.Builder(fragment.requireContext()) +// builder.setTitle(R.string.background_task_wait) +// builder.setMessage(R.string.background_task_loading) +// builder.setOnCancelListener { job.cancel() } +// builder.setPositiveButton(R.string.common_cancel) { _, _ -> job.cancel() } +// val dialog = builder.create() +// dialog.show() +// +// // Add additional handler to close the dialog +// job.invokeOnCompletion { +// launch(Dispatchers.Main) { +// dialog.dismiss() +// } +// } +// } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt new file mode 100644 index 00000000..8c78b73f --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt @@ -0,0 +1,198 @@ +/* + * DownloadUtil.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.util + +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import java.util.LinkedList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.moire.ultrasonic.R +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.Track +import org.moire.ultrasonic.service.DownloadService +import org.moire.ultrasonic.service.MusicServiceFactory + +/** + * Retrieves a list of songs and adds them to the now playing list + */ +@Suppress("LongParameterList") +object DownloadUtil { + + fun justDownload( + action: DownloadAction, + fragment: Fragment, + id: String? = null, + name: String? = "", + isShare: Boolean = false, + isDirectory: Boolean = true, + isArtist: Boolean = false, + tracks: List? = null + ) { + + val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope + + // Launch the Job + scope.launchWithToast { + val tracksToDownload: List = tracks + ?: getTracksFromServerAsync(isArtist, id!!, isDirectory, name, isShare) + + // If we are just downloading tracks we don't need to add them to the controller + when (action) { + DownloadAction.DOWNLOAD -> DownloadService.downloadAsync( + tracksToDownload, + save = false, + updateSaveFlag = true + ) + DownloadAction.PIN -> DownloadService.downloadAsync( + tracksToDownload, + save = true, + updateSaveFlag = true + ) + DownloadAction.UNPIN -> DownloadService.unpinAsync(tracksToDownload) + DownloadAction.DELETE -> DownloadService.deleteAsync(tracksToDownload) + } + + // Return the string which should be displayed + getToastString(action, fragment, tracksToDownload) + } + } + + suspend fun getTracksFromServerAsync( + isArtist: Boolean, + id: String, + isDirectory: Boolean, + name: String?, + isShare: Boolean + ): MutableList { + return withContext(Dispatchers.IO) { + getTracksFromServer(isArtist, id, isDirectory, name, isShare) + } + } + + fun getTracksFromServer( + isArtist: Boolean, + id: String, + isDirectory: Boolean, + name: String?, + isShare: Boolean + ): MutableList { + val musicService = MusicServiceFactory.getMusicService() + val songs: MutableList = LinkedList() + val root: MusicDirectory + if (ActiveServerProvider.shouldUseId3Tags() && isArtist) { + return getSongsForArtist(id) + } else { + if (isDirectory) { + root = if (ActiveServerProvider.shouldUseId3Tags()) + musicService.getAlbumAsDir(id, name, false) + else + musicService.getMusicDirectory(id, name, false) + } else if (isShare) { + root = MusicDirectory() + val shares = musicService.getShares(true) + // Filter the received shares by the given id, and get their entries + val entries = shares.filter { it.id == id }.flatMap { it.getEntries() } + root.addAll(entries) + } else { + root = musicService.getPlaylist(id, name!!) + } + getSongsRecursively(root, songs) + } + return songs + } + + @Suppress("DestructuringDeclarationWithTooManyEntries") + @Throws(Exception::class) + private fun getSongsRecursively( + parent: MusicDirectory, + songs: MutableList + ) { + if (songs.size > Constants.MAX_SONGS_RECURSIVE) { + return + } + for (song in parent.getTracks()) { + if (!song.isVideo) { + songs.add(song) + } + } + val musicService = MusicServiceFactory.getMusicService() + for ((id1, _, _, title) in parent.getAlbums()) { + val root: MusicDirectory = if (ActiveServerProvider.shouldUseId3Tags()) + musicService.getAlbumAsDir(id1, title, false) + else + musicService.getMusicDirectory(id1, title, false) + getSongsRecursively(root, songs) + } + } + + @Throws(Exception::class) + private fun getSongsForArtist( + id: String + ): MutableList { + val songs: MutableList = LinkedList() + val musicService = MusicServiceFactory.getMusicService() + val artist = musicService.getAlbumsOfArtist(id, "", false) + for ((id1) in artist) { + val albumDirectory = musicService.getAlbumAsDir( + id1, + "", + false + ) + for (song in albumDirectory.getTracks()) { + if (!song.isVideo) { + songs.add(song) + } + } + } + return songs + } + + private fun getToastString( + action: DownloadAction, + fragment: Fragment, + tracksToDownload: List + ): String { + return when (action) { + DownloadAction.DOWNLOAD -> fragment.resources.getQuantityString( + R.plurals.n_songs_to_be_downloaded, + tracksToDownload.size, + tracksToDownload.size + ) + + DownloadAction.UNPIN -> { + fragment.resources.getQuantityString( + R.plurals.n_songs_unpinned, + tracksToDownload.size, + tracksToDownload.size + ) + } + + DownloadAction.PIN -> { + fragment.resources.getQuantityString( + R.plurals.n_songs_pinned, + tracksToDownload.size, + tracksToDownload.size + ) + } + + DownloadAction.DELETE -> { + fragment.resources.getQuantityString( + R.plurals.n_songs_deleted, + tracksToDownload.size, + tracksToDownload.size + ) + } + } + } +} + +enum class DownloadAction { + DOWNLOAD, PIN, UNPIN, DELETE +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index 5b179ec1..c6e69c8f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -136,10 +136,6 @@ object Settings { val seekIntervalMillis: Long get() = (seekInterval / 1000).toLong() - @JvmStatic - var mediaButtonsEnabled - by BooleanSetting(getKey(R.string.setting_key_media_buttons), true) - var resumePlayOnHeadphonePlug by BooleanSetting(R.string.setting_key_resume_play_on_headphones_plug, true) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt index 933730f4..a777605d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt @@ -41,7 +41,7 @@ object Storage { if (rootNotFoundError) { Settings.customCacheLocation = false Settings.cacheLocationUri = "" - Util.toast(UApp.applicationContext(), R.string.settings_cache_location_error) + Util.toast(R.string.settings_cache_location_error, true, UApp.applicationContext()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPicker.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPicker.kt index 63566313..90316aaf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPicker.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeSpanPicker.kt @@ -31,7 +31,7 @@ class TimeSpanPicker(private var mContext: Context, attrs: AttributeSet?, defSty AdapterView.OnItemSelectedListener { private val timeSpanEditText: EditText private val timeSpanSpinner: Spinner - private val timeSpanDisableCheckbox: CheckBox + val timeSpanDisableCheckbox: CheckBox private var mTimeSpan: Long = -1L private val adapter: ArrayAdapter private val dialog: View diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index 83ecb11b..fae3ef41 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -8,7 +8,6 @@ package org.moire.ultrasonic.util import android.Manifest.permission.POST_NOTIFICATIONS -import android.annotation.SuppressLint import android.app.Activity import android.app.Notification import android.app.NotificationChannel @@ -37,12 +36,15 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.AnyRes +import androidx.annotation.StringRes import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Timeline +import androidx.navigation.fragment.findNavController import java.io.Closeable import java.io.UnsupportedEncodingException import java.security.MessageDigest @@ -107,33 +109,41 @@ object Util { context.getString(R.string.setting_key_theme_dark) -> { R.style.UltrasonicTheme_Dark } + context.getString(R.string.setting_key_theme_black) -> { R.style.UltrasonicTheme_Black } + context.getString(R.string.setting_key_theme_light) -> { R.style.UltrasonicTheme_Light } + else -> { R.style.UltrasonicTheme_DayNight } } } + fun getString(@StringRes resId: Int): String { + return applicationContext().resources.getString(resId) + } + @JvmStatic @JvmOverloads - fun toast(context: Context?, messageId: Int, shortDuration: Boolean = true) { - toast(context, context!!.getString(messageId), shortDuration) + fun toast(messageId: Int, shortDuration: Boolean = true, context: Context?) { + toast(applicationContext().getString(messageId), shortDuration, context) } @JvmStatic - fun toast(context: Context?, message: CharSequence?) { - toast(context, message, true) + fun toast(message: CharSequence, context: Context?) { + toast(message, true, context) } @JvmStatic - @SuppressLint("ShowToast") // Invalid warning - fun toast(context: Context?, message: CharSequence?, shortDuration: Boolean) { - // If called after doing some background processing, our context might have expired! + // Toast needs a real context or it will throw a IllegalAccessException + // We wrap it in a try-catch block, because if called after doing + // some background processing, our context might have expired! + fun toast(message: CharSequence, shortDuration: Boolean, context: Context?) { try { if (toast == null) { toast = Toast.makeText( @@ -153,6 +163,22 @@ object Util { } } + fun Fragment.toast(message: CharSequence, shortDuration: Boolean = true) { + toast( + message, + shortDuration = shortDuration, + context = this.context + ) + } + + fun Fragment.toast(messageId: Int = 0, shortDuration: Boolean = true) { + toast( + messageId = messageId, + shortDuration = shortDuration, + context = this.context + ) + } + /** * Converts a byte-count to a formatted string suitable for display to the user. * For instance: @@ -479,7 +505,7 @@ object Util { @JvmStatic fun isNullOrWhiteSpace(string: String?): Boolean { - return string == null || string.isEmpty() || string.trim { it <= ' ' }.isEmpty() + return string.isNullOrEmpty() || string.trim { it <= ' ' }.isEmpty() } @JvmOverloads @@ -504,18 +530,22 @@ object Util { seconds ) } + hours > 0 -> { String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds) } + minutes >= DEGRADE_PRECISION_AFTER -> { String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds) } + minutes > 0 -> String.format( Locale.getDefault(), "%d:%02d", minutes, seconds ) + else -> String.format(Locale.getDefault(), "0:%02d", seconds) } } @@ -557,7 +587,7 @@ object Util { val requestPermissionLauncher = fragment.registerForActivityResult(ActivityResultContracts.RequestPermission()) { if (!it) { - toast(applicationContext(), R.string.notification_permission_required) + toast(R.string.notification_permission_required, context = fragment) } } @@ -569,6 +599,7 @@ object Util { } } } + fun postNotificationIfPermitted( notificationManagerCompat: NotificationManagerCompat, id: Int, @@ -583,8 +614,8 @@ object Util { notificationManagerCompat.notify(id, notification) } } + @JvmStatic - @Suppress("DEPRECATION") fun getVersionName(context: Context): String? { var versionName: String? = null val pm = context.packageManager @@ -838,4 +869,11 @@ object Util { Timber.d("${it.key}: ${it.value}") } } + + fun Fragment.navigateToCurrent() { + if (Settings.shouldTransitionOnPlayback) { + findNavController().popBackStack(R.id.playerFragment, true) + findNavController().navigate(R.id.playerFragment) + } + } } diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 140946b6..3cf8675c 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -1,7 +1,6 @@ - Načítám… Chyba sítě. Ověřte adresu serveru nebo zkuste později. API serveru v%1$s nepodporuje tuto funkci. Tento program vyžaduje síťové připojení. Zapněte Wi-Fi nebo mobilní připojení. @@ -9,7 +8,6 @@ Nesrozumitelná odpověď. Ověřte adresu serveru. Chyba HTTPS certifikátu: %1$s. Vyjímka SSL připojení. Ověřte certifikát serveru. - Chvilku strpení… Záložky Knihovna médií Chat @@ -188,8 +186,6 @@ Neomezené Max Bitrate - wi-fi Maximum skladeb - Odpovídat na tlačítka ovládání médií telefonu, sluchátek a bluetooth - Tlačítka médií Čas vypršení připojení 105 sekund 120 sekund diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index bc954811..46aa76fc 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -1,6 +1,5 @@ - Lade… Ein Netzwerkfehler ist aufgetreten. Bitte die Serveradresse prüfen oder später noch einmal versuchen. Server API v%1$s unterstützt diese Funktion nicht. Dieses Programm benötigt eine Netzwerkverbindung. Bitte das WLAN oder Mobilfunk einschalten. @@ -8,7 +7,6 @@ Antwort nicht verstanden. Bitte die Serveradresse überprüfen. HTTPS Zertifikatsfehler: %1$s. SSL Verbindungsfehler. Bitte das Serverzertifikat überprüfen. - Bitte warten… Lesezeichen Medienbibliothek Chat @@ -230,8 +228,6 @@ Unbegrenzt Max. Bitrate - WLAN Max. Anzahl der Titel - Auf Telefon, Headset und Bluetooth-Media-Tasten reagieren - Medien Tasten Netzwerk Zeitüberschreitung 105 Sekunden 120 Sekunden diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 6583680d..28991f37 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -1,6 +1,5 @@ - Cargando… Se ha producido un error de red. Por favor comprueba la dirección del servidor o reinténtalo mas tarde. La API del servidor v%1$s no admite esta función. Este programa requiere acceso a la red. Por favor enciende la Wi-Fi o la red móvil. @@ -8,7 +7,6 @@ No se entiende la respuesta. Por favor comprueba la dirección del servidor. Error del certificado HTTPS: %1$s. Excepción de conexión SSL. Compruebe el certificado del servidor. - Por favor espera… Marcadores Biblioteca Chat @@ -232,8 +230,6 @@ Ilimitado Bitrate máximo - Wi-Fi Máximo de Canciones - Responder a los botones multimedia del dispositivo, auriculares y Bluetooth - Botones multimedia Tiempo de espera de la red 105 segundos 120 segundos diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 0f2f8e74..f2578bef 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -1,6 +1,5 @@ - Chargement… Une erreur réseau est survenue. Veuillez vérifier l\'adresse du serveur ou réessayer plus tard. L’API v%1$s du serveur ne supporte pas cette fonction. Cette application requiert un accès au réseau. Veuillez activer le Wi-Fi ou le réseau mobile. @@ -8,7 +7,6 @@ Réponse incorrecte. Veuillez vérifier l\'adresse du serveur. Erreur de certificat HTTPS : %1$s. Erreur de connexion SSL. Veuillez vérifier le certificat du serveur. - Veuillez patienter… Signets Bibliothèque musicale Salon de discussion @@ -225,8 +223,6 @@ Illimité Débit maximal - Wi-Fi Titres maximum - Répondre au boutons média de l\'appareil, du casque et du Bluetooth - Boutons média Délai d\'attente de connexion 105 secondes 120 secondes diff --git a/ultrasonic/src/main/res/values-gl/strings.xml b/ultrasonic/src/main/res/values-gl/strings.xml index 54b09230..4eee6eae 100644 --- a/ultrasonic/src/main/res/values-gl/strings.xml +++ b/ultrasonic/src/main/res/values-gl/strings.xml @@ -1,6 +1,5 @@ - Cargando… Ocorreu un erro de rede. Por favor comproba a dirección do servidor ou téntao de novo mais tarde. A API do servidor v%1$s non admite esta función. Este programa require acceso á rede. Por favor acende a Wi-Fi ou a rede móbil. @@ -8,7 +7,6 @@ Non se entende a resposta. Por favor comproba a dirección do servidor. Erro do certificado HTTPS: %1$s. Excepción de conexión SSL. Comprobe o certificado do servidor. - Por favor agarde… Biblioteca Chat Reproducindo agora diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index cca3e6db..0bfd09f0 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -1,7 +1,6 @@ - Betöltés… Hálózati hiba történt! Kérjük, ellenőrizze a kiszolgáló címét vagy próbálja később! A v%1$s verziójú Szerver api nem támogatja ezt a funkciót. Az alkalmazás hálózati hozzáférést igényel. Kérjük, kapcsolja be a Wi-Fi-t vagy a mobilhálózatot! @@ -9,7 +8,6 @@ Értelmezhetetlen válasz! Kérjük, ellenőrizze a kiszolgáló címét! HTTPS tanúsítványhiba: %1$s. SSL kapcsolat kivétel. Kérjük, ellenőrizze a szerver tanúsítványát. - Kérem várjon!… Könyvjelzők Médiakönyvtár Csevegés (Chat) @@ -194,8 +192,6 @@ Korlátlan Max. bitráta - Wi-Fi kapcsolat Dalok max. találati száma - Telefon irányítása a Bluetooth eszköz, vagy a fülhallgató vezérlőgombjaival. - Média vezérlőgombok Hálózati időtúllépés 105 másodperc 120 másodperc diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index b0f2faa9..77fed0ee 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -1,13 +1,11 @@ - Caricamento… Si è verificato un errore di rete. Si prega di controllare l\'indirizzo del server o riprovare più tardi. Questo programma richiede l\'accesso alla rete. Per favore, abilita la connessione Wi-FI o la rete mobile. Risorsa non trovata. Si prega di controllare l\'indirizzo del server. Risposta non comprensibile. Si prega di controllare l\'indirizzo del server. Errore certificato HTTPS. %1$s. Anomalia connessione SSL. Si prega di controllare il certificato del server. - Attendere per favore#8230; Segnalibri Libreria multimediale Chat @@ -184,8 +182,6 @@ Illimitato Bitrate Max - Wi-Fi N° Max Canzoni - Controlli per risposta alle chiamate, per cuffie e Bluetooth - Tasti Media Timeout Rete 105 seconds 120 seconds diff --git a/ultrasonic/src/main/res/values-ja/strings.xml b/ultrasonic/src/main/res/values-ja/strings.xml index 51274e09..8641b570 100644 --- a/ultrasonic/src/main/res/values-ja/strings.xml +++ b/ultrasonic/src/main/res/values-ja/strings.xml @@ -1,11 +1,9 @@ - 読み込み中… ネットワークエラーが発生しました。サーバーのアドレスを確認してやり直してください。 応答が確認できません。サーバーのアドレスを確認してください。 HTTPS証明書エラー: %1$s. SSL接続が異常です。サーバーの証明書を確認してください。 - お待ち下さい… ブックマーク メディアライブラリ チャット @@ -181,8 +179,6 @@ 64 Kbps 80 Kbps 96 Kbps - 端末本体、ヘッドセットやBluetoothの再生コントロールボタンに対応します - メディアボタン 15秒 75秒 90秒 diff --git a/ultrasonic/src/main/res/values-nb-rNO/strings.xml b/ultrasonic/src/main/res/values-nb-rNO/strings.xml index 7a2c4406..81719af4 100644 --- a/ultrasonic/src/main/res/values-nb-rNO/strings.xml +++ b/ultrasonic/src/main/res/values-nb-rNO/strings.xml @@ -37,7 +37,6 @@ Stopp Søk Send en melding - Laster inn … Ultrasonic Mediebibliotek Spill @@ -137,7 +136,6 @@ Fant ikke ressursen. Sjekk tjeneradressen. Forsto ikke svaret. Sjekk tjeneradressen. HTTPS-sertifikatsfeil: %1$s. - Vent … Bokmerker Spilles nå Ingen nettradioopptakskanaler registrert. @@ -219,7 +217,6 @@ Angi en gyldig nettadresse. Maks. artister Ubegrenset - Medieknapper Nettverkstidsavbrudd 90 sekunder 75 sekunder @@ -317,7 +314,6 @@ Klarte ikke å slette %s-spillelisten Bytt til «Spilles nå» etter at avspilling startes i medievisning Trer i effekt neste gang Android skanner enheten din for musikk. - Svarer på enhets-, hodesett, og Blåtannsmedieknapper Kun last ned på ubegrensede tilkoblinger Last ned medier i bakgrunnen …\? Merknader kreves for medieavspilling. Du kan innvilge tilgangen når som helst i Android-innstillingene. diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index af539bfd..5f34a530 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -1,7 +1,6 @@ - Bezig met laden… Er is een netwerkfout opgetreden. Controleer het serveradres of probeer het later opnieuw. De server-api, v %1$s, heeft geen ondersteuning voor deze functie. Deze app vereist netwerktoegang. Schakel wifi of mobiel internet in. @@ -9,7 +8,6 @@ Het antwoord werd niet begrepen. Controleer het serveradres. HTTPS-certificaatfout: %1$s. SSL-verbindingsuitzondering. Controleer het servercertificaat. - Even geduld… Bladwijzers Mediabibliotheek Chat @@ -233,8 +231,6 @@ Ongelimiteerd Max. bitsnelheid via wifi Max. aantal nummers - Reageren op telefoon-, headset- en bluetooth-mediatoetsen. - Mediatoetsen Netwerktime-out 105 seconden 120 seconden diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index f62deae6..6610ad1c 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -1,6 +1,5 @@ - Ładowanie… Wystąpił błąd sieci. Proszę sprawdzić adres serwera lub spróbować później. API serwera w wersji v%1$s nie wspiera tej funkcjonalności. Ta aplikacja wymaga dostępu do sieci. Proszę włączyć Wi-Fi lub dane komórkowe. @@ -8,7 +7,6 @@ Brak prawidłowej odpowiedzi. Proszę sprawdzić adres serwera. Błąd certyfikatu HTTPS: %1$s. Błąd połączenia SSL. Proszę sprawdzić certyfikat serwera. - Proszę czekać… Zakładki Biblioteka Czat @@ -187,8 +185,6 @@ Bez limitu Maksymalny bitrate dla połączenia Wi-Fi Maksymalna ilość wyników - utwory - Reaguj na przyciski multimedialne telefonu, słuchawek i urządzeń Bluetooth - Przyciski Przekroczenie limitu czasu sieci 105 sekund 120 sekund diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index 70a42293..43c96885 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -1,7 +1,6 @@ - Carregando… Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde. O servidor api v%1$s não tem suporte para esta função. Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados. @@ -9,7 +8,6 @@ Não entendi a resposta. Verifique o endereço do servidor. Erro de certificado HTTPS: %1$s. Exceção de conexão SSL. Verifique o certificado do servidor. - Por favor aguarde… Favoritos Biblioteca de Mídia Chat @@ -231,8 +229,6 @@ Ilimitado Taxa Máxima de Bits - Wi-Fi Máximo de Músicas - Obedecer aos botões do celular, fones e botões de mídia do Bluetooth - Botões de Mídia Timeout da Rede 105 segundos 120 segundos diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index 834ea509..ad5cd34f 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -1,6 +1,5 @@ - Carregando… Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde. Server api v%1$s does not support this function. Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados. @@ -8,7 +7,6 @@ Não entendi a resposta. Verifique o endereço do servidor. Erro de certificado HTTPS: %1$s. Exceção de conexão SSL. Verifique o certificado do servidor. - Por favor aguarde… Favoritos Biblioteca de Mídia Chat @@ -187,8 +185,6 @@ Ilimitado Máx. de Taxa de Bits - Wi-Fi Máximo de Músicas - Obedecer aos botões do telemóvel, auricular e botões de mídia do Bluetooth - Botões de Mídia Timeout da Rede 105 segundos 120 segundos diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 0c750c2f..38756d33 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -1,7 +1,6 @@ - Загрузка… Произошла ошибка сети. Пожалуйста, проверьте адрес сервера или повторите попытку позже. Серверный api версии %1$s не поддерживает эту функцию. Эта программа требует доступа к сети. Пожалуйста, включите Wi-Fi или мобильную сеть. @@ -9,7 +8,6 @@ Не понятный ответ. Пожалуйста, проверьте адрес сервера. Ошибка сертификата HTTPS: %1$s. Исключение SSL-соединения. Пожалуйста, проверьте сертификат сервера. - Пожалуйста, подождите… Закладки Медиа библиотека Чат @@ -212,8 +210,6 @@ Неограниченный Максимальный битрейт - Wi-Fi подключение Максимум треков - Отвечать на телефон, гарнитуру и мультимедийные кнопки Bluetooth - Медиа кнопки Таймаут сети 105 секунд 120 секунд diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 2410948c..625d81fb 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -1,6 +1,5 @@ - 加载中… 发生网络错误。请检查服务器地址或稍后重试。 服务端 API v%1$s 不支持此功能。 此软件需要连接网络,请打开 Wi-Fi 或移动网络。 @@ -8,7 +7,6 @@ 未知回复内容,请检查服务器地址。 HTTPS 证书错误:%1$s. SSL 连接异常。请检查服务器证书。 - 请稍等… 书签 媒体库 聊天 @@ -218,8 +216,6 @@ 不限制 最大比特率 - WIFI 最大歌曲 - 响应手机、耳机和蓝牙设备的媒体按钮 - 媒体按钮 网络超时 105 秒 120 秒 diff --git a/ultrasonic/src/main/res/values-zh-rTW/strings.xml b/ultrasonic/src/main/res/values-zh-rTW/strings.xml index ced7be3b..6645fc95 100644 --- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml @@ -1,6 +1,5 @@ - 載入中… 書籤 媒體庫 正在播放 @@ -146,7 +145,6 @@ 荷蘭語 已關閉遠端控制,音樂將在手機上播放。 德語 - 請稍候… 取消固定 輸入播放清單名稱: 依照時間排列 diff --git a/ultrasonic/src/main/res/values/setting_keys.xml b/ultrasonic/src/main/res/values/setting_keys.xml index 0e135e5e..20356d83 100644 --- a/ultrasonic/src/main/res/values/setting_keys.xml +++ b/ultrasonic/src/main/res/values/setting_keys.xml @@ -19,7 +19,6 @@ preloadCount parallelDownloads hideMedia - mediaButtons scrobble serverScaling wifiRequiredForDownload diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 04488224..4ab4279d 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -1,7 +1,6 @@ - Loading… A network error occurred. Please check the server address or try again later. Server API v%1$s does not support this function. This program requires network access. Please turn on Wi-Fi or mobile network. @@ -9,7 +8,6 @@ Didn\'t understand the reply. Please check the server address. HTTPS certificate error: %1$s. SSL connection exception. Please check server certificate. - Please wait… Bookmarks Media Library Chat @@ -234,8 +232,6 @@ Max Bitrate - Wi-Fi Max Bitrate - When pinning a song permanently Max Songs - Respond to phone, headset and Bluetooth media buttons - Media Buttons Network Timeout 105 seconds 120 seconds diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index 692117c5..bb5d6fdd 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -72,12 +72,6 @@ a:summary="@string/settings.show_artist_picture_summary" a:title="@string/settings.show_artist_picture" app:iconSpaceReserved="false"/> -