From 7124939467e731c2563983f7c3032c9e4546781f Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Thu, 29 Jun 2023 13:09:51 +0000 Subject: [PATCH 01/25] [ROBOTEST] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 67f84832..fa541152 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -138,7 +138,7 @@ RoboTest: script: - curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash - gcloud auth activate-service-account --key-file .secure_files/firebase-key.json - - gcloud firebase test android run --project ultrasonic-61089 --type robo --app ultrasonic-release/${PACKAGE_APK} --robo-directives click:button1= --device model=Nexus6,version=21,locale=en,orientation=portrait --device model=Pixel3,version=28,locale=fr,orientation=landscape --device model=Pixel5,version=30,locale=zh,orientation=portrait + - gcloud firebase test android run --project ultrasonic-61089 --type robo --app ultrasonic-release/${PACKAGE_APK} --robo-directives click:button1= --device model=Nexus6,version=21,locale=en,orientation=portrait --device model=Pixel3,version=28,locale=fr,orientation=landscape rules: # Run when releasing a new tag - if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID From 500ffa8009535a4b1b0fd65f23156c6c47c1e0fc Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 29 Jun 2023 13:31:51 +0000 Subject: [PATCH 02/25] Update kotlinxCoroutines to v1.7.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 816a968e..0e615463 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.5.2" kotlin = "1.8.22" -kotlinxCoroutines = "1.7.1" +kotlinxCoroutines = "1.7.2" viewModelKtx = "2.6.1" swipeRefresh = "1.1.0" From d08711eb0cb4b4f55e4f6effbd1518bf1dcc3655 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 30 Jun 2023 18:33:00 +0000 Subject: [PATCH 03/25] Update dependency gradle to v8.2 --- gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 63375 bytes gradle/wrapper/gradle-wrapper.properties | 3 ++- gradlew | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d3e0ab67b14947c167a862655af9b..033e24c4cdf41af1ab109bc7f253b2b887023340 100644 GIT binary patch delta 16170 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 delta 14892 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8707e8b5..bf01c4d1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cbb..fcb6fca1 100755 --- a/gradlew +++ b/gradlew @@ -130,10 +130,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. From db278afe4af97de4065f3436b34f78eca47a1c97 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 3 Jul 2023 22:32:22 +0000 Subject: [PATCH 04/25] Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.5.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 816a968e..b2326a3c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ navigation = "2.6.0" gradlePlugin = "8.0.2" androidxcore = "1.10.1" ktlint = "0.43.2" -ktlintGradle = "11.4.2" +ktlintGradle = "11.5.0" detekt = "1.23.0" preferences = "1.2.0" media3 = "1.0.2" From 211af57e8ba2a7ab6ae28bd5dc358aa197d9edea Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 6 Jul 2023 15:06:49 +0000 Subject: [PATCH 05/25] Update media3 to v1.1.0 --- gradle/libs.versions.toml | 2 +- .../playback/CustomNotificationProvider.kt | 40 ------------------- .../ultrasonic/playback/PlaybackService.kt | 25 +++++------- .../ultrasonic/service/JukeboxMediaPlayer.kt | 22 ++++++++++ .../service/JukeboxUnimplementedFunctions.kt | 17 +++++--- 5 files changed, 46 insertions(+), 60 deletions(-) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 21d9e191..088fe2d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ ktlint = "0.43.2" ktlintGradle = "11.5.0" detekt = "1.23.0" preferences = "1.2.0" -media3 = "1.0.2" +media3 = "1.1.0" androidSupport = "1.6.0" materialDesign = "1.9.0" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt deleted file mode 100644 index 50fa4c4f..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * CustomNotificationProvider.kt - * Copyright (C) 2009-2022 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ -package org.moire.ultrasonic.playback - -import android.content.Context -import androidx.media3.common.Player -import androidx.media3.common.util.UnstableApi -import androidx.media3.session.CommandButton -import androidx.media3.session.DefaultMediaNotificationProvider -import androidx.media3.session.MediaSession -import com.google.common.collect.ImmutableList -import org.koin.core.component.KoinComponent - -@UnstableApi -class CustomNotificationProvider(ctx: Context) : - DefaultMediaNotificationProvider(ctx), - KoinComponent { - - // By default the skip buttons are not shown in compact view. - // We add the COMMAND_KEY_COMPACT_VIEW_INDEX to show them - // See also: https://github.com/androidx/media/issues/410 - override fun getMediaButtons( - session: MediaSession, - playerCommands: Player.Commands, - customLayout: ImmutableList, - playWhenReady: Boolean - ): ImmutableList { - val commands = super.getMediaButtons(session, playerCommands, customLayout, playWhenReady) - - commands.forEachIndexed { index, command -> - command.extras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, index) - } - - return commands - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 7965558e..e58a6799 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -130,21 +130,14 @@ class PlaybackService : private fun initializeSessionAndPlayer() { if (isStarted) return - setMediaNotificationProvider(CustomNotificationProvider(UApp.applicationContext())) - - // TODO: Remove minor code duplication with updateBackend() val desiredBackend = if (activeServerProvider.getActiveServer().jukeboxByDefault) { + Timber.i("Jukebox enabled by default") MediaPlayerManager.PlayerBackend.JUKEBOX } else { MediaPlayerManager.PlayerBackend.LOCAL } - player = if (activeServerProvider.getActiveServer().jukeboxByDefault) { - Timber.i("Jukebox enabled by default") - getJukeboxPlayer() - } else { - getLocalPlayer() - } + player = createNewBackend(desiredBackend) actualBackend = desiredBackend @@ -213,11 +206,7 @@ class PlaybackService : player.removeListener(listener) player.release() - player = if (newBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) { - getJukeboxPlayer() - } else { - getLocalPlayer() - } + player = createNewBackend(newBackend) // Add fresh listeners player.addListener(listener) @@ -227,6 +216,14 @@ class PlaybackService : actualBackend = newBackend } + private fun createNewBackend(newBackend: MediaPlayerManager.PlayerBackend): Player { + return if (newBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) { + getJukeboxPlayer() + } else { + getLocalPlayer() + } + } + private fun getJukeboxPlayer(): Player { return JukeboxMediaPlayer() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt index dfc06cd5..20894ca6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt @@ -206,6 +206,8 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_GET_TIMELINE, Player.COMMAND_GET_DEVICE_VOLUME, + Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS, + Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS, Player.COMMAND_ADJUST_DEVICE_VOLUME, Player.COMMAND_SET_DEVICE_VOLUME ) @@ -213,6 +215,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { if (playlist.isNotEmpty()) { commandsBuilder.addAll( Player.COMMAND_GET_CURRENT_MEDIA_ITEM, + Player.COMMAND_GET_METADATA, Player.COMMAND_GET_MEDIA_ITEMS_METADATA, Player.COMMAND_PLAY_PAUSE, Player.COMMAND_PREPARE, @@ -284,6 +287,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {} override fun setDeviceVolume(volume: Int) { + setDeviceVolume(volume, 0) + } + + override fun setDeviceVolume(volume: Int, flags: Int) { gain = volume tasks.remove(SetGain::class.java) tasks.add(SetGain(floatGain)) @@ -299,17 +306,32 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } } + @Deprecated("Deprecated in Java") override fun increaseDeviceVolume() { + increaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI) + } + + override fun increaseDeviceVolume(flags: Int) { gain = (gain + 1).coerceAtMost(MAX_GAIN) deviceVolume = gain } + @Deprecated("Deprecated in Java") override fun decreaseDeviceVolume() { + decreaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI) + } + + override fun decreaseDeviceVolume(flags: Int) { gain = (gain - 1).coerceAtLeast(0) deviceVolume = gain } + @Deprecated("Deprecated in Java") override fun setDeviceMuted(muted: Boolean) { + setDeviceMuted(muted, C.VOLUME_FLAG_SHOW_UI) + } + + override fun setDeviceMuted(muted: Boolean, flags: Int) { gain = 0 deviceVolume = gain } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt index a22111be..c92abd23 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt @@ -67,6 +67,18 @@ abstract class JukeboxUnimplementedFunctions : Player { TODO("Not yet implemented") } + override fun replaceMediaItem(index: Int, mediaItem: MediaItem) { + TODO("Not yet implemented") + } + + override fun replaceMediaItems( + fromIndex: Int, + toIndex: Int, + mediaItems: MutableList + ) { + TODO("Not yet implemented") + } + override fun setPlayWhenReady(playWhenReady: Boolean) { TODO("Not yet implemented") } @@ -134,11 +146,6 @@ abstract class JukeboxUnimplementedFunctions : Player { override fun setPlaybackSpeed(speed: Float) { TODO("Not yet implemented") } - - override fun stop(reset: Boolean) { - TODO("Not yet implemented") - } - override fun getCurrentTracks(): Tracks { // TODO Dummy information is returned for now, this seems to work return Tracks.EMPTY From 9fa80d206b0791d8a78bef08640a9eabc8975bc8 Mon Sep 17 00:00:00 2001 From: Alex Katlein Date: Thu, 6 Jul 2023 15:11:02 +0000 Subject: [PATCH 06/25] Properly handling nested directory structures in Android Auto --- .../playback/AutoMediaBrowserCallback.kt | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index 8ca449af..d6b375b5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -274,10 +274,12 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : customCommandFuture = onSetRating(session, controller, HeartRating(true)) updateCustomHeartButton(session, true) } + PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF -> { customCommandFuture = onSetRating(session, controller, HeartRating(false)) updateCustomHeartButton(session, false) } + else -> { Timber.d( "CustomCommand not recognized %s with extra %s", @@ -290,6 +292,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : return customCommandFuture return super.onCustomCommand(session, controller, customCommand, args) } + override fun onSetRating( session: MediaSession, controller: MediaSession.ControllerInfo, @@ -385,10 +388,12 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong( mediaIdParts[1], mediaIdParts[2], mediaIdParts[3] ) + MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2]) MEDIA_ALBUM_SONG_ITEM -> playAlbumSong( mediaIdParts[1], mediaIdParts[2], mediaIdParts[3] ) + MEDIA_SONG_STARRED_ID -> playStarredSongs() MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1]) MEDIA_SONG_RANDOM_ID -> playRandomSongs() @@ -400,6 +405,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode( mediaIdParts[1], mediaIdParts[2] ) + MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1]) else -> null } @@ -432,6 +438,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : MEDIA_ALBUM_PAGE_ID -> return getAlbums( AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt() ) + MEDIA_PLAYLIST_ID -> return getPlaylists() MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(AlbumListType.FREQUENT) MEDIA_ALBUM_NEWEST_ID -> return getAlbums(AlbumListType.NEWEST) @@ -447,6 +454,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : MEDIA_ARTIST_ITEM -> return getAlbumsForArtist( parentIdParts[1], parentIdParts[2] ) + MEDIA_ALBUM_ITEM -> return getSongsForAlbum(parentIdParts[1], parentIdParts[2]) MEDIA_SHARE_ITEM -> return getSongsForShare(parentIdParts[1]) MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(parentIdParts[1]) @@ -534,10 +542,12 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong( mediaIdParts[1], mediaIdParts[2], mediaIdParts[3] ) + MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2]) MEDIA_ALBUM_SONG_ITEM -> playAlbumSong( mediaIdParts[1], mediaIdParts[2], mediaIdParts[3] ) + MEDIA_SONG_STARRED_ID -> playStarredSongs() MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1]) MEDIA_SONG_RANDOM_ID -> playRandomSongs() @@ -549,6 +559,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode( mediaIdParts[1], mediaIdParts[2] ) + MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1]) else -> { listOf() @@ -768,7 +779,17 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|")) // TODO: Paging is not implemented for songs, is it necessary at all? - val items = songs.getTracks().take(DISPLAY_LIMIT) + val items = songs.getChildren().take(DISPLAY_LIMIT).toMutableList() + + items.sortWith { o1, o2 -> + if (o1.isDirectory && o2.isDirectory) + (o1.title ?: "").compareTo(o2.title ?: "") + else if (o1.isDirectory) + -1 + else + 1 + } + items.map { item -> if (item.isDirectory) mediaItems.add( @@ -776,7 +797,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|"), FOLDER_TYPE_TITLES ) - else + else if (item is Track) mediaItems.add( item.toMediaItem( listOf( @@ -789,6 +810,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : ) } } + return@future LibraryResult.ofItemList(mediaItems, null) } } From 243653760941786d760fb6986df2b2b16e76a86d Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 6 Jul 2023 17:26:33 +0200 Subject: [PATCH 07/25] Fix a bug introduced in 725d9281 --- .../org/moire/ultrasonic/fragment/TrackCollectionFragment.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9b923e95..b16fe044 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -36,6 +36,7 @@ import org.moire.ultrasonic.adapters.AlbumHeader import org.moire.ultrasonic.adapters.AlbumRowDelegate import org.moire.ultrasonic.adapters.HeaderViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory @@ -582,7 +583,7 @@ open class TrackCollectionFragment( } else { setTitle(name) - if (isAlbum) { + if (isAlbum && ActiveServerProvider.shouldUseId3Tags()) { listModel.getAlbum(refresh2, id, name) } else { listModel.getMusicDirectory(refresh2, id, name) From 6be96ee8c9a5feda4398f5764676eb7c7858486b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 6 Jul 2023 15:46:47 +0000 Subject: [PATCH 08/25] [ROBOTEST] Update koin to v3.4.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 057f57a7..3396bd6c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ retrofit = "2.9.0" ## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24 jackson = "2.13.5" okhttp = "4.11.0" -koin = "3.3.2" +koin = "3.4.2" picasso = "2.8" junit4 = "4.13.2" From ecee57e166f9f4d203e91b98caf2678250cce24d Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 6 Jul 2023 18:05:16 +0200 Subject: [PATCH 09/25] Avoid rare NPE --- .../ultrasonic/activity/NavigationActivity.kt | 2 +- .../ultrasonic/fragment/SettingsFragment.kt | 2 +- .../org/moire/ultrasonic/util/Storage.kt | 33 ++++++++++++------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 76df1ea5..160677b1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -306,7 +306,7 @@ class NavigationActivity : AppCompatActivity() { Storage.reset() lifecycleScope.launch(Dispatchers.IO) { - Storage.ensureRootIsAvailable() + Storage.checkForErrorsWithCustomRoot() } setMenuForServerCapabilities() 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 700bde05..d007c399 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -344,7 +344,7 @@ class SettingsFragment : // Clear download queue. mediaPlayerManager.clear() Storage.reset() - Storage.ensureRootIsAvailable() + Storage.checkForErrorsWithCustomRoot() } private fun setDebugLogToFile(writeLog: Boolean) { 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 f6b5b9d2..933730f4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt @@ -22,19 +22,23 @@ import timber.log.Timber object Storage { val mediaRoot: ResettableLazy = ResettableLazy { - getRoot()!! + val ret = getRoot() + rootNotFoundError = ret.second + ret.first } + var rootNotFoundError: Boolean = false + fun reset() { StorageFile.storageFilePathDictionary.clear() StorageFile.notExistingPathDictionary.clear() mediaRoot.reset() + rootNotFoundError = false Timber.i("StorageFile caches were reset") } - fun ensureRootIsAvailable() { - val root = getRoot() - if (root == null) { + fun checkForErrorsWithCustomRoot() { + if (rootNotFoundError) { Settings.customCacheLocation = false Settings.cacheLocationUri = "" Util.toast(UApp.applicationContext(), R.string.settings_cache_location_error) @@ -98,18 +102,25 @@ object Storage { return success } - private fun getRoot(): AbstractFile? { + private fun getRoot(): Pair { return if (Settings.customCacheLocation) { - if (Settings.cacheLocationUri.isBlank()) return null + if (Settings.cacheLocationUri.isBlank()) return Pair(getDefaultRoot(), true) val documentFile = DocumentFile.fromTreeUri( UApp.applicationContext(), Uri.parse(Settings.cacheLocationUri) - ) ?: return null - if (!documentFile.exists()) return null - StorageFile(null, documentFile.uri, documentFile.name!!, documentFile.isDirectory) + ) ?: return Pair(getDefaultRoot(), true) + if (!documentFile.exists()) return Pair(getDefaultRoot(), true) + Pair( + StorageFile(null, documentFile.uri, documentFile.name!!, documentFile.isDirectory), + false + ) } else { - val file = File(FileUtil.defaultMusicDirectory.path) - JavaFile(null, file) + Pair(getDefaultRoot(), false) } } + + private fun getDefaultRoot(): JavaFile { + val file = File(FileUtil.defaultMusicDirectory.path) + return JavaFile(null, file) + } } From 1fba65ed3a8398c44a8b0e3a29d48684a7d717e5 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 10 Jul 2023 12:32:41 +0000 Subject: [PATCH 10/25] Update dependency gradle to v8.2.1 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bf01c4d1..c747538f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From e7825fc90c0deebe6aeb7db8273a46ce322cab04 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 23 Jul 2023 13:31:45 +0000 Subject: [PATCH 11/25] Update dependency org.junit.vintage:junit-vintage-engine to v5.10.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 918bc7c1..b48afdce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ koin = "3.4.2" picasso = "2.8" junit4 = "4.13.2" -junit5 = "5.9.3" +junit5 = "5.10.0" mockito = "5.4.0" mockitoKotlin = "5.0.0" kluent = "1.73" From 62ba16eedd69aef15ee1397a5eac56304c75b6c9 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Mon, 24 Jul 2023 18:38:08 +0000 Subject: [PATCH 12/25] Fix unpin --- .../ultrasonic/fragment/SearchFragment.kt | 6 +- .../ultrasonic/playback/PlaybackService.kt | 2 +- .../ultrasonic/service/DownloadService.kt | 103 +++++++++++++----- .../ultrasonic/subsonic/DownloadHandler.kt | 12 +- 4 files changed, 89 insertions(+), 34 deletions(-) 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 cd4d1104..4d2ffe2f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -140,7 +140,11 @@ class SearchFragment : MultiListFragment(), KoinComponent { private fun downloadBackground(save: Boolean, songs: List) { val onValid = Runnable { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - DownloadService.download(songs.filterNotNull(), save) + DownloadService.download( + songs.filterNotNull(), + save = save, + updateSaveFlag = true + ) } onValid.run() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index e58a6799..8de83393 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -325,7 +325,7 @@ class PlaybackService : ).map { it.toTrack() } launch { - DownloadService.download(nextSongs, save = false, isHighPriority = true) + DownloadService.download(nextSongs, isHighPriority = true) } } 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 808c3bbe..dff3d5b3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -274,46 +274,26 @@ class DownloadService : Service(), KoinComponent { @Synchronized fun download( tracks: List, - save: Boolean, - isHighPriority: Boolean = false + save: Boolean = false, + isHighPriority: Boolean = false, + updateSaveFlag: Boolean = false ) { CoroutineScope(Dispatchers.IO).launch { - // First handle and filter out those tracks that are already completed - var filteredTracks: List - if (save) { - tracks.filter { Storage.isPathExists(it.getCompleteFile()) }.forEach { track -> - Storage.getFromPath(track.getCompleteFile())?.let { - Storage.renameOrDeleteIfAlreadyExists(it, track.getPinnedFile()) - postState(track, DownloadState.PINNED) - } - } - filteredTracks = tracks.filter { !Storage.isPathExists(it.getPinnedFile()) } + // Remove tracks which are already downloaded and update the save flag + // if needed + var filteredTracks = if (updateSaveFlag) { + setSaveFlagForTracks(save, tracks) } else { - tracks.filter { Storage.isPathExists(it.getPinnedFile()) }.forEach { track -> - Storage.getFromPath(track.getPinnedFile())?.let { - Storage.renameOrDeleteIfAlreadyExists(it, track.getCompleteFile()) - postState(track, DownloadState.DONE) - } - } - filteredTracks = tracks.filter { !Storage.isPathExists(it.getCompleteFile()) } - } - - // Update Pinned flag of items in progress - downloadQueue.filter { item -> tracks.any { it.id == item.id } } - .forEach { it.pinned = save } - tracks.forEach { - activeDownloads[it.id]?.downloadTrack?.pinned = save - } - tracks.forEach { - failedList[it.id]?.pinned = save + removeDownloadedTracksFromList(tracks) } + // Remove tracks which are currently downloading filteredTracks = filteredTracks.filter { !downloadQueue.any { i -> i.id == it.id } && !activeDownloads.containsKey(it.id) } - // The remainder tracks should be added to the download queue + // The remaining tracks should be added to the download queue // By using the counter we ensure that the songs are added in the correct order var priority = 0 val tracksToDownload = @@ -334,6 +314,69 @@ class DownloadService : Service(), KoinComponent { } } + private fun removeDownloadedTracksFromList(tracks: List): List { + return tracks.filter { track -> + val pinnedFile = Storage.getFromPath(track.getPinnedFile()) + val completeFile = Storage.getFromPath(track.getCompleteFile()) + + completeFile?.let { + postState(track, DownloadState.DONE) + false + } + pinnedFile?.let { + postState(track, DownloadState.PINNED) + false + } + true + } + } + + private fun setSaveFlagForTracks( + shouldPin: Boolean, + tracks: List + ): List { + // Walk through the tracks. If a track is pinned or complete and needs to be changed + // to the other state, rename it, but don't return it, thereby excluding it from + // further processing. + // If it is neither pinned nor saved, return it, so that it can be processed. + val filteredTracks: List = tracks.map { track -> + val pinnedFile = Storage.getFromPath(track.getPinnedFile()) + val completeFile = Storage.getFromPath(track.getCompleteFile()) + + if (shouldPin) { + pinnedFile?.let { + null + } + completeFile?.let { + Storage.renameOrDeleteIfAlreadyExists(it, track.getPinnedFile()) + postState(track, DownloadState.PINNED) + null + } + } else { + completeFile?.let { + null + } + pinnedFile?.let { + Storage.renameOrDeleteIfAlreadyExists(it, track.getCompleteFile()) + postState(track, DownloadState.DONE) + null + } + } + track + } + + // Update Pinned flag of items in progress + downloadQueue.filter { item -> tracks.any { it.id == item.id } } + .forEach { it.pinned = shouldPin } + tracks.forEach { + activeDownloads[it.id]?.downloadTrack?.pinned = shouldPin + } + tracks.forEach { + failedList[it.id]?.pinned = shouldPin + } + return filteredTracks + } + fun requestStop() { val context = UApp.applicationContext() val intent = Intent(context, DownloadService::class.java) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 55bceca2..2bae06e8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -53,8 +53,16 @@ class DownloadHandler( withContext(Dispatchers.Main) { // If we are just downloading tracks we don't need to add them to the controller when (action) { - DownloadAction.DOWNLOAD -> DownloadService.download(tracksToDownload, false) - DownloadAction.PIN -> DownloadService.download(tracksToDownload, true) + DownloadAction.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) } From a04517de903dc2004129218e57675c67dffce56b Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Mon, 24 Jul 2023 18:57:29 +0000 Subject: [PATCH 13/25] Fix an exception when removeIncompleteTracksFromPlaylist() could be called on the wrong thread. --- .../ultrasonic/service/MediaPlayerManager.kt | 43 ++++++++++--------- .../org/moire/ultrasonic/service/RxBus.kt | 9 +--- 2 files changed, 25 insertions(+), 27 deletions(-) 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 d1ffd067..2a4cbbdc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt @@ -181,19 +181,22 @@ class MediaPlayerManager( createMediaController(onCreated) - rxBusSubscription += RxBus.activeServerChangingObservable.subscribe { oldServer -> - if (oldServer != OFFLINE_DB_ID) { - // When the server changes, the playlist can retain the downloaded songs. - // Incomplete songs should be removed as the new server won't recognise them. - removeIncompleteTracksFromPlaylist() - DownloadService.requestStop() + rxBusSubscription += RxBus.activeServerChangingObservable + // All interaction with the Media3 needs to happen on the main thread + .subscribeOn(RxBus.mainThread()) + .subscribe { oldServer -> + if (oldServer != OFFLINE_DB_ID) { + // When the server changes, the playlist can retain the downloaded songs. + // Incomplete songs should be removed as the new server won't recognise them. + removeIncompleteTracksFromPlaylist() + DownloadService.requestStop() + } + if (controller is JukeboxMediaPlayer) { + // When the server changes, the Jukebox should be released. + // The new server won't understand the jukebox requests of the old one. + switchToLocalPlayer() + } } - if (controller is JukeboxMediaPlayer) { - // When the server changes, the Jukebox should be released. - // The new server won't understand the jukebox requests of the old one. - switchToLocalPlayer() - } - } rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { val jukebox = activeServerProvider.getActiveServer().jukeboxByDefault @@ -204,19 +207,19 @@ class MediaPlayerManager( isJukeboxEnabled = jukebox } - rxBusSubscription += RxBus.throttledPlaylistObservable.subscribe { - // Even though Rx should launch on the main thread it doesn't always :( - mainScope.launch { + rxBusSubscription += RxBus.throttledPlaylistObservable + // All interaction with the Media3 needs to happen on the main thread + .subscribeOn(RxBus.mainThread()) + .subscribe { serializeCurrentSession() } - } - rxBusSubscription += RxBus.throttledPlayerStateObservable.subscribe { - // Even though Rx should launch on the main thread it doesn't always :( - mainScope.launch { + rxBusSubscription += RxBus.throttledPlayerStateObservable + // All interaction with the Media3 needs to happen on the main thread + .subscribeOn(RxBus.mainThread()) + .subscribe { serializeCurrentSession() } - } rxBusSubscription += RxBus.shutdownCommandObservable.subscribe { clear(false) 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 9c87c519..9912cf0d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -1,8 +1,8 @@ package org.moire.ultrasonic.service -import android.os.Looper import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Scheduler import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.subjects.PublishSubject @@ -12,14 +12,9 @@ import org.moire.ultrasonic.domain.Track class RxBus { - /* - * TODO: mainThread() seems to be not equal to the "normal" main Thread, so it causes - * a lot of often unnecessary thread switching. It looks like observeOn can actually - * be removed in many cases - */ companion object { - private fun mainThread() = AndroidSchedulers.from(Looper.getMainLooper()) + fun mainThread(): Scheduler = AndroidSchedulers.mainThread() val shufflePlayPublisher: PublishSubject = PublishSubject.create() From 77c1329be58a70c7a850b25b9446993c14d502b0 Mon Sep 17 00:00:00 2001 From: Alex Katlein Date: Mon, 24 Jul 2023 19:20:21 +0000 Subject: [PATCH 14/25] Added custom buttons for shuffling the current queue and setting repeat mode --- .../playback/AutoMediaBrowserCallback.kt | 345 ++++++++++++++---- .../ultrasonic/playback/PlaybackService.kt | 12 +- ultrasonic/src/main/res/drawable/empty.xml | 7 + 3 files changed, 285 insertions(+), 79 deletions(-) create mode 100644 ultrasonic/src/main/res/drawable/empty.xml diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index d6b375b5..291dc5fe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -17,6 +17,7 @@ import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES +import androidx.media3.common.Player import androidx.media3.common.Rating import androidx.media3.common.StarRating import androidx.media3.session.CommandButton @@ -116,22 +117,59 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : private val isOffline get() = ActiveServerProvider.isOffline() private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId - private var customCommands: List - internal var customLayout = ImmutableList.of() + private val placeholderButton = getPlaceholderButton() + + private var heartIsCurrentlyOn = false + + // This button is used for an unstarred track, and its action will star the track + private val heartButtonToggleOn = + getHeartCommandButton( + SessionCommand( + PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON, + Bundle.EMPTY + ), + willHeart = true + ) + + // This button is used for an starred track, and its action will star the track + private val heartButtonToggleOff = + getHeartCommandButton( + SessionCommand( + PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF, + Bundle.EMPTY + ), + willHeart = false + ) + + private val shuffleButton: CommandButton + + private val repeatOffButton: CommandButton + private val repeatOneButton: CommandButton + private val repeatAllButton: CommandButton + + private val allCustomCommands: List + + val defaultCustomCommands: List init { - customCommands = - listOf( - // This button is used for an unstarred track, and its action will star the track - getHeartCommandButton( - SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON, Bundle.EMPTY) - ), - // This button is used for an starred track, and its action will unstar the track - getHeartCommandButton( - SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF, Bundle.EMPTY) - ) - ) - customLayout = ImmutableList.of(customCommands[0]) + val shuffleCommand = SessionCommand(PlaybackService.CUSTOM_COMMAND_SHUFFLE, Bundle.EMPTY) + shuffleButton = getShuffleCommandButton(shuffleCommand) + + val repeatCommand = SessionCommand(PlaybackService.CUSTOM_COMMAND_REPEAT_MODE, Bundle.EMPTY) + repeatOffButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_OFF) + repeatOneButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_ONE) + repeatAllButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_ALL) + + allCustomCommands = listOf( + heartButtonToggleOn, + heartButtonToggleOff, + shuffleButton, + repeatOffButton, + repeatOneButton, + repeatAllButton + ) + + defaultCustomCommands = listOf(heartButtonToggleOn, shuffleButton, repeatOffButton) } /** @@ -188,14 +226,17 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : controller: MediaSession.ControllerInfo ): MediaSession.ConnectionResult { Timber.i("onConnect") + val connectionResult = super.onConnect(session, controller) val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() - for (commandButton in customCommands) { + for (commandButton in allCustomCommands) { // Add custom command to available session commands. commandButton.sessionCommand?.let { availableSessionCommands.add(it) } } + session.player.repeatMode = Player.REPEAT_MODE_ALL + return MediaSession.ConnectionResult.accept( availableSessionCommands.build(), connectionResult.availablePlayerCommands @@ -203,26 +244,72 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : } override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { - if (!customLayout.isEmpty() && controller.controllerVersion != 0) { + if (controller.controllerVersion != 0) { // Let Media3 controller (for instance the MediaNotificationProvider) // know about the custom layout right after it connected. - session.setCustomLayout(customLayout) + with(session) { + setCustomLayout(session.buildCustomCommands(canShuffle = canShuffle())) + } } } - private fun getHeartCommandButton(sessionCommand: SessionCommand): CommandButton { - val willHeart = - (sessionCommand.customAction == PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON) - return CommandButton.Builder() - .setDisplayName("Love") + private fun getHeartCommandButton(sessionCommand: SessionCommand, willHeart: Boolean) = + CommandButton.Builder() + .setDisplayName( + if (willHeart) + "Love" + else + "Dislike" + ) .setIconResId( - if (willHeart) R.drawable.ic_star_hollow - else R.drawable.ic_star_full + if (willHeart) + R.drawable.ic_star_hollow + else + R.drawable.ic_star_full + ) + .setSessionCommand(sessionCommand) + .setEnabled(true) + .build() + + private fun getShuffleCommandButton(sessionCommand: SessionCommand) = + CommandButton.Builder() + .setDisplayName("Shuffle") + .setIconResId(R.drawable.media_shuffle) + .setSessionCommand(sessionCommand) + .setEnabled(true) + .build() + + private fun getPlaceholderButton() = CommandButton.Builder() + .setDisplayName("Placeholder") + .setIconResId(R.drawable.empty) + .setSessionCommand( + SessionCommand( + PlaybackService.CUSTOM_COMMAND_PLACEHOLDER, + Bundle.EMPTY + ) + ) + .setEnabled(false) + .build() + + private fun getRepeatModeButton(sessionCommand: SessionCommand, repeatMode: Int) = + CommandButton.Builder() + .setDisplayName( + when (repeatMode) { + Player.REPEAT_MODE_ONE -> "Repeat One" + Player.REPEAT_MODE_ALL -> "Repeat All" + else -> "Repeat None" + } + ) + .setIconResId( + when (repeatMode) { + Player.REPEAT_MODE_ONE -> R.drawable.media_repeat_one + Player.REPEAT_MODE_ALL -> R.drawable.media_repeat_all + else -> R.drawable.media_repeat_off + } ) .setSessionCommand(sessionCommand) .setEnabled(true) .build() - } override fun onGetItem( session: MediaLibraryService.MediaLibrarySession, @@ -266,18 +353,30 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : customCommand: SessionCommand, args: Bundle ): ListenableFuture { - Timber.i("onCustomCommand") + Timber.i("onCustomCommand %s", customCommand.customAction) var customCommandFuture: ListenableFuture? = null when (customCommand.customAction) { PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON -> { customCommandFuture = onSetRating(session, controller, HeartRating(true)) - updateCustomHeartButton(session, true) + updateCustomHeartButton(session, isHeart = true) } PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF -> { customCommandFuture = onSetRating(session, controller, HeartRating(false)) - updateCustomHeartButton(session, false) + updateCustomHeartButton(session, isHeart = false) + } + + PlaybackService.CUSTOM_COMMAND_SHUFFLE -> { + customCommandFuture = Futures.immediateFuture(SessionResult(RESULT_SUCCESS)) + shuffleCurrentPlaylist(session.player) + } + + PlaybackService.CUSTOM_COMMAND_REPEAT_MODE -> { + customCommandFuture = Futures.immediateFuture(SessionResult(RESULT_SUCCESS)) + + session.player.setNextRepeatMode() + session.updateCustomCommands() } else -> { @@ -288,9 +387,14 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : ) } } - if (customCommandFuture != null) - return customCommandFuture - return super.onCustomCommand(session, controller, customCommand, args) + + return customCommandFuture + ?: super.onCustomCommand( + session, + controller, + customCommand, + args + ) } override fun onSetRating( @@ -299,6 +403,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : rating: Rating ): ListenableFuture { val mediaItem = session.player.currentMediaItem + if (mediaItem != null) { if (rating is HeartRating) { mediaItem.toTrack().starred = rating.isHeart @@ -312,6 +417,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : rating ) } + return super.onSetRating(session, controller, rating) } @@ -381,6 +487,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : private fun onAddLegacyAutoItems( mediaItems: MutableList ): ListenableFuture> { + Timber.i("onAddLegacyAutoItems %s", mediaItems.first().mediaId) + val mediaIdParts = mediaItems.first().mediaId.split('|') val tracks = when (mediaIdParts.first()) { @@ -410,55 +518,54 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : else -> null } - if (tracks != null) { - return Futures.immediateFuture( - tracks.map { track -> track.toMediaItem() } - .toMutableList() - ) - } - - // Fallback to the original list - return Futures.immediateFuture(mediaItems) + return tracks + ?.let { + Futures.immediateFuture( + it.map { track -> track.toMediaItem() } + .toMutableList() + ) + } + ?: Futures.immediateFuture(mediaItems) } - @Suppress("ReturnCount", "ComplexMethod") - fun onLoadChildren( + @Suppress("ComplexMethod") + private fun onLoadChildren( parentId: String, ): ListenableFuture>> { Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId) val parentIdParts = parentId.split('|') - when (parentIdParts.first()) { - MEDIA_ROOT_ID -> return getRootItems() - MEDIA_LIBRARY_ID -> return getLibrary() - MEDIA_ARTIST_ID -> return getArtists() - MEDIA_ARTIST_SECTION -> return getArtists(parentIdParts[1]) - MEDIA_ALBUM_ID -> return getAlbums(AlbumListType.SORTED_BY_NAME) - MEDIA_ALBUM_PAGE_ID -> return getAlbums( + return when (parentIdParts.first()) { + MEDIA_ROOT_ID -> getRootItems() + MEDIA_LIBRARY_ID -> getLibrary() + MEDIA_ARTIST_ID -> getArtists() + MEDIA_ARTIST_SECTION -> getArtists(parentIdParts[1]) + MEDIA_ALBUM_ID -> getAlbums(AlbumListType.SORTED_BY_NAME) + MEDIA_ALBUM_PAGE_ID -> getAlbums( AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt() ) - MEDIA_PLAYLIST_ID -> return getPlaylists() - MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(AlbumListType.FREQUENT) - MEDIA_ALBUM_NEWEST_ID -> return getAlbums(AlbumListType.NEWEST) - MEDIA_ALBUM_RECENT_ID -> return getAlbums(AlbumListType.RECENT) - MEDIA_ALBUM_RANDOM_ID -> return getAlbums(AlbumListType.RANDOM) - MEDIA_ALBUM_STARRED_ID -> return getAlbums(AlbumListType.STARRED) - MEDIA_SONG_RANDOM_ID -> return getRandomSongs() - MEDIA_SONG_STARRED_ID -> return getStarredSongs() - MEDIA_SHARE_ID -> return getShares() - MEDIA_BOOKMARK_ID -> return getBookmarks() - MEDIA_PODCAST_ID -> return getPodcasts() - MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2]) - MEDIA_ARTIST_ITEM -> return getAlbumsForArtist( + MEDIA_PLAYLIST_ID -> getPlaylists() + MEDIA_ALBUM_FREQUENT_ID -> getAlbums(AlbumListType.FREQUENT) + MEDIA_ALBUM_NEWEST_ID -> getAlbums(AlbumListType.NEWEST) + MEDIA_ALBUM_RECENT_ID -> getAlbums(AlbumListType.RECENT) + MEDIA_ALBUM_RANDOM_ID -> getAlbums(AlbumListType.RANDOM) + MEDIA_ALBUM_STARRED_ID -> getAlbums(AlbumListType.STARRED) + MEDIA_SONG_RANDOM_ID -> getRandomSongs() + MEDIA_SONG_STARRED_ID -> getStarredSongs() + MEDIA_SHARE_ID -> getShares() + MEDIA_BOOKMARK_ID -> getBookmarks() + MEDIA_PODCAST_ID -> getPodcasts() + MEDIA_PLAYLIST_ITEM -> getPlaylist(parentIdParts[1], parentIdParts[2]) + MEDIA_ARTIST_ITEM -> getAlbumsForArtist( parentIdParts[1], parentIdParts[2] ) - MEDIA_ALBUM_ITEM -> return getSongsForAlbum(parentIdParts[1], parentIdParts[2]) - MEDIA_SHARE_ITEM -> return getSongsForShare(parentIdParts[1]) - MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(parentIdParts[1]) - else -> return Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null)) + MEDIA_ALBUM_ITEM -> getSongsForAlbum(parentIdParts[1], parentIdParts[2]) + MEDIA_SHARE_ITEM -> getSongsForShare(parentIdParts[1]) + MEDIA_PODCAST_ITEM -> getPodcastEpisodes(parentIdParts[1]) + else -> Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null)) } } @@ -1316,14 +1423,102 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : } } - fun updateCustomHeartButton( - session: MediaSession, - isHeart: Boolean - ) { - val command = if (isHeart) customCommands[1] else customCommands[0] - // Change the custom layout to contain the right heart button - customLayout = ImmutableList.of(command) - // Send the updated custom layout to controllers. - session.setCustomLayout(customLayout) + private fun Player.setNextRepeatMode() { + repeatMode = + when (repeatMode) { + Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ALL + Player.REPEAT_MODE_ALL -> Player.REPEAT_MODE_ONE + else -> Player.REPEAT_MODE_OFF + } + } + + private fun MediaSession.updateCustomCommands() { + setCustomLayout( + buildCustomCommands( + heartIsCurrentlyOn, + canShuffle() + ) + ) + } + + fun updateCustomHeartButton(session: MediaSession, isHeart: Boolean) { + with(session) { + setCustomLayout( + buildCustomCommands( + isHeart = isHeart, + canShuffle = canShuffle() + ) + ) + } + } + + private fun MediaSession.canShuffle() = + player.mediaItemCount > 2 + + private fun MediaSession.buildCustomCommands( + isHeart: Boolean = false, + canShuffle: Boolean = false + ): ImmutableList { + Timber.d("building custom commands (isHeart = %s, canShuffle = %s)", isHeart, canShuffle) + + heartIsCurrentlyOn = isHeart + + return ImmutableList.copyOf( + buildList { + // placeholder must come first here because if there is no next button the first + // custom command button is place right next to the play/pause button + if ( + player.repeatMode != Player.REPEAT_MODE_ALL && + player.currentMediaItemIndex == player.mediaItemCount - 1 + ) + add(placeholderButton) + + // due to the previous placeholder this heart button will always appear to the left + // of the default playback items + add( + if (isHeart) + heartButtonToggleOff + else + heartButtonToggleOn + ) + + // both the shuffle and the active repeat mode button will end up in the overflow + // menu if both are available at the same time + if (canShuffle) + add(shuffleButton) + + add( + when (player.repeatMode) { + Player.REPEAT_MODE_ONE -> repeatOneButton + Player.REPEAT_MODE_ALL -> repeatAllButton + else -> repeatOffButton + } + ) + }.asIterable() + ) + } + + private fun shuffleCurrentPlaylist(player: Player) { + Timber.d("shuffleCurrentPlaylist") + + // 3 was chosen because that leaves at least two other songs to be shuffled around + @Suppress("MagicNumber") + if (player.mediaItemCount < 3) + return + + val mediaItemsToShuffle = mutableListOf() + + for (i in 0 until player.currentMediaItemIndex) { + mediaItemsToShuffle += player.getMediaItemAt(i) + } + + for (i in player.currentMediaItemIndex + 1 until player.mediaItemCount) { + mediaItemsToShuffle += player.getMediaItemAt(i) + } + + player.removeMediaItems(player.currentMediaItemIndex + 1, player.mediaItemCount) + player.removeMediaItems(0, player.currentMediaItemIndex) + + player.addMediaItems(mediaItemsToShuffle.shuffled()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index e58a6799..40b57843 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -150,10 +150,8 @@ class PlaybackService : .setBitmapLoader(ArtworkBitmapLoader()) .build() - if (!librarySessionCallback.customLayout.isEmpty()) { - // Send custom layout to legacy session. - mediaLibrarySession.setCustomLayout(librarySessionCallback.customLayout) - } + // Send custom layout to legacy session. + mediaLibrarySession.setCustomLayout(librarySessionCallback.defaultCustomCommands) // Set a listener to update the API client when the active server has changed rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { @@ -422,6 +420,12 @@ class PlaybackService : "org.moire.ultrasonic.HEART_ON" const val CUSTOM_COMMAND_TOGGLE_HEART_OFF = "org.moire.ultrasonic.HEART_OFF" + const val CUSTOM_COMMAND_SHUFFLE = + "org.moire.ultrasonic.SHUFFLE" + const val CUSTOM_COMMAND_PLACEHOLDER = + "org.moire.ultrasonic.PLACEHOLDER" + const val CUSTOM_COMMAND_REPEAT_MODE = + "org.moire.ultrasonic.REPEAT_MODE" private const val NOTIFICATION_ID = 3009 } } diff --git a/ultrasonic/src/main/res/drawable/empty.xml b/ultrasonic/src/main/res/drawable/empty.xml new file mode 100644 index 00000000..a4762a86 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/empty.xml @@ -0,0 +1,7 @@ + + + + From 0492b0fa6ff234636e686cd55c92cd5d190c7e6d Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Mon, 24 Jul 2023 21:08:01 +0000 Subject: [PATCH 15/25] Modernize code after media3 1.1.0 update --- .../imageloader/ArtworkBitmapLoader.kt | 2 +- .../playback/AutoMediaBrowserCallback.kt | 96 +++++++++---------- .../ultrasonic/playback/PlaybackService.kt | 2 +- .../ultrasonic/service/JukeboxMediaPlayer.kt | 8 +- .../ultrasonic/util/MediaItemConverter.kt | 28 ++---- 5 files changed, 62 insertions(+), 74 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ArtworkBitmapLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ArtworkBitmapLoader.kt index b7b6c15a..29bb2e16 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ArtworkBitmapLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ArtworkBitmapLoader.kt @@ -11,7 +11,7 @@ import android.annotation.SuppressLint import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri -import androidx.media3.session.BitmapLoader +import androidx.media3.common.util.BitmapLoader import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListeningExecutorService import com.google.common.util.concurrent.MoreExecutors diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index 291dc5fe..d7abb1bb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -7,16 +7,15 @@ package org.moire.ultrasonic.playback -import android.annotation.SuppressLint import android.os.Bundle import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES +import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS +import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS +import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_MIXED +import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS +import androidx.media3.common.MediaMetadata.MEDIA_TYPE_MIXED +import androidx.media3.common.MediaMetadata.MEDIA_TYPE_PLAYLIST import androidx.media3.common.Player import androidx.media3.common.Rating import androidx.media3.common.StarRating @@ -46,7 +45,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.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.RatingManager import org.moire.ultrasonic.util.Util @@ -97,11 +95,8 @@ const val PLAY_COMMAND = "play " * MediaBrowserService implementation for e.g. Android Auto */ @Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember") -@SuppressLint("UnsafeOptInUsageError") -class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : - MediaLibraryService.MediaLibrarySession.Callback, KoinComponent { +class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callback, KoinComponent { - private val mediaPlayerManager by inject() private val activeServerProvider: ActiveServerProvider by inject() private val serviceJob = SupervisorJob() @@ -213,8 +208,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : "Root Folder", MEDIA_ROOT_ID, isPlayable = false, - folderType = FOLDER_TYPE_MIXED, - mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED + isBrowsable = true, + mediaType = MEDIA_TYPE_FOLDER_MIXED ), params ) @@ -528,7 +523,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : ?: Futures.immediateFuture(mediaItems) } - @Suppress("ComplexMethod") + @Suppress("ReturnCount", "ComplexMethod") private fun onLoadChildren( parentId: String, ): ListenableFuture>> { @@ -598,8 +593,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : mediaItems.add( album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) - .joinToString("|"), - FOLDER_TYPE_ALBUMS + .joinToString("|") ) } @@ -691,7 +685,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : R.string.music_library_label, MEDIA_LIBRARY_ID, null, - folderType = FOLDER_TYPE_MIXED, + isBrowsable = true, + mediaType = MEDIA_TYPE_FOLDER_MIXED, icon = R.drawable.ic_library ) @@ -699,7 +694,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : R.string.main_artists_title, MEDIA_ARTIST_ID, null, - folderType = FOLDER_TYPE_ARTISTS, + isBrowsable = true, + mediaType = MEDIA_TYPE_FOLDER_ARTISTS, icon = R.drawable.ic_artist ) @@ -708,7 +704,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : R.string.main_albums_title, MEDIA_ALBUM_ID, null, - folderType = FOLDER_TYPE_ALBUMS, + isBrowsable = true, + mediaType = MEDIA_TYPE_FOLDER_ALBUMS, icon = R.drawable.ic_menu_browse ) @@ -716,7 +713,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : R.string.playlist_label, MEDIA_PLAYLIST_ID, null, - folderType = FOLDER_TYPE_PLAYLISTS, + isBrowsable = true, + mediaType = MEDIA_TYPE_FOLDER_PLAYLISTS, icon = R.drawable.ic_menu_playlists ) @@ -731,14 +729,16 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : R.string.main_songs_random, MEDIA_SONG_RANDOM_ID, R.string.main_songs_title, - folderType = FOLDER_TYPE_TITLES + isBrowsable = true, + mediaType = MEDIA_TYPE_PLAYLIST ) mediaItems.add( R.string.main_songs_starred, MEDIA_SONG_STARRED_ID, R.string.main_songs_title, - folderType = FOLDER_TYPE_TITLES + isBrowsable = true, + mediaType = MEDIA_TYPE_PLAYLIST ) // Albums @@ -752,28 +752,28 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : R.string.main_albums_recent, MEDIA_ALBUM_RECENT_ID, R.string.main_albums_title, - folderType = FOLDER_TYPE_ALBUMS + mediaType = MEDIA_TYPE_FOLDER_ALBUMS, ) mediaItems.add( R.string.main_albums_frequent, MEDIA_ALBUM_FREQUENT_ID, R.string.main_albums_title, - folderType = FOLDER_TYPE_ALBUMS + mediaType = MEDIA_TYPE_FOLDER_ALBUMS, ) mediaItems.add( R.string.main_albums_random, MEDIA_ALBUM_RANDOM_ID, R.string.main_albums_title, - folderType = FOLDER_TYPE_ALBUMS + mediaType = MEDIA_TYPE_FOLDER_ALBUMS, ) mediaItems.add( R.string.main_albums_starred, MEDIA_ALBUM_STARRED_ID, R.string.main_albums_title, - folderType = FOLDER_TYPE_ALBUMS + mediaType = MEDIA_TYPE_FOLDER_ALBUMS, ) // Other @@ -822,8 +822,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : index.add(currentSection) mediaItems.add( currentSection, - listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"), - FOLDER_TYPE_ARTISTS + listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|") ) } } @@ -831,8 +830,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : artists.map { artist -> mediaItems.add( artist.name ?: "", - listOf(childMediaId, artist.id, artist.name).joinToString("|"), - FOLDER_TYPE_ARTISTS + listOf(childMediaId, artist.id, artist.name).joinToString("|") ) } } @@ -862,8 +860,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : mediaItems.add( album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) - .joinToString("|"), - FOLDER_TYPE_ALBUMS + .joinToString("|") ) } return@future LibraryResult.ofItemList(mediaItems, null) @@ -901,8 +898,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : if (item.isDirectory) mediaItems.add( item.title ?: "", - listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|"), - FOLDER_TYPE_TITLES + listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|") ) else if (item is Track) mediaItems.add( @@ -951,8 +947,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : mediaItems.add( album.title ?: "", listOf(MEDIA_ALBUM_ITEM, album.id, album.name) - .joinToString("|"), - FOLDER_TYPE_ALBUMS + .joinToString("|") ) } @@ -980,7 +975,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : playlist.name, listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name) .joinToString("|"), - FOLDER_TYPE_PLAYLISTS + mediaType = MEDIA_TYPE_PLAYLIST, ) } return@future LibraryResult.ofItemList(mediaItems, null) @@ -1074,7 +1069,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : mediaItems.add( podcast.title ?: "", listOf(MEDIA_PODCAST_ITEM, podcast.id).joinToString("|"), - FOLDER_TYPE_MIXED + mediaType = MEDIA_TYPE_FOLDER_MIXED, ) } return@future LibraryResult.ofItemList(mediaItems, null) @@ -1177,7 +1172,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : share.name ?: "", listOf(MEDIA_SHARE_ITEM, share.id) .joinToString("|"), - FOLDER_TYPE_MIXED + mediaType = MEDIA_TYPE_FOLDER_MIXED, ) } return@future LibraryResult.ofItemList(mediaItems, null) @@ -1355,14 +1350,16 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : private fun MutableList.add( title: String, mediaId: String, - folderType: Int + mediaType: Int = MEDIA_TYPE_MIXED, + isBrowsable: Boolean = false ) { val mediaItem = buildMediaItem( title, mediaId, isPlayable = false, - folderType = folderType + isBrowsable = isBrowsable, + mediaType = mediaType ) this.add(mediaItem) @@ -1373,8 +1370,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : resId: Int, mediaId: String, groupNameId: Int?, - browsable: Boolean = true, - folderType: Int = FOLDER_TYPE_MIXED, + isBrowsable: Boolean = true, + mediaType: Int = MEDIA_TYPE_FOLDER_MIXED, icon: Int? = null ) { val applicationContext = UApp.applicationContext() @@ -1382,14 +1379,15 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : val mediaItem = buildMediaItem( applicationContext.getString(resId), mediaId, - isPlayable = !browsable, - folderType = folderType, + isPlayable = !isBrowsable, + isBrowsable = isBrowsable, + imageUri = if (icon != null) { + Util.getUriToDrawable(applicationContext, icon) + } else null, group = if (groupNameId != null) { applicationContext.getString(groupNameId) } else null, - imageUri = if (icon != null) { - Util.getUriToDrawable(applicationContext, icon) - } else null + mediaType = mediaType ) this.add(mediaItem) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 8ddf5f19..9ec36400 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -142,7 +142,7 @@ class PlaybackService : actualBackend = desiredBackend // Create browser interface - librarySessionCallback = AutoMediaBrowserCallback(this) + librarySessionCallback = AutoMediaBrowserCallback() // This will need to use the AutoCalls mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt index 20894ca6..fb7d3b71 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt @@ -82,7 +82,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { companion object { // This is quite important, by setting the DeviceInfo the player is recognized by // Android as being a remote playback surface - val DEVICE_INFO = DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 10) + val DEVICE_INFO = DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE) + .setMinVolume(0) + .setMaxVolume(10) + .build() val running = AtomicBoolean() const val MAX_GAIN = 10 } @@ -208,15 +211,12 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS, Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS, - Player.COMMAND_ADJUST_DEVICE_VOLUME, - Player.COMMAND_SET_DEVICE_VOLUME ) if (isPlaying) commandsBuilder.add(Player.COMMAND_STOP) if (playlist.isNotEmpty()) { commandsBuilder.addAll( Player.COMMAND_GET_CURRENT_MEDIA_ITEM, Player.COMMAND_GET_METADATA, - Player.COMMAND_GET_MEDIA_ITEMS_METADATA, Player.COMMAND_PLAY_PAUSE, Player.COMMAND_PREPARE, Player.COMMAND_SEEK_BACK, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaItemConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaItemConverter.kt index f7eebd3e..e05957ed 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaItemConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaItemConverter.kt @@ -14,7 +14,8 @@ import androidx.core.net.toUri import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE +import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_MIXED +import androidx.media3.common.MediaMetadata.MEDIA_TYPE_MUSIC import androidx.media3.common.StarRating import java.text.DateFormat import java.text.ParseException @@ -22,7 +23,7 @@ import java.util.Date import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.provider.AlbumArtContentProvider -// Copied from androidx.media.utils.MediaConstants in order to avoid importing a whole dependecy +// Copied from androidx.media.utils.MediaConstants in order to avoid importing a whole dependency // for a single string value private const val DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE = "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT" @@ -76,15 +77,16 @@ fun Track.toMediaItem( title = title ?: "", mediaId = mediaId, isPlayable = !isDirectory, - folderType = if (isDirectory) MediaMetadata.FOLDER_TYPE_TITLES - else MediaMetadata.FOLDER_TYPE_NONE, + isBrowsable = isDirectory, album = album, artist = artist, genre = genre, sourceUri = uri.toUri(), imageUri = artworkUri, starred = starred, - group = null + group = null, + mediaType = if (isDirectory) MEDIA_TYPE_FOLDER_MIXED + else MEDIA_TYPE_MUSIC ) val metadataBuilder = mediaItem.mediaMetadata.buildUpon() @@ -204,14 +206,6 @@ private fun safeParseDate(created: String?): Date? { } else null } -fun MediaItem.setPin(pin: Boolean) { - this.mediaMetadata.extras?.putBoolean("pin", pin) -} - -fun MediaItem.shouldBePinned(): Boolean { - return this.mediaMetadata.extras?.getBoolean("pin") ?: false -} - /** * Build a new MediaItem from a list of attributes. * Especially useful to create folder entries in the Auto interface. @@ -222,7 +216,7 @@ fun buildMediaItem( title: String, mediaId: String, isPlayable: Boolean, - folderType: @MediaMetadata.FolderType Int, + isBrowsable: Boolean = false, album: String? = null, artist: String? = null, genre: String? = null, @@ -241,17 +235,13 @@ fun buildMediaItem( .setAlbumArtist(artist) .setGenre(genre) .setUserRating(HeartRating(starred)) - .setFolderType(folderType) + .setIsBrowsable(isBrowsable) .setIsPlayable(isPlayable) if (imageUri != null) { metadataBuilder.setArtworkUri(imageUri) } - if (folderType > FOLDER_TYPE_NONE) { - metadataBuilder.setIsBrowsable(true) - } - if (mediaType != null) { metadataBuilder.setMediaType(mediaType) } From 509e33ac20a808fa62b37c83b431ebab465a3ab1 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 25 Jul 2023 16:31:46 +0000 Subject: [PATCH 16/25] Update kotlinxCoroutines to v1.7.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b48afdce..eff41024 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.5.2" kotlin = "1.8.22" -kotlinxCoroutines = "1.7.2" +kotlinxCoroutines = "1.7.3" viewModelKtx = "2.6.1" swipeRefresh = "1.1.0" From 06496ddf3791607fc8f6b84acc3d90cafdf90382 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 26 Jul 2023 17:31:46 +0000 Subject: [PATCH 17/25] Update dependency androidx.preference:preference to v1.2.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b48afdce..5b0eafc7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ androidxcore = "1.10.1" ktlint = "0.43.2" ktlintGradle = "11.5.0" detekt = "1.23.0" -preferences = "1.2.0" +preferences = "1.2.1" media3 = "1.1.0" androidSupport = "1.6.0" From 1f6df202b9248283b95e083140a7312ab9c70c0c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 28 Jul 2023 08:31:49 +0000 Subject: [PATCH 18/25] Update koin to v3.4.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b48afdce..bdefc21f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ retrofit = "2.9.0" ## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24 jackson = "2.13.5" okhttp = "4.11.0" -koin = "3.4.2" +koin = "3.4.3" picasso = "2.8" junit4 = "4.13.2" From f89b2da30d7f5b989c6d0f1093607dab21747c42 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 31 Jul 2023 12:31:26 +0200 Subject: [PATCH 19/25] More more functions to InsertionMode pattern, show a Toast when adding tracks to the playlist. --- .../ultrasonic/activity/NavigationActivity.kt | 3 +- .../ultrasonic/adapters/HeaderViewBinder.kt | 2 +- .../ultrasonic/fragment/BookmarksFragment.kt | 2 +- .../ultrasonic/fragment/EntryListFragment.kt | 10 +- .../ultrasonic/fragment/SearchFragment.kt | 17 ++-- .../fragment/TrackCollectionFragment.kt | 94 +++++++++---------- .../fragment/legacy/SharesFragment.kt | 9 +- .../ultrasonic/subsonic/DownloadHandler.kt | 55 +++++------ ultrasonic/src/main/res/values-cs/strings.xml | 2 +- ultrasonic/src/main/res/values-de/strings.xml | 14 +-- ultrasonic/src/main/res/values-es/strings.xml | 14 +-- ultrasonic/src/main/res/values-fr/strings.xml | 14 +-- ultrasonic/src/main/res/values-gl/strings.xml | 4 +- ultrasonic/src/main/res/values-hu/strings.xml | 2 +- ultrasonic/src/main/res/values-ja/strings.xml | 14 +-- .../src/main/res/values-nb-rNO/strings.xml | 14 +-- ultrasonic/src/main/res/values-nl/strings.xml | 14 +-- ultrasonic/src/main/res/values-pl/strings.xml | 14 +-- .../src/main/res/values-pt-rBR/strings.xml | 14 +-- ultrasonic/src/main/res/values-pt/strings.xml | 14 +-- ultrasonic/src/main/res/values-ru/strings.xml | 2 +- .../src/main/res/values-zh-rCN/strings.xml | 14 +-- ultrasonic/src/main/res/values/strings.xml | 18 ++-- 23 files changed, 177 insertions(+), 183 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 160677b1..4cadda0d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -488,8 +488,7 @@ class NavigationActivity : AppCompatActivity() { val downloadHandler: DownloadHandler by inject() downloadHandler.addTracksToMediaController( songs = musicDirectory.getTracks(), - append = false, - playNext = false, + insertionMode = MediaPlayerManager.InsertionMode.CLEAR, autoPlay = true, shuffle = false, fragment = currentFragment, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt index c2d25b0c..48e89673 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt @@ -98,7 +98,7 @@ class HeaderViewBinder( holder.yearView.text = year val songs = resources.getQuantityString( - R.plurals.select_album_n_songs, item.childCount, + R.plurals.n_songs, item.childCount, item.childCount ) holder.songCountView.text = songs 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 5f0ef215..b959742e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -56,7 +56,7 @@ class BookmarksFragment : TrackCollectionFragment() { super.setupButtons(view) playNowButton!!.setOnClickListener { - playNow(getSelectedSongs()) + playNow(getSelectedTracks()) } } 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 48815367..075b3e31 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -20,6 +20,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.GenericEntry 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 @@ -133,27 +134,24 @@ abstract class EntryListFragment : MultiListFragment() { downloadHandler.fetchTracksAndAddToController( fragment, item.id, - append = false, + insertionMode = MediaPlayerManager.InsertionMode.CLEAR, autoPlay = true, - playNext = false, isArtist = isArtist ) R.id.menu_play_next -> downloadHandler.fetchTracksAndAddToController( fragment, item.id, - append = false, + insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT, autoPlay = true, - playNext = true, isArtist = isArtist ) R.id.menu_play_last -> downloadHandler.fetchTracksAndAddToController( fragment, item.id, - append = true, + insertionMode = MediaPlayerManager.InsertionMode.APPEND, autoPlay = false, - playNext = false, isArtist = isArtist ) R.id.menu_pin -> 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 4d2ffe2f..40d71cf3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -253,7 +253,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { insertionMode = MediaPlayerManager.InsertionMode.APPEND ) mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1) - toast(context, resources.getQuantityString(R.plurals.select_album_n_songs_added, 1, 1)) + toast(context, resources.getQuantityString(R.plurals.n_songs_added_to_end, 1, 1)) } private fun onVideoSelected(track: Track) { @@ -307,8 +307,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { songs.add(item) downloadHandler.addTracksToMediaController( songs = songs, - append = false, - playNext = false, + insertionMode = MediaPlayerManager.InsertionMode.CLEAR, autoPlay = true, shuffle = false, fragment = this, @@ -319,8 +318,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { songs.add(item) downloadHandler.addTracksToMediaController( songs = songs, - append = true, - playNext = true, + insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT, autoPlay = false, shuffle = false, fragment = this, @@ -331,8 +329,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { songs.add(item) downloadHandler.addTracksToMediaController( songs = songs, - append = true, - playNext = false, + insertionMode = MediaPlayerManager.InsertionMode.APPEND, autoPlay = false, shuffle = false, fragment = this, @@ -344,7 +341,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { toast( context, resources.getQuantityString( - R.plurals.select_album_n_songs_pinned, + R.plurals.n_songs_pinned, songs.size, songs.size ) @@ -356,7 +353,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { toast( context, resources.getQuantityString( - R.plurals.select_album_n_songs_downloaded, + R.plurals.n_songs_to_be_downloaded, songs.size, songs.size ) @@ -368,7 +365,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { toast( context, resources.getQuantityString( - R.plurals.select_album_n_songs_unpinned, + R.plurals.n_songs_unpinned, songs.size, songs.size ) 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 b16fe044..7d076d83 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -159,7 +159,7 @@ open class TrackCollectionFragment( // Change the buttons if the status of any selected track changes rxBusSubscription += RxBus.trackDownloadStateObservable.subscribe { if (it.progress != null) return@subscribe - val selectedSongs = getSelectedSongs() + val selectedSongs = getSelectedTracks() if (!selectedSongs.any { song -> song.id == it.id }) return@subscribe triggerButtonUpdate(selectedSongs) } @@ -211,23 +211,15 @@ open class TrackCollectionFragment( } playNowButton?.setOnClickListener { - playNow(false) + playNow(MediaPlayerManager.InsertionMode.CLEAR, toast = true) } playNextButton?.setOnClickListener { - downloadHandler.addTracksToMediaController( - songs = getSelectedSongs(), - append = true, - playNext = true, - autoPlay = false, - shuffle = false, - playlistName = navArgs.playlistName, - this@TrackCollectionFragment - ) + playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, toast = true) } playLastButton!!.setOnClickListener { - playNow(true) + playNow(MediaPlayerManager.InsertionMode.APPEND, toast = true) } pinButton?.setOnClickListener { @@ -291,7 +283,7 @@ open class TrackCollectionFragment( return true } else if (item.itemId == R.id.menu_item_share) { shareHandler.createShare( - this@TrackCollectionFragment, getSelectedSongs(), + this@TrackCollectionFragment, getSelectedTracks(), refreshListView, cancellationToken!!, navArgs.id ) @@ -308,20 +300,37 @@ open class TrackCollectionFragment( } private fun playNow( - append: Boolean, - selectedSongs: List = getSelectedSongs() + insertionMode: MediaPlayerManager.InsertionMode, + selectedTracks: List = getSelectedTracks(), + toast: Boolean = false ) { - if (selectedSongs.isNotEmpty()) { + if (selectedTracks.isNotEmpty()) { downloadHandler.addTracksToMediaController( - songs = selectedSongs, - append = append, - playNext = false, - autoPlay = !append, + songs = selectedTracks, + insertionMode = insertionMode, + autoPlay = (insertionMode == MediaPlayerManager.InsertionMode.CLEAR), playlistName = null, fragment = this ) } else { - playAll(false, append) + 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) } } @@ -338,7 +347,10 @@ open class TrackCollectionFragment( } } - private fun playAll(shuffle: Boolean = false, append: Boolean = false) { + private fun playAll( + shuffle: Boolean = false, + insertionMode: MediaPlayerManager.InsertionMode = MediaPlayerManager.InsertionMode.CLEAR + ) { var hasSubFolders = false for (item in viewAdapter.getCurrentList()) { @@ -355,18 +367,16 @@ open class TrackCollectionFragment( downloadHandler.fetchTracksAndAddToController( fragment = this, id = navArgs.id!!, - append = append, - autoPlay = !append, + insertionMode = insertionMode, + autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND), shuffle = shuffle, - playNext = false, isArtist = isArtist ) } else { downloadHandler.addTracksToMediaController( songs = getAllSongs(), - append = append, - playNext = false, - autoPlay = !append, + insertionMode = insertionMode, + autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND), shuffle = shuffle, playlistName = navArgs.playlistName, fragment = this @@ -397,7 +407,7 @@ open class TrackCollectionFragment( } @Synchronized - fun triggerButtonUpdate(selection: List = getSelectedSongs()) { + fun triggerButtonUpdate(selection: List = getSelectedTracks()) { listModel.calculateButtonState(selection, ::updateButtonState) } @@ -421,7 +431,7 @@ open class TrackCollectionFragment( } } - private fun downloadBackground(save: Boolean, tracks: List = getSelectedSongs()) { + private fun downloadBackground(save: Boolean, tracks: List = getSelectedTracks()) { var songs = tracks if (songs.isEmpty()) { @@ -436,7 +446,7 @@ open class TrackCollectionFragment( ) } - internal fun delete(songs: List = getSelectedSongs()) { + internal fun delete(songs: List = getSelectedTracks()) { downloadHandler.justDownload( action = DownloadAction.DELETE, fragment = this, @@ -444,7 +454,7 @@ open class TrackCollectionFragment( ) } - internal fun unpin(songs: List = getSelectedSongs()) { + internal fun unpin(songs: List = getSelectedTracks()) { downloadHandler.justDownload( action = DownloadAction.UNPIN, fragment = this, @@ -502,10 +512,7 @@ open class TrackCollectionFragment( val playAll = navArgs.autoPlay if (playAll && songCount > 0) { - playAll( - navArgs.shuffle, - false - ) + playAll(navArgs.shuffle, MediaPlayerManager.InsertionMode.CLEAR) } listModel.currentListIsSortable = true @@ -513,7 +520,7 @@ open class TrackCollectionFragment( Timber.i("Processed list") } - internal fun getSelectedSongs(): List { + internal fun getSelectedTracks(): List { // Walk through selected set and get the Entries based on the saved ids. return viewAdapter.getCurrentList().mapNotNull { if (it is Track && viewAdapter.isSelected(it.longId)) @@ -608,20 +615,13 @@ open class TrackCollectionFragment( when (menuItem.itemId) { R.id.song_menu_play_now -> { - playNow(false, songs) + playNow(MediaPlayerManager.InsertionMode.CLEAR, songs, true) } R.id.song_menu_play_next -> { - downloadHandler.addTracksToMediaController( - songs = songs, - append = true, - playNext = true, - autoPlay = false, - playlistName = navArgs.playlistName, - fragment = this@TrackCollectionFragment - ) + playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, songs, true) } R.id.song_menu_play_last -> { - playNow(true, songs) + playNow(MediaPlayerManager.InsertionMode.APPEND, songs, true) } R.id.song_menu_pin -> { downloadBackground(true, songs) 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 49c33749..31fa05bd 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 @@ -35,6 +35,7 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.domain.Share 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 @@ -165,10 +166,9 @@ class SharesFragment : Fragment(), KoinComponent { this, share.id, share.name, - append = false, + insertionMode = MediaPlayerManager.InsertionMode.CLEAR, autoPlay = true, - shuffle = false, - playNext = false, + shuffle = false ) } R.id.share_menu_play_shuffled -> { @@ -176,10 +176,9 @@ class SharesFragment : Fragment(), KoinComponent { this, share.id, share.name, - append = false, + insertionMode = MediaPlayerManager.InsertionMode.CLEAR, autoPlay = true, shuffle = true, - playNext = false, ) } R.id.share_menu_delete -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 2bae06e8..207c9141 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -68,27 +68,27 @@ class DownloadHandler( } successString = when (action) { DownloadAction.DOWNLOAD -> fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_downloaded, + R.plurals.n_songs_to_be_downloaded, tracksToDownload.size, tracksToDownload.size ) DownloadAction.UNPIN -> { fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_unpinned, + R.plurals.n_songs_unpinned, tracksToDownload.size, tracksToDownload.size ) } DownloadAction.PIN -> { fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_pinned, + R.plurals.n_songs_pinned, tracksToDownload.size, tracksToDownload.size ) } DownloadAction.DELETE -> { fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_deleted, + R.plurals.n_songs_deleted, tracksToDownload.size, tracksToDownload.size ) @@ -104,10 +104,9 @@ class DownloadHandler( name: String? = "", isShare: Boolean = false, isDirectory: Boolean = true, - append: Boolean, + insertionMode: MediaPlayerManager.InsertionMode, autoPlay: Boolean, shuffle: Boolean = false, - playNext: Boolean, isArtist: Boolean = false ) { var successString: String? = null @@ -119,26 +118,28 @@ class DownloadHandler( withContext(Dispatchers.Main) { addTracksToMediaController( songs = songs, - append = append, - playNext = playNext, + insertionMode = insertionMode, autoPlay = autoPlay, shuffle = shuffle, playlistName = null, fragment = fragment ) + // Play Now doesn't get a Toast :) - if (playNext) { - successString = fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_play_next, - songs.size, - songs.size - ) - } else if (append) { - successString = fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_added, - songs.size, - songs.size - ) + 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 } @@ -146,8 +147,7 @@ class DownloadHandler( fun addTracksToMediaController( songs: List, - append: Boolean, - playNext: Boolean, + insertionMode: MediaPlayerManager.InsertionMode, autoPlay: Boolean, shuffle: Boolean = false, playlistName: String? = null, @@ -157,12 +157,6 @@ class DownloadHandler( networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - val insertionMode = when { - append -> MediaPlayerManager.InsertionMode.APPEND - playNext -> MediaPlayerManager.InsertionMode.AFTER_CURRENT - else -> MediaPlayerManager.InsertionMode.CLEAR - } - if (playlistName != null) { mediaPlayerManager.suggestedPlaylistName = playlistName } @@ -173,7 +167,10 @@ class DownloadHandler( shuffle, insertionMode ) - if (Settings.shouldTransitionOnPlayback && (!append || autoPlay)) { + + if (Settings.shouldTransitionOnPlayback && + (insertionMode == MediaPlayerManager.InsertionMode.CLEAR || autoPlay) + ) { fragment.findNavController().popBackStack(R.id.playerFragment, true) fragment.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 2bcf7604..140946b6 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -318,7 +318,7 @@ Posunout níž Ověření Rozšířené možnosti - + %d skladba %d skladby %d skladeb diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index b60c265f..c73cc34b 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -383,31 +383,31 @@ Demo Server Website besuchen Einen Fehler melden - + %d Titel %d Titel - + %d Titel zum Anheften ausgewählt %d Titel zum Anheften ausgewählt - + %d Titel zum herunterladen ausgewählt %d Titel zum herunterladen ausgewählt - + %d Titel losgelöst %d Titel losgelöst - + %d Titel gelöscht %d Titel gelöscht - + %d Titel am Ende hinzugefügt %d Titel am Ende hinzugefügt - + %d Titel nach aktuellen Titel hinzugefügt %d Titel nach aktuellen Titel hinzugefügt diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 9d4d3466..6584faf7 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -390,37 +390,37 @@ Visitar la página web Informar de un error Ultrasonic es un cliente Android de streaming de música gratuito y de código abierto para servidores compatibles con la API de Subsonic (versión 1.7.0 o superior).\n\nCon Ultrasonic puede transmitir o descargar fácilmente música desde su ordenador de casa a su teléfono Android utilizando su servidor multimedia compatible con Subsonic. El software del servidor Subsonic requiere una configuración adicional aparte de Ultrasonic.\n\nPor defecto, Ultrasonic no está configurado. Una vez que hayas configurado tu propio servidor, cambia la configuración del servidor para que se conecte a tu propio ordenador. - + %d canción %d canciones %d canciones - + %d canción seleccionada para ser anclada %d canciones seleccionadas para ser ancladas %d canciones seleccionadas para ser ancladas - + %d canción seleccionada para ser descargada %d canciones seleccionadas para ser descargadas %d canciones seleccionadas para ser descargadas - + %d canción desanclada %d canciones desancladas %d canciones desancladas - + %d canción eliminada %d canciones eliminadas %d canciones eliminadas - + %d canción añadida al final de la cola de reproducción %d canciones añadidas al final de la cola de reproducción %d canciones añadidas al final de la cola de reproducción - + %d canción insertada después de la canción actual %d canciones insertadas después de la canción actual %d canciones insertadas después de la canción actual diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 73226555..0f2f8e74 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -372,7 +372,7 @@ Visiter la page web Signaler un bug Ultrasonic est un client Android de streaming musical gratuit et open-source pour les serveurs compatibles avec l\'API Subsonic (version 1.7.0 ou supérieure). Avec Ultrasonic, vous pouvez facilement diffuser ou télécharger de la musique depuis votre ordinateur personnel vers votre téléphone Android en utilisant votre serveur multimédia compatible Subsonic. Le logiciel du serveur Subsonic nécessite une configuration supplémentaire distincte d\'Ultrasonic. Par défaut, Ultrasonic n\'est pas configuré. Une fois que vous avez mis en place votre propre serveur, veuillez modifier la configuration du serveur afin qu\'il se connecte à votre ordinateur. - + %d titre %d titres %d titres @@ -400,12 +400,12 @@ 50 morceaux Image d\'avatar Jour et nuit - + %d morceau ajouté à la file d\'attente de fin de lecture "%d morceaux ajoutés à la file d\'attente de fin de lecture" %d morceaux ajoutés à la file d\'attente de fin de lecture - + %d morceau supprimé %d morceaux supprimés %d morceaux supprimés @@ -429,22 +429,22 @@ Combien de chansons peuvent être téléchargées en parallèle Afficher plus de détails sur la chanson dans la lecture en cours (genre, année, débit) Liste - + %d chanson sélectionnée pour téléchargement %d chansons sélectionnées pour téléchargement %d chansons sélectionnées pour téléchargement - + %d chanson désépinglée %d chansons désépinglées %d chansons désépinglées - + %d chanson insérée après la chanson en cours %d chansons insérées après la chanson en cours %d chansons insérées après la chanson en cours - + %d chanson sélectionnée à épingler %d chansons sélectionnées à épingler %d chansons sélectionnées à épingler diff --git a/ultrasonic/src/main/res/values-gl/strings.xml b/ultrasonic/src/main/res/values-gl/strings.xml index 36cb490a..54b09230 100644 --- a/ultrasonic/src/main/res/values-gl/strings.xml +++ b/ultrasonic/src/main/res/values-gl/strings.xml @@ -51,12 +51,12 @@ Reproducir última Ancorar Mostra un cadro de diálogo de confirmación antes de eliminar ou desancorar as cancións - + %d canción seleccionada para ser ancorada %d cancións seleccionadas para ser ancoradas Tasa de bits máxima: ao fixar unha canción de forma permanente - + %d canción desancorada %d cancións desancoradas diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index 853d1e7f..cca3e6db 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -326,7 +326,7 @@ Lejjebb mozgat Bejelentkezés Haladó beállítások - + %d dal %d dal diff --git a/ultrasonic/src/main/res/values-ja/strings.xml b/ultrasonic/src/main/res/values-ja/strings.xml index d09880f5..51274e09 100644 --- a/ultrasonic/src/main/res/values-ja/strings.xml +++ b/ultrasonic/src/main/res/values-ja/strings.xml @@ -302,25 +302,25 @@ デモサーバー Webページにアクセス バグを報告 - + %d 曲 - + %d 曲がダウンロード選択されました - + %d 曲が固定解除されました - + %d 曲が固定されるよう選択されました - + %d 曲が削除されました - + %d 曲が再生キューの末尾に追加されました - + %d 曲が現在再生中の曲の次に追加されました 一般APIエラー: %1$s diff --git a/ultrasonic/src/main/res/values-nb-rNO/strings.xml b/ultrasonic/src/main/res/values-nb-rNO/strings.xml index 86e58df5..7a2c4406 100644 --- a/ultrasonic/src/main/res/values-nb-rNO/strings.xml +++ b/ultrasonic/src/main/res/values-nb-rNO/strings.xml @@ -123,11 +123,11 @@ Sett delingsinnstillinger Delinger Veksle spilleliste - + %d spor lagt til etter nåværende spor %d spor lagt til etter nåværende spor - + %d spor lagt til på slutten av spillekøen %d spor lagt til på slutten av spillekøen @@ -290,19 +290,19 @@ Skriv avlusningslogg til fil Rapporter en feil Generisk API-feil: %1$s - + %d spor %d spor - + %d spor å feste %d spor å feste - + %d spor løsnet %d spor løsnet - + %d spor slettet %d spor løsnet @@ -446,7 +446,7 @@ Omslag Støttede funksjoner Jukebox - + %d spor valgt for nedlasting %d spor valgt for nedlasting diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 2fea5e44..af539bfd 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -391,31 +391,31 @@ Website openen Bug melden Ultrasonic is een gratis, open source muziekstreamingclient voor Android, die gebruikmaakt van servers die compatibel zijn met de Subsonic-api (versie 1.7.0 of hoger).\n\nMet Ultrasonic kun je eenvoudig muziek streamen of downloaden van je computer naar je Android-telefoon met behulp van een met Subsonic compatibele mediaserver. Let op: de Subsonic-serversoftware vereist aanvullende configuratie.\n\nStandaard is Ultrasonic niet ingesteld. Zet je eigen server op en wijzig de serverconfiguratie in die van je eigen. - + %d nummer %d nummers - + %d vast te maken nummer geselecteerd %d vast te maken nummers geselecteerd - + %d te downloaden nummer geselecteerd %d te downloaden nummers geselecteerd - + %d nummer losgemaakt %d nummers losgemaakt - + %d nummer verwijderd %d nummers verwijderd - + %d nummer toegevoegd aan het einde van afspeelwachtrij %d nummers toegevoegd aan het einde van afspeelwachtrij - + %d nummer ingevoegd na het huidige nummer %d nummers ingevoegd na het huidige nummer diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 2e87a7ad..f62deae6 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -307,7 +307,7 @@ Przesuń się w dół Authentication Ustawienia zaawansowane - + %d utwór %d utwory %d utworów @@ -356,7 +356,7 @@ Węgierski Zapisanych jest %1$s plików z logami, które zajmują ~%2$s MB miejsca w katalogu %3$s. Czy chcesz je zachować\? Poprzednie - + Usunięto %d utwór Usunięto %d utwory Usunięto %d utworów @@ -403,20 +403,20 @@ Pobieraj tylko przez Wi-Fi Zawsze pytaj o opis i czas wygaśnięcia podczas tworzenia udostępnienia na serwerze Usuń pliki - + %d utwór zaznaczony do przypięcia %d utwory zaznaczone do przypięcia %d utworów zaznaczonych do przypięcia %d utworów zaznaczonych do przypięcia - + %d utworów zaznaczonych do pobrania %d utwory zaznaczone do pobrania %d utworów zaznaczonych do pobrania %d utworów zaznaczonych do pobrania Odwiedź stronę internetową - + Odpięto %d utwór Odpięto %d utwory Odpięto %d utworów @@ -446,13 +446,13 @@ Lista Okładka Spróbuj odtworzyć pliki multimedialne za pomocą układu dekodującego w telefonie. Może to zmniejszyć zużycie baterii! - + Dodano %d utwór na koniec kolejki odtwarzania Dodano %d utwory na koniec kolejki odtwarzania Dodano %d utworów na koniec kolejki odtwarzania Dodano %d utworów na koniec kolejki odtwarzania - + Wstawiono %d utwór po bieżącym utworze Wstawiono %d utwory po bieżącym utworze Wstawiono %d utworów po bieżącym utworze diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index 96691401..70a42293 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -388,37 +388,37 @@ Visitar a página web Reportar um erro Ultrasonic é um cliente gratuito e open-source para Android de streaming de música para API de servidores compatíveis com Subsonic (version 1.7.0 ou maior).\n\nCom Ultrasonic você pode facilmente reproduzir online ou baixar música de seu computador doméstico para seu telefone Android usando um servidor de mídia compatível com Subsonic. O software do servidor Subsonic necessita uma configuração independente do Ultrasonic.\n\nPor padrão, o servidor Ultrasonic não é configurado. Uma vez que você configurou seu próprio servidor, altere a configuração no Ultrasonic para poder conectá-lo. - + %d música %d músicas %d músicas - + %d música selecionada para ser fixada %d músicas selecionadas para serem fixadas %d músicas selecionadas para serem fixadas - + %d música selecionada para ser baixada %d músicas selecionadas para serem baixadas %d músicas selecionadas para serem baixadas - + %d música desfixada %d músicas desfixadas %d músicas desfixadas - + %d música excluída %d músicas excluídas %d músicas excluídas - + %d música adicionada ao final da playlist %d músicas adicionadas ao final da playlist %d músicas adicionadas ao final da playlist - + %d música adicionada após a atual %d músicas adicionadas após a atual %d músicas adicionadas após a atual diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index e0cf7285..834ea509 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -307,7 +307,7 @@ Move down Authentication Configurações avançadas - + %d música %d músicas %d músicas @@ -330,22 +330,22 @@ Servidor Demonstração Visitar a página web Reportar um erro - + %d música selecionada para ser fixada %d músicas selecionadas para serem fixadas %d músicas selecionadas para serem fixadas - + %d música desfixada %d músicas desfixadas %d músicas desfixadas - + %d música excluída %d músicas excluídas %d músicas excluídas - + %d música adicionada ao final da playlist %d músicas adicionadas ao final da playlist %d músicas adicionadas ao final da playlist @@ -447,12 +447,12 @@ \nCom Ultrasonic, pode facilmente reproduzir online ou descarregar música do seu computador doméstico para o seu telefone Android usando um servidor de mídia compatível com Subsonic. O software do servidor Subsonic necessita uma configuração independente do Ultrasonic. \n \nPor padrão, o servidor Ultrasonic não é configurado. Uma vez que configurou o seu próprio servidor, altere a configuração no Ultrasonic para poder conectá-lo. - + %d música selecionada a ser descarregada %d músicas selecionadas para serem baixadas %d músicas selecionadas para serem baixadas - + %d música adicionada após a atual %d músicas adicionadas após a atual %d músicas adicionadas após a atual diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 0d9e5ff4..0c750c2f 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -347,7 +347,7 @@ Одна или несколько функций были отключены, потому что сервер их не поддерживает.\nВы можете запустить этот тест снова в любое время. Демо-сервер - + %d песня %d песни %d песен diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index f704d2a1..2410948c 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -368,25 +368,25 @@ \n通过使用 Ultrasonic 你可以轻松的从你的电脑上的 Subsonic 兼容服务端流式传输或者下载音乐。 Subsonic 服务端与 Ultrasonic 都需要额外的配置才可使用。 \n \n默认情况下,Ultrasonic 并未进行配置,当服务端配置完成后,请确保配置允许客户端连接到你的计算机。 - + %d 首曲目 - + 已选择 %d 首歌曲进行固定 - + 已选择要下载 %d 首歌曲 - + 已选择 %d 首歌曲取消固定 - + %d 首歌曲被删除 - + 已将 %d 首歌曲添加到播放队列的末尾 - + 在当前歌曲之后插入了 %d 首歌曲 diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 08db0901..04488224 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -402,31 +402,35 @@ https://ultrasonic.gitlab.io/ https://gitlab.com/ultrasonic/ultrasonic/issues - + %d song %d songs - + %d song selected to be pinned %d songs selected to be pinned - + %d song selected to be downloaded %d songs selected to be downloaded - + %d song unpinned %d songs unpinned - + %d song deleted %d songs deleted - + + %d song added to the play queue + %d songs added to the play queue + + %d song added to the end of play queue %d songs added to the end of play queue - + %d song inserted after current song %d songs inserted after current song From 7fcb58963c919c57f112b1d4cff2c49c02e6547b Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 31 Jul 2023 12:47:49 +0200 Subject: [PATCH 20/25] Show Pin button also when offline --- .../org/moire/ultrasonic/fragment/TrackCollectionFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7d076d83..108016dc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -424,7 +424,7 @@ open class TrackCollectionFragment( playNowButton?.isVisible = show.all playNextButton?.isVisible = show.all && multipleSelection playLastButton?.isVisible = show.all && multipleSelection - pinButton?.isVisible = show.all && !isOffline() && show.pin + pinButton?.isVisible = show.all && show.pin unpinButton?.isVisible = show.all && show.unpin downloadButton?.isVisible = show.all && show.download && !isOffline() deleteButton?.isVisible = show.all && show.delete From f99fb1c92a33d5ab11da6f366866946c17861fd8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 25 Jul 2023 17:31:49 +0000 Subject: [PATCH 21/25] Update dependency com.android.tools.build:gradle to v8.1.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 930482e9..daa7e3df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ gradle = "8.1.1" navigation = "2.6.0" -gradlePlugin = "8.0.2" +gradlePlugin = "8.1.0" androidxcore = "1.10.1" ktlint = "0.43.2" ktlintGradle = "11.5.0" From ddf8ce7029eb97749c684523c297d4c891049038 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 31 Jul 2023 14:46:52 +0200 Subject: [PATCH 22/25] Migrate from KAPT to KSP --- build.gradle | 13 ++++++++++- core/domain/build.gradle | 7 ++++-- core/subsonic-api/build.gradle | 4 ++++ gradle/libs.versions.toml | 4 ++++ .../android-module-bootstrap.gradle | 2 +- gradle_scripts/kotlin-module-bootstrap.gradle | 3 +-- ultrasonic/build.gradle | 22 ++++++++++--------- 7 files changed, 39 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index d9cf0212..1797e96b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { apply from: 'gradle/versions.gradle' @@ -10,6 +12,7 @@ buildscript { repositories { google() mavenCentral() + gradlePluginPortal() maven { url "https://plugins.gradle.org/m2/" } } dependencies { @@ -26,21 +29,29 @@ allprojects { buildscript { repositories { mavenCentral() + gradlePluginPortal() google() } } repositories { mavenCentral() + gradlePluginPortal() google() } // Set Kotlin JVM target to the same for all subprojects - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + tasks.withType(KotlinCompile).configureEach { kotlinOptions { jvmTarget = "17" } } + + tasks.withType(JavaCompile).tap { + configureEach { + options.compilerArgs.add("-Xlint:deprecation") + } + } } wrapper { diff --git a/core/domain/build.gradle b/core/domain/build.gradle index 75d3d4a5..069c9605 100644 --- a/core/domain/build.gradle +++ b/core/domain/build.gradle @@ -1,11 +1,14 @@ +plugins { + alias libs.plugins.ksp +} + apply from: bootstrap.androidModule -apply plugin: 'kotlin-kapt' dependencies { implementation libs.core implementation libs.roomRuntime implementation libs.roomKtx - kapt libs.room + ksp libs.room } android { diff --git a/core/subsonic-api/build.gradle b/core/subsonic-api/build.gradle index 9f044c21..034da1e2 100644 --- a/core/subsonic-api/build.gradle +++ b/core/subsonic-api/build.gradle @@ -1,3 +1,7 @@ +plugins { + alias libs.plugins.ksp +} + apply from: bootstrap.kotlinModule dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index daa7e3df..6137c2c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.5.2" kotlin = "1.8.22" +ksp = "1.8.22-1.0.11" kotlinxCoroutines = "1.7.3" viewModelKtx = "2.6.1" swipeRefresh = "1.1.0" @@ -100,3 +101,6 @@ kluentAndroid = { module = "org.amshove.kluent:kluent-android", versio mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } + +[plugins] +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file diff --git a/gradle_scripts/android-module-bootstrap.gradle b/gradle_scripts/android-module-bootstrap.gradle index f84d8fe5..bfa5dac2 100644 --- a/gradle_scripts/android-module-bootstrap.gradle +++ b/gradle_scripts/android-module-bootstrap.gradle @@ -4,7 +4,7 @@ apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" -apply plugin: 'org.jetbrains.kotlin.kapt' +apply plugin: 'com.google.devtools.ksp' android { compileSdkVersion versions.compileSdk diff --git a/gradle_scripts/kotlin-module-bootstrap.gradle b/gradle_scripts/kotlin-module-bootstrap.gradle index c71044e9..0a69a824 100644 --- a/gradle_scripts/kotlin-module-bootstrap.gradle +++ b/gradle_scripts/kotlin-module-bootstrap.gradle @@ -2,7 +2,7 @@ * This module provides a base for for pure kotlin modules */ apply plugin: 'kotlin' -apply plugin: 'org.jetbrains.kotlin.kapt' +apply plugin: 'com.google.devtools.ksp' apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" sourceSets { @@ -12,7 +12,6 @@ sourceSets { test.resources.srcDirs += "${projectDir}/src/integrationTest/resources" } - dependencies { api libs.kotlinStdlib diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 9787150a..004d151c 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -1,6 +1,9 @@ +plugins { + alias libs.plugins.ksp +} + apply plugin: 'com.android.application' apply plugin: 'org.jetbrains.kotlin.android' -apply plugin: 'org.jetbrains.kotlin.kapt' apply plugin: "androidx.navigation.safeargs.kotlin" apply from: "../gradle_scripts/code_quality.gradle" @@ -64,20 +67,20 @@ android { targetCompatibility JavaVersion.VERSION_17 } - kapt { - arguments { - arg("room.schemaLocation", "$rootDir/ultrasonic/schemas".toString()) - } + ksp { + arg("room.schemaLocation", "$rootDir/ultrasonic/schemas") } lint { baseline = file("lint-baseline.xml") abortOnError true warningsAsErrors true - disable 'IconMissingDensityFolder', 'VectorPath' - ignore 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity' warning 'ImpliedQuantity' + disable 'IconMissingDensityFolder', 'VectorPath' + disable 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity' disable 'ObsoleteLintCustomCheck' + // We manage dependencies on Gitlab with RenovateBot + disable 'GradleDependency' textReport true checkDependencies true } @@ -85,7 +88,7 @@ android { } -tasks.withType(Test) { +tasks.withType(Test).configureEach { useJUnitPlatform() } @@ -129,7 +132,7 @@ dependencies { implementation libs.rxAndroid implementation libs.multiType - kapt libs.room + ksp libs.room testImplementation libs.kotlinReflect testImplementation libs.junit @@ -141,6 +144,5 @@ dependencies { testImplementation libs.robolectric implementation libs.timber - } From a0b9e738a5ec4f20c05f81adf0ed77c5deb09466 Mon Sep 17 00:00:00 2001 From: Alex Katlein Date: Wed, 2 Aug 2023 19:43:55 +0200 Subject: [PATCH 23/25] Using android.R.color.transparent instead of empty drawable for the placeholder - For some reason Android Auto displays a square with a white fill color where the empty space should be - Got rid of empty drawable --- .../moire/ultrasonic/playback/AutoMediaBrowserCallback.kt | 2 +- ultrasonic/src/main/res/drawable/empty.xml | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 ultrasonic/src/main/res/drawable/empty.xml diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index d7abb1bb..0154a05a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -276,7 +276,7 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac private fun getPlaceholderButton() = CommandButton.Builder() .setDisplayName("Placeholder") - .setIconResId(R.drawable.empty) + .setIconResId(android.R.color.transparent) .setSessionCommand( SessionCommand( PlaybackService.CUSTOM_COMMAND_PLACEHOLDER, diff --git a/ultrasonic/src/main/res/drawable/empty.xml b/ultrasonic/src/main/res/drawable/empty.xml deleted file mode 100644 index a4762a86..00000000 --- a/ultrasonic/src/main/res/drawable/empty.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - From e9fdbd924bf8c32c631b518474ec7f5be182660a Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 3 Aug 2023 14:28:22 +0200 Subject: [PATCH 24/25] Don't skip around when doing "Play Next" while music is playing --- .../kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt | 2 +- .../kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 93588750..d07b4c52 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt @@ -460,7 +460,7 @@ class MediaPlayerManager( // We can't just use play(0,0) then all random playlists will start with the first track. // Additionally the shuffle order becomes clear on after some time, so we need to wait for // the right event, and can start playback only then. - if (autoPlay) { + if (autoPlay && controller?.isPlaying != true) { if (isShufflePlayEnabled) { deferredPlay = { val start = controller?.currentTimeline diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 207c9141..a0b4c156 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -169,7 +169,7 @@ class DownloadHandler( ) if (Settings.shouldTransitionOnPlayback && - (insertionMode == MediaPlayerManager.InsertionMode.CLEAR || autoPlay) + insertionMode == MediaPlayerManager.InsertionMode.CLEAR ) { fragment.findNavController().popBackStack(R.id.playerFragment, true) fragment.findNavController().navigate(R.id.playerFragment) From 818cb96eb569fcc4cf18e8f7e636c2afff4ad65f Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 3 Aug 2023 18:24:54 +0200 Subject: [PATCH 25/25] Release 4.7.0 --- fastlane/metadata/android/en-US/changelogs/130.txt | 9 +++++++++ ultrasonic/build.gradle | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/130.txt diff --git a/fastlane/metadata/android/en-US/changelogs/130.txt b/fastlane/metadata/android/en-US/changelogs/130.txt new file mode 100644 index 00000000..3d2ccf81 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/130.txt @@ -0,0 +1,9 @@ +### Features +- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto) +- Properly handling nested directory structures (Android Auto) +- Add a toast when adding tracks to the playlist +- Allow pinning when offline + +### Dependencies +- Update koin +- Update media3 to v1.1.0 \ No newline at end of file diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 004d151c..cea9d70f 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -12,8 +12,8 @@ android { defaultConfig { applicationId "org.moire.ultrasonic" - versionCode 126 - versionName "4.6.3" + versionCode 128 + versionName "4.7.0" minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk