From bdcb1a505ba68a60510fd12f9bf8999b3d63da91 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Fri, 19 May 2023 21:37:31 +0000 Subject: [PATCH] Use the JukeboxPlayer as a Player instead of an Controller --- ultrasonic/lint-baseline.xml | 81 +--- ultrasonic/src/main/AndroidManifest.xml | 7 - .../receiver/BluetoothIntentReceiver.kt | 1 + .../receiver/UltrasonicIntentReceiver.java | 2 +- .../ultrasonic/activity/NavigationActivity.kt | 23 +- .../moire/ultrasonic/di/MediaPlayerModule.kt | 4 +- .../ultrasonic/fragment/BookmarksFragment.kt | 2 +- .../ultrasonic/fragment/NowPlayingFragment.kt | 14 +- .../ultrasonic/fragment/PlayerFragment.kt | 130 ++++--- .../ultrasonic/fragment/SearchFragment.kt | 12 +- .../ultrasonic/fragment/SettingsFragment.kt | 6 +- .../fragment/TrackCollectionFragment.kt | 4 +- .../moire/ultrasonic/model/EditServerModel.kt | 4 +- .../playback/AutoMediaBrowserCallback.kt | 12 +- .../playback/CustomNotificationProvider.kt | 6 +- .../ultrasonic/playback/PlaybackService.kt | 157 +++++--- .../ultrasonic/service/JukeboxMediaPlayer.kt | 367 ++++++++---------- .../JukeboxNotificationActionFactory.kt | 97 ----- .../service/JukeboxUnimplementedFunctions.kt | 23 +- .../service/MediaPlayerLifecycleSupport.kt | 58 +-- ...yerController.kt => MediaPlayerManager.kt} | 151 +++---- .../ultrasonic/subsonic/DownloadHandler.kt | 14 +- .../org/moire/ultrasonic/util/CacheCleaner.kt | 6 +- .../org/moire/ultrasonic/util/Settings.kt | 3 + .../src/main/res/layout/jukebox_volume.xml | 28 -- ultrasonic/src/main/res/values-cs/strings.xml | 1 - ultrasonic/src/main/res/values-de/strings.xml | 1 - ultrasonic/src/main/res/values-es/strings.xml | 1 - ultrasonic/src/main/res/values-fr/strings.xml | 1 - ultrasonic/src/main/res/values-hu/strings.xml | 1 - ultrasonic/src/main/res/values-it/strings.xml | 1 - ultrasonic/src/main/res/values-ja/strings.xml | 1 - .../src/main/res/values-nb-rNO/strings.xml | 1 - ultrasonic/src/main/res/values-nl/strings.xml | 1 - ultrasonic/src/main/res/values-pl/strings.xml | 1 - .../src/main/res/values-pt-rBR/strings.xml | 1 - ultrasonic/src/main/res/values-pt/strings.xml | 1 - ultrasonic/src/main/res/values-ru/strings.xml | 1 - .../src/main/res/values-zh-rCN/strings.xml | 1 - .../src/main/res/values-zh-rTW/strings.xml | 1 - ultrasonic/src/main/res/values/strings.xml | 1 - 41 files changed, 470 insertions(+), 758 deletions(-) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxNotificationActionFactory.kt rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/{MediaPlayerController.kt => MediaPlayerManager.kt} (87%) delete mode 100644 ultrasonic/src/main/res/layout/jukebox_volume.xml diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index 2458dc9d..f77f8b2f 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -1,27 +1,5 @@ - - - - - - - - - + @@ -48,50 +26,6 @@ file="../core/subsonic-api/build/libs/subsonic-api.jar"/> - - - - - - - - - - - - - - - - - - - - - - - = Build.VERSION_CODES.TIRAMISU) { getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) } else { + @Suppress("DEPRECATION") getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java index 9297b5d8..5eb8e214 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java @@ -13,7 +13,7 @@ import static org.koin.java.KoinJavaComponent.inject; public class UltrasonicIntentReceiver extends BroadcastReceiver { - private final Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); + private Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); @Override public void onReceive(Context context, Intent intent) 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 6ba4f89d..8ed8c8bc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -17,7 +17,6 @@ import android.os.Build import android.os.Bundle import android.provider.MediaStore import android.provider.SearchRecentSuggestions -import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import android.view.View @@ -55,8 +54,8 @@ import org.moire.ultrasonic.data.ServerSettingDao import org.moire.ultrasonic.fragment.OnBackPressedHandler import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.provider.SearchSuggestionProvider -import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.util.Constants @@ -98,7 +97,7 @@ class NavigationActivity : AppCompatActivity() { private val serverSettingsModel: ServerSettingsModel by viewModel() private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() - private val mediaPlayerController: MediaPlayerController by inject() + private val mediaPlayerManager: MediaPlayerManager by inject() private val activeServerProvider: ActiveServerProvider by inject() private val serverRepository: ServerSettingDao by inject() @@ -274,18 +273,6 @@ class NavigationActivity : AppCompatActivity() { } } - override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - val isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN - val isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP - val isVolumeAdjust = isVolumeDown || isVolumeUp - val isJukebox = mediaPlayerController.isJukeboxEnabled - if (isVolumeAdjust && isJukebox) { - mediaPlayerController.adjustVolume(isVolumeUp) - return true - } - return super.onKeyDown(keyCode, event) - } - private fun setupNavigationMenu(navController: NavController) { navigationView?.setupWithNavController(navController) @@ -308,7 +295,7 @@ class NavigationActivity : AppCompatActivity() { } R.id.menu_exit -> { setResult(Constants.RESULT_CLOSE_ALL) - mediaPlayerController.onDestroy() + mediaPlayerManager.onDestroy() finish() exit() } @@ -475,9 +462,9 @@ class NavigationActivity : AppCompatActivity() { } if (nowPlayingView != null) { - val playerState: Int = mediaPlayerController.playbackState + val playerState: Int = mediaPlayerManager.playbackState if (playerState == STATE_BUFFERING || playerState == STATE_READY) { - val item: MediaItem? = mediaPlayerController.currentMediaItem + val item: MediaItem? = mediaPlayerManager.currentMediaItem if (item != null) { nowPlayingView?.visibility = View.VISIBLE } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt index 6ec150c6..823a84fa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt @@ -2,8 +2,8 @@ package org.moire.ultrasonic.di import org.koin.dsl.module import org.moire.ultrasonic.service.ExternalStorageMonitor -import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.PlaybackStateSerializer /** @@ -15,5 +15,5 @@ val mediaPlayerModule = module { single { ExternalStorageMonitor() } // TODO Ideally this can be cleaned up when all circular references are removed. - single { MediaPlayerController(get(), get(), get()) } + single { MediaPlayerManager(get(), get(), get()) } } 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 ed644e72..5f0ef215 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -72,7 +72,7 @@ class BookmarksFragment : TrackCollectionFragment() { currentPlayingPosition = songs[0].bookmarkPosition ) - mediaPlayerController.restore( + mediaPlayerManager.restore( state = state, autoPlay = true, newPlaylist = true diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index b6020f56..70eaf028 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -25,7 +25,7 @@ import kotlin.math.abs import org.koin.android.ext.android.inject import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.Settings @@ -48,7 +48,7 @@ class NowPlayingFragment : Fragment() { private var nowPlayingArtist: TextView? = null private var rxBusSubscription: Disposable? = null - private val mediaPlayerController: MediaPlayerController by inject() + private val mediaPlayerManager: MediaPlayerManager by inject() private val imageLoaderProvider: ImageLoaderProvider by inject() override fun onCreate(savedInstanceState: Bundle?) { @@ -85,13 +85,13 @@ class NowPlayingFragment : Fragment() { @SuppressLint("ClickableViewAccessibility") private fun update() { try { - if (mediaPlayerController.isPlaying) { + if (mediaPlayerManager.isPlaying) { playButton!!.setIconResource(R.drawable.media_pause) } else { playButton!!.setIconResource(R.drawable.media_start) } - val file = mediaPlayerController.currentMediaItem?.toTrack() + val file = mediaPlayerManager.currentMediaItem?.toTrack() if (file != null) { val title = file.title @@ -127,7 +127,7 @@ class NowPlayingFragment : Fragment() { // This empty onClickListener is necessary for the onTouchListener to work requireView().setOnClickListener { } - playButton!!.setOnClickListener { mediaPlayerController.togglePlayPause() } + playButton!!.setOnClickListener { mediaPlayerManager.togglePlayPause() } } catch (all: Exception) { Timber.w(all, "Failed to get notification cover art") } @@ -149,10 +149,10 @@ class NowPlayingFragment : Fragment() { if (abs(deltaX) > MIN_DISTANCE) { // left or right if (deltaX < 0) { - mediaPlayerController.seekToPrevious() + mediaPlayerManager.seekToPrevious() } if (deltaX > 0) { - mediaPlayerController.seekToNext() + mediaPlayerManager.seekToNext() } } else if (abs(deltaY) > MIN_DISTANCE) { if (deltaY < 0) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 65f6b442..bdc08eec 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -87,7 +87,7 @@ import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign @@ -128,7 +128,7 @@ class PlayerFragment : // Data & Services private val networkAndStorageChecker: NetworkAndStorageChecker by inject() - private val mediaPlayerController: MediaPlayerController by inject() + private val mediaPlayerManager: MediaPlayerManager by inject() private val shareHandler: ShareHandler by inject() private val imageLoaderProvider: ImageLoaderProvider by inject() private var currentSong: Track? = null @@ -263,8 +263,8 @@ class PlayerFragment : val previousButton: AutoRepeatButton = view.findViewById(R.id.button_previous) val nextButton: AutoRepeatButton = view.findViewById(R.id.button_next) shuffleButton = view.findViewById(R.id.button_shuffle) - updateShuffleButtonState(mediaPlayerController.isShufflePlayEnabled) - updateRepeatButtonState(mediaPlayerController.repeatMode) + updateShuffleButtonState(mediaPlayerManager.isShufflePlayEnabled) + updateRepeatButtonState(mediaPlayerManager.repeatMode) val ratingLinearLayout = view.findViewById(R.id.song_rating) if (!useFiveStarRating) ratingLinearLayout.isVisible = false @@ -286,7 +286,7 @@ class PlayerFragment : previousButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() launch(CommunicationError.getHandler(context)) { - mediaPlayerController.seekToPrevious() + mediaPlayerManager.seekToPrevious() } } @@ -297,7 +297,7 @@ class PlayerFragment : nextButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() launch(CommunicationError.getHandler(context)) { - mediaPlayerController.seekToNext() + mediaPlayerManager.seekToNext() } } @@ -307,22 +307,22 @@ class PlayerFragment : pauseButton.setOnClickListener { launch(CommunicationError.getHandler(context)) { - mediaPlayerController.pause() + mediaPlayerManager.pause() } } stopButton.setOnClickListener { launch(CommunicationError.getHandler(context)) { - mediaPlayerController.reset() + mediaPlayerManager.reset() } } playButton.setOnClickListener { - if (!mediaPlayerController.isJukeboxEnabled) + if (!mediaPlayerManager.isJukeboxEnabled) networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() launch(CommunicationError.getHandler(context)) { - mediaPlayerController.play() + mediaPlayerManager.play() } } @@ -331,12 +331,12 @@ class PlayerFragment : } repeatButton.setOnClickListener { - var newRepeat = mediaPlayerController.repeatMode + 1 + var newRepeat = mediaPlayerManager.repeatMode + 1 if (newRepeat == 3) { newRepeat = 0 } - mediaPlayerController.repeatMode = newRepeat + mediaPlayerManager.repeatMode = newRepeat onPlaylistChanged() @@ -358,7 +358,7 @@ class PlayerFragment : progressBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { override fun onStopTrackingTouch(seekBar: SeekBar) { launch(CommunicationError.getHandler(context)) { - mediaPlayerController.seekTo(progressBar.progress) + mediaPlayerManager.seekTo(progressBar.progress) } } @@ -395,12 +395,19 @@ class PlayerFragment : // Query the Jukebox state in an IO Context ioScope.launch(CommunicationError.getHandler(context)) { try { - jukeboxAvailable = mediaPlayerController.isJukeboxAvailable + jukeboxAvailable = mediaPlayerManager.isJukeboxAvailable } catch (all: Exception) { Timber.e(all) } } + // Subscribe to change in command availability + mediaPlayerManager.addListener(object : Player.Listener { + override fun onAvailableCommandsChanged(availableCommands: Player.Commands) { + updateMediaButtonActivationState() + } + }) + view.setOnTouchListener { _, event -> gestureScanner.onTouchEvent(event) } } @@ -432,7 +439,7 @@ class PlayerFragment : } private fun toggleShuffle() { - val isEnabled = mediaPlayerController.toggleShuffle() + val isEnabled = mediaPlayerManager.toggleShuffle() if (isEnabled) { Util.toast(activity, R.string.download_menu_shuffle_on) @@ -445,7 +452,7 @@ class PlayerFragment : override fun onResume() { super.onResume() - if (mediaPlayerController.currentMediaItem == null) { + if (mediaPlayerManager.currentMediaItem == null) { playlistFlipper.displayedChild = 1 } else { // Download list and Album art must be updated when resumed @@ -458,7 +465,7 @@ class PlayerFragment : executorService = Executors.newSingleThreadScheduledExecutor() executorService.scheduleWithFixedDelay(runnable, 0L, 500L, TimeUnit.MILLISECONDS) - if (mediaPlayerController.keepScreenOn) { + if (mediaPlayerManager.keepScreenOn) { requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } else { requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) @@ -469,7 +476,7 @@ class PlayerFragment : // Scroll to current playing. private fun scrollToCurrent() { - val index = mediaPlayerController.currentMediaItemIndex + val index = mediaPlayerManager.currentMediaItemIndex if (index != -1) { val smoothScroller = LinearSmoothScroller(context) @@ -557,7 +564,7 @@ class PlayerFragment : equalizerMenuItem.isVisible = isEqualizerAvailable } - val mediaPlayerController = mediaPlayerController + val mediaPlayerController = mediaPlayerManager val track = mediaPlayerController.currentMediaItem?.toTrack() if (track != null) { @@ -666,12 +673,12 @@ class PlayerFragment : } R.id.menu_item_screen_on_off -> { val window = requireActivity().window - if (mediaPlayerController.keepScreenOn) { + if (mediaPlayerManager.keepScreenOn) { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - mediaPlayerController.keepScreenOn = false + mediaPlayerManager.keepScreenOn = false } else { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - mediaPlayerController.keepScreenOn = true + mediaPlayerManager.keepScreenOn = true } return true } @@ -684,8 +691,8 @@ class PlayerFragment : return true } R.id.menu_item_jukebox -> { - val jukeboxEnabled = !mediaPlayerController.isJukeboxEnabled - mediaPlayerController.isJukeboxEnabled = jukeboxEnabled + val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled + mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled Util.toast( context, if (jukeboxEnabled) R.string.download_jukebox_on @@ -699,13 +706,13 @@ class PlayerFragment : return true } R.id.menu_item_clear_playlist -> { - mediaPlayerController.isShufflePlayEnabled = false - mediaPlayerController.clear() + mediaPlayerManager.isShufflePlayEnabled = false + mediaPlayerManager.clear() onPlaylistChanged() return true } R.id.menu_item_save_playlist -> { - if (mediaPlayerController.playlistSize > 0) { + if (mediaPlayerManager.playlistSize > 0) { showSavePlaylistDialog() } return true @@ -724,7 +731,7 @@ class PlayerFragment : if (track == null) return true val songId = track.id - val playerPosition = mediaPlayerController.playerPosition + val playerPosition = mediaPlayerManager.playerPosition track.bookmarkPosition = playerPosition val bookmarkTime = Util.formatTotalDuration(playerPosition.toLong(), true) Thread { @@ -759,7 +766,7 @@ class PlayerFragment : return true } R.id.menu_item_share -> { - val mediaPlayerController = mediaPlayerController + val mediaPlayerController = mediaPlayerManager val tracks: MutableList = ArrayList() val playlist = mediaPlayerController.playlist for (item in playlist) { @@ -794,8 +801,7 @@ class PlayerFragment : private fun update(cancel: CancellationToken? = null) { if (cancel?.isCancellationRequested == true) return - val mediaPlayerController = mediaPlayerController - if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) { + if (currentSong?.id != mediaPlayerManager.currentMediaItem?.mediaId) { onTrackChanged() } updateSeekBar() @@ -803,10 +809,10 @@ class PlayerFragment : private fun savePlaylistInBackground(playlistName: String) { Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName)) - mediaPlayerController.suggestedPlaylistName = playlistName + mediaPlayerManager.suggestedPlaylistName = playlistName // The playlist can be acquired only from the main thread - val entries = mediaPlayerController.playlist.map { + val entries = mediaPlayerManager.playlist.map { it.toTrack() } @@ -859,8 +865,8 @@ class PlayerFragment : // Create listener val clickHandler: ((Track, Int) -> Unit) = { _, listPos -> - val mediaIndex = mediaPlayerController.getUnshuffledIndexOf(listPos) - mediaPlayerController.play(mediaIndex) + val mediaIndex = mediaPlayerManager.getUnshuffledIndexOf(listPos) + mediaPlayerManager.play(mediaIndex) } viewAdapter.register( @@ -924,7 +930,7 @@ class PlayerFragment : @SuppressLint("NotifyDataSetChanged") override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { val pos = viewHolder.bindingAdapterPosition - val item = mediaPlayerController.getMediaItemAt(pos) + val item = mediaPlayerManager.getMediaItemAt(pos) // Remove the item from the list quickly val items = viewAdapter.getCurrentList().toMutableList() @@ -940,7 +946,7 @@ class PlayerFragment : Util.toast(context, songRemoved) // Remove the item from the playlist - mediaPlayerController.removeFromPlaylist(pos) + mediaPlayerManager.removeFromPlaylist(pos) } override fun onSelectedChanged( @@ -960,7 +966,7 @@ class PlayerFragment : dragging = false // Move the item in the playlist separately Timber.i("Moving item %s to %s", startPosition, endPosition) - mediaPlayerController.moveItemInPlaylist(startPosition, endPosition) + mediaPlayerManager.moveItemInPlaylist(startPosition, endPosition) } } @@ -1038,7 +1044,7 @@ class PlayerFragment : } private fun onPlaylistChanged() { - val mediaPlayerController = mediaPlayerController + val mediaPlayerController = mediaPlayerManager // Try to display playlist in play order val list = mediaPlayerController.playlistInPlayOrder emptyTextView.setText(R.string.playlist_empty) @@ -1050,12 +1056,12 @@ class PlayerFragment : } private fun onTrackChanged() { - currentSong = mediaPlayerController.currentMediaItem?.toTrack() + currentSong = mediaPlayerManager.currentMediaItem?.toTrack() scrollToCurrent() - val totalDuration = mediaPlayerController.playListDuration - val totalSongs = mediaPlayerController.playlistSize - val currentSongIndex = mediaPlayerController.currentMediaItemIndex + 1 + val totalDuration = mediaPlayerManager.playListDuration + val totalSongs = mediaPlayerManager.playlistSize + val currentSongIndex = mediaPlayerManager.currentMediaItemIndex + 1 val duration = Util.formatTotalDuration(totalDuration) val trackFormat = String.format(Locale.getDefault(), "%d / %d", currentSongIndex, totalSongs) @@ -1110,23 +1116,27 @@ class PlayerFragment : updateSongRating() - nextButton.isEnabled = mediaPlayerController.canSeekToNext() - previousButton.isEnabled = mediaPlayerController.canSeekToPrevious() + updateMediaButtonActivationState() + } + + private fun updateMediaButtonActivationState() { + nextButton.isEnabled = mediaPlayerManager.canSeekToNext() + previousButton.isEnabled = mediaPlayerManager.canSeekToPrevious() } @Synchronized private fun updateSeekBar() { - val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled - val millisPlayed: Int = max(0, mediaPlayerController.playerPosition) - val duration: Int = mediaPlayerController.playerDuration - val playbackState: Int = mediaPlayerController.playbackState + val isJukeboxEnabled: Boolean = mediaPlayerManager.isJukeboxEnabled + val millisPlayed: Int = max(0, mediaPlayerManager.playerPosition) + val duration: Int = mediaPlayerManager.playerDuration + val playbackState: Int = mediaPlayerManager.playbackState if (currentSong != null) { positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true) durationTextView.text = Util.formatTotalDuration(duration.toLong(), true) progressBar.max = if (duration == 0) 100 else duration // Work-around for apparent bug. progressBar.progress = millisPlayed - progressBar.isEnabled = mediaPlayerController.isPlaying || isJukeboxEnabled + progressBar.isEnabled = mediaPlayerManager.isPlaying || isJukeboxEnabled } else { positionTextView.setText(R.string.util_zero_time) durationTextView.setText(R.string.util_no_time) @@ -1135,7 +1145,7 @@ class PlayerFragment : progressBar.isEnabled = false } - val progress = mediaPlayerController.bufferedPercentage + val progress = mediaPlayerManager.bufferedPercentage updateBufferProgress(playbackState, progress) } @@ -1148,7 +1158,7 @@ class PlayerFragment : setTitle(this@PlayerFragment, downloadStatus) } Player.STATE_READY -> { - if (mediaPlayerController.isShufflePlayEnabled) { + if (mediaPlayerManager.isShufflePlayEnabled) { setTitle( this@PlayerFragment, R.string.download_playerstate_playing_shuffle @@ -1172,7 +1182,7 @@ class PlayerFragment : } private fun updateButtonStates(playbackState: Int) { - val isPlaying = mediaPlayerController.isPlaying + val isPlaying = mediaPlayerManager.isPlaying when (playbackState) { Player.STATE_READY -> { pauseButton.isVisible = isPlaying @@ -1195,9 +1205,9 @@ class PlayerFragment : private fun seek(forward: Boolean) { launch(CommunicationError.getHandler(context)) { if (forward) { - mediaPlayerController.seekForward() + mediaPlayerManager.seekForward() } else { - mediaPlayerController.seekBack() + mediaPlayerManager.seekBack() } } } @@ -1223,28 +1233,28 @@ class PlayerFragment : // Right to Left swipe if (e1X - e2X > swipeDistance && absX > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.seekToNext() + mediaPlayerManager.seekToNext() return true } // Left to Right swipe if (e2X - e1X > swipeDistance && absX > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.seekToPrevious() + mediaPlayerManager.seekToPrevious() return true } // Top to Bottom swipe if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.seekTo(mediaPlayerController.playerPosition + 30000) + mediaPlayerManager.seekTo(mediaPlayerManager.playerPosition + 30000) return true } // Bottom to Top swipe if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000) + mediaPlayerManager.seekTo(mediaPlayerManager.playerPosition - 8000) return true } return false @@ -1309,7 +1319,7 @@ class PlayerFragment : builder.setView(layout) builder.setCancelable(true) val dialog = builder.create() - val playlistName = mediaPlayerController.suggestedPlaylistName + val playlistName = mediaPlayerManager.suggestedPlaylistName if (playlistName != null) { playlistNameView.setText(playlistName) } else { 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 9dfcc1ef..e8a26e58 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -42,7 +42,7 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.model.SearchListModel import org.moire.ultrasonic.service.DownloadService -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo @@ -63,7 +63,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { private var searchRefresh: SwipeRefreshLayout? = null private var searchView: SearchView? = null - private val mediaPlayerController: MediaPlayerController by inject() + private val mediaPlayerManager: MediaPlayerManager by inject() private val shareHandler: ShareHandler by inject() private val networkAndStorageChecker: NetworkAndStorageChecker by inject() @@ -305,15 +305,15 @@ class SearchFragment : MultiListFragment(), KoinComponent { private fun onSongSelected(song: Track, append: Boolean) { if (!append) { - mediaPlayerController.clear() + mediaPlayerManager.clear() } - mediaPlayerController.addToPlaylist( + mediaPlayerManager.addToPlaylist( listOf(song), autoPlay = false, shuffle = false, - insertionMode = MediaPlayerController.InsertionMode.APPEND + insertionMode = MediaPlayerManager.InsertionMode.APPEND ) - mediaPlayerController.play(mediaPlayerController.mediaItemCount - 1) + mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1) toast(context, resources.getQuantityString(R.plurals.select_album_n_songs_added, 1, 1)) } 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 295885ea..700bde05 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -28,7 +28,7 @@ import org.moire.ultrasonic.log.FileLoggerTree.Companion.getLogFileSizes import org.moire.ultrasonic.log.FileLoggerTree.Companion.plantToTimberForest import org.moire.ultrasonic.log.FileLoggerTree.Companion.uprootFromTimberForest import org.moire.ultrasonic.provider.SearchSuggestionProvider -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.util.ConfirmationDialog import org.moire.ultrasonic.util.Constants @@ -62,7 +62,7 @@ class SettingsFragment : private var debugLogToFile: CheckBoxPreference? = null private var customCacheLocation: CheckBoxPreference? = null - private val mediaPlayerController: MediaPlayerController by inject() + private val mediaPlayerManager: MediaPlayerManager by inject() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.settings, rootKey) @@ -342,7 +342,7 @@ class SettingsFragment : Settings.cacheLocationUri = path // Clear download queue. - mediaPlayerController.clear() + mediaPlayerManager.clear() Storage.reset() Storage.ensureRootIsAvailable() } 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 48f39ffb..9e03df83 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -40,7 +40,7 @@ import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.model.TrackCollectionModel -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.subsonic.DownloadAction @@ -82,7 +82,7 @@ open class TrackCollectionFragment( private var playAllButton: MenuItem? = null private var shareButton: MenuItem? = null - internal val mediaPlayerController: MediaPlayerController by inject() + internal val mediaPlayerManager: MediaPlayerManager by inject() private val shareHandler: ShareHandler by inject() internal var cancellationToken: CancellationToken? = null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt index c7e51723..622f7b99 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt @@ -10,7 +10,7 @@ package org.moire.ultrasonic.model import android.app.Application import androidx.lifecycle.AndroidViewModel import java.io.IOException -import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.flatMapMerge @@ -90,7 +90,7 @@ class EditServerModel(val app: Application) : AndroidViewModel(app), KoinCompone } } - @OptIn(FlowPreview::class) + @OptIn(ExperimentalCoroutinesApi::class) suspend fun queryFeatureSupport(currentServerSetting: ServerSetting): Flow { val client = buildTestClient(currentServerSetting) // One line of magic: 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 d4f12bbc..456f23ef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -19,7 +19,6 @@ 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.session.LibraryResult import androidx.media3.session.MediaLibraryService @@ -47,7 +46,7 @@ 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.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.RatingManager import org.moire.ultrasonic.util.MainThreadExecutor @@ -101,10 +100,10 @@ const val PLAY_COMMAND = "play " */ @Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember") @SuppressLint("UnsafeOptInUsageError") -class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibraryService) : +class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : MediaLibraryService.MediaLibrarySession.Callback, KoinComponent { - private val mediaPlayerController by inject() + private val mediaPlayerManager by inject() private val activeServerProvider: ActiveServerProvider by inject() private val serviceJob = SupervisorJob() @@ -241,7 +240,7 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr * is stored in the track.starred value * See https://github.com/androidx/media/issues/33 */ - val track = mediaPlayerController.currentMediaItem?.toTrack() + val track = mediaPlayerManager.currentMediaItem?.toTrack() if (track != null) { customCommandFuture = onSetRating( session, @@ -254,12 +253,13 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr override fun onSuccess(result: SessionResult) { track.starred = !track.starred // This needs to be called on the main Thread + // TODO: This is a looping reference libraryService.onUpdateNotification(session) } override fun onFailure(t: Throwable) { Toast.makeText( - mediaPlayerController.context, + mediaPlayerManager.context, "There was an error updating the rating", LENGTH_SHORT ).show() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt index 65674111..0f283267 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt @@ -20,7 +20,7 @@ import com.google.common.collect.ImmutableList import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.R -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.util.toTrack @UnstableApi @@ -33,7 +33,7 @@ class CustomNotificationProvider(ctx: Context) : * is stored in the track.starred value. See https://github.com/androidx/media/issues/33 * TODO: Once the bug is fixed remove this circular reference! */ - private val mediaPlayerController by inject() + private val mediaPlayerManager by inject() override fun addNotificationActions( mediaSession: MediaSession, @@ -48,7 +48,7 @@ class CustomNotificationProvider(ctx: Context) : * is stored in the track.starred value * See https://github.com/androidx/media/issues/33 */ - val rating = mediaPlayerController.currentMediaItem?.toTrack()?.starred?.let { + val rating = mediaPlayerManager.currentMediaItem?.toTrack()?.starred?.let { HeartRating( it ) 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 7603f94f..6d0f0db6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -26,8 +26,7 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory -import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder -import androidx.media3.exoplayer.source.ShuffleOrder.UnshuffledShuffleOrder +import androidx.media3.exoplayer.source.ShuffleOrder import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -37,6 +36,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import okhttp3.OkHttpClient import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.app.UApp @@ -46,6 +46,8 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider import org.moire.ultrasonic.service.DownloadService +import org.moire.ultrasonic.service.JukeboxMediaPlayer +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign @@ -61,9 +63,10 @@ class PlaybackService : MediaLibraryService(), KoinComponent, CoroutineScope by CoroutineScope(Dispatchers.IO) { - private lateinit var player: ExoPlayer + private lateinit var player: Player private lateinit var mediaLibrarySession: MediaLibrarySession private var equalizer: EqualizerController? = null + private val activeServerProvider: ActiveServerProvider by inject() private lateinit var librarySessionCallback: MediaLibrarySession.Callback @@ -76,6 +79,7 @@ class PlaybackService : super.onCreate() initializeSessionAndPlayer() setListener(MediaSessionServiceListener()) + instance = this } private fun getWakeModeFlag(): Int { @@ -99,6 +103,7 @@ class PlaybackService : } private fun releasePlayerAndSession() { + Timber.i("Releasing player and session") // Broadcast that the service is being shutdown RxBus.stopServiceCommandPublisher.onNext(Unit) @@ -127,6 +132,91 @@ class PlaybackService : setMediaNotificationProvider(CustomNotificationProvider(UApp.applicationContext())) + player = if (activeServerProvider.getActiveServer().jukeboxByDefault) { + Timber.i("Jukebox enabled by default") + getJukeboxPlayer() + } else { + getLocalPlayer() + } + + // Create browser interface + librarySessionCallback = AutoMediaBrowserCallback(this) + + // This will need to use the AutoCalls + mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) + .setSessionActivity(getPendingIntentForContent()) + .setBitmapLoader(ArtworkBitmapLoader()) + .build() + + // Set a listener to update the API client when the active server has changed + rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { + // Set the player wake mode + (player as? ExoPlayer)?.setWakeMode(getWakeModeFlag()) + } + + // Set a listener to reset the ShuffleOrder + rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle -> + // This only applies for local playback + val exo = if (player is ExoPlayer) { + player as ExoPlayer + } else { + return@subscribe + } + val len = player.currentTimeline.windowCount + + Timber.i("Resetting shuffle order, isShuffled: %s", shuffle) + + // If disabling Shuffle return early + if (!shuffle) { + return@subscribe exo.setShuffleOrder( + ShuffleOrder.UnshuffledShuffleOrder(len) + ) + } + + // Get the position of the current track in the unshuffled order + val cur = player.currentMediaItemIndex + val seed = System.currentTimeMillis() + val random = Random(seed) + + val list = createShuffleListFromCurrentIndex(cur, len, random) + Timber.i("New Shuffle order: %s", list.joinToString { it.toString() }) + exo.setShuffleOrder(ShuffleOrder.DefaultShuffleOrder(list, seed)) + } + + // Listen to the shutdown command + rxBusSubscription += RxBus.shutdownCommandObservable.subscribe { + Timber.i("Received destroy command via Rx") + onDestroy() + } + + player.addListener(listener) + isStarted = true + } + + private fun updateBackend(newBackend: MediaPlayerManager.PlayerBackend) { + Timber.i("Switching player backends") + // Remove old listeners + player.removeListener(listener) + player.release() + + player = if (newBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) { + getJukeboxPlayer() + } else { + getLocalPlayer() + } + + // Add fresh listeners + player.addListener(listener) + + mediaLibrarySession.player = player + actualBackend = newBackend + } + + private fun getJukeboxPlayer(): Player { + return JukeboxMediaPlayer() + } + + private fun getLocalPlayer(): Player { // Create a new plain OkHttpClient val builder = OkHttpClient.Builder() val client = builder.build() @@ -147,7 +237,7 @@ class PlaybackService : renderer.setEnableAudioOffload(true) // Create the player - player = ExoPlayer.Builder(this) + val player = ExoPlayer.Builder(this) .setAudioAttributes(getAudioAttributes(), true) .setWakeMode(getWakeModeFlag()) .setHandleAudioBecomingNoisy(true) @@ -157,59 +247,17 @@ class PlaybackService : .setSeekForwardIncrementMs(Settings.seekInterval.toLong()) .build() + // Setup Equalizer equalizer = EqualizerController.create(player.audioSessionId) // Enable audio offload if (Settings.useHwOffload) player.experimentalSetOffloadSchedulingEnabled(true) - // Create browser interface - librarySessionCallback = AutoMediaBrowserCallback(player, this) - - // This will need to use the AutoCalls - mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) - .setSessionActivity(getPendingIntentForContent()) - .setBitmapLoader(ArtworkBitmapLoader()) - .build() - - // Set a listener to update the API client when the active server has changed - rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { - // Set the player wake mode - player.setWakeMode(getWakeModeFlag()) - } - - // Set a listener to reset the ShuffleOrder - rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle -> - val len = player.currentTimeline.windowCount - - Timber.i("Resetting shuffle order, isShuffled: %s", shuffle) - - // If disabling Shuffle return early - if (!shuffle) { - return@subscribe player.setShuffleOrder(UnshuffledShuffleOrder(len)) - } - - // Get the position of the current track in the unshuffled order - val cur = player.currentMediaItemIndex - val seed = System.currentTimeMillis() - val random = Random(seed) - - val list = createShuffleListFromCurrentIndex(cur, len, random) - Timber.i("New Shuffle order: %s", list.joinToString { it.toString() }) - player.setShuffleOrder(DefaultShuffleOrder(list, seed)) - } - - // Listen to the shutdown command - rxBusSubscription += RxBus.shutdownCommandObservable.subscribe { - Timber.i("Received destroy command via Rx") - onDestroy() - } - - player.addListener(listener) - isStarted = true + return player } - fun createShuffleListFromCurrentIndex( + private fun createShuffleListFromCurrentIndex( currentIndex: Int, length: Int, random: Random @@ -244,6 +292,7 @@ class PlaybackService : } private fun cacheNextSongs() { + if (actualBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) return Timber.d("PlaybackService caching the next songs") val nextSongs = Util.getPlayListFromTimeline( player.currentTimeline, @@ -333,6 +382,16 @@ class PlaybackService : } companion object { + var actualBackend: MediaPlayerManager.PlayerBackend? = null + + private var desiredBackend: MediaPlayerManager.PlayerBackend? = null + fun setBackend(playerBackend: MediaPlayerManager.PlayerBackend) { + desiredBackend = playerBackend + instance?.updateBackend(playerBackend) + } + + var instance: PlaybackService? = null + private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic.error" private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic error messages" private const val NOTIFICATION_ID = 3009 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 d8550f09..bb67ab9b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt @@ -7,29 +7,12 @@ package org.moire.ultrasonic.service import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.content.pm.ServiceInfo -import android.os.Build import android.os.Handler -import android.os.IBinder import android.os.Looper -import android.view.Gravity -import android.view.KeyEvent -import android.view.KeyEvent.KEYCODE_MEDIA_NEXT -import android.view.KeyEvent.KEYCODE_MEDIA_PAUSE -import android.view.KeyEvent.KEYCODE_MEDIA_PLAY -import android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -import android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS -import android.view.KeyEvent.KEYCODE_MEDIA_STOP -import android.view.LayoutInflater -import android.view.View -import android.widget.ProgressBar -import android.widget.Toast -import androidx.core.app.NotificationManagerCompat import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.DeviceInfo +import androidx.media3.common.FlagSet import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.PlaybackException @@ -39,34 +22,27 @@ import androidx.media3.common.Timeline import androidx.media3.common.TrackSelectionParameters import androidx.media3.common.VideoSize import androidx.media3.common.text.CueGroup +import androidx.media3.common.util.Clock +import androidx.media3.common.util.ListenerSet import androidx.media3.common.util.Size -import androidx.media3.session.MediaSession -import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.ListenableFuture -import com.google.common.util.concurrent.SettableFuture import java.util.concurrent.Executors import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicLong -import kotlin.math.roundToInt import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.api.subsonic.SubsonicRESTException import org.moire.ultrasonic.app.UApp.Companion.applicationContext import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.JukeboxStatus -import org.moire.ultrasonic.playback.CustomNotificationProvider import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.Util.getPendingIntentToShowPlayer +import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util.sleepQuietly -import org.moire.ultrasonic.util.Util.stopForegroundRemoveNotification import timber.log.Timber private const val STATUS_UPDATE_INTERVAL_SECONDS = 5L -private const val SEEK_INCREMENT_SECONDS = 5L -private const val SEEK_START_AFTER_SECONDS = 5 private const val QUEUE_POLL_INTERVAL_SECONDS = 1L /** @@ -86,135 +62,64 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { private val timeOfLastUpdate = AtomicLong() private var jukeboxStatus: JukeboxStatus? = null private var previousJukeboxStatus: JukeboxStatus? = null - private var gain = 0.5f - private var volumeToast: VolumeToast? = null + private var gain = (MAX_GAIN / 3) + private val floatGain: Float + get() = gain.toFloat() / MAX_GAIN + private var serviceThread: Thread? = null - private var listeners: MutableList = mutableListOf() + private var listeners: ListenerSet private val playlist: MutableList = mutableListOf() - private var currentIndex: Int = 0 - private val notificationProvider = CustomNotificationProvider(applicationContext()) - private lateinit var mediaSession: MediaSession - private lateinit var notificationManagerCompat: NotificationManagerCompat - @Suppress("MagicNumber") - override fun onCreate() { - super.onCreate() - if (running.get()) return + private var _currentIndex: Int = 0 + private var currentIndex: Int + get() = _currentIndex + set(value) { + // This must never be smaller 0 + _currentIndex = if (value >= 0) value else 0 + } + + 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 running = AtomicBoolean() + const val MAX_GAIN = 10 + } + + init { running.set(true) + listeners = ListenerSet( + applicationLooper, + Clock.DEFAULT + ) { listener: Player.Listener, flags: FlagSet? -> + listener.onEvents( + this, + Player.Events( + flags!! + ) + ) + } tasks.clear() updatePlaylist() stop() - - startFuture?.set(this) - startProcessTasks() - - notificationManagerCompat = NotificationManagerCompat.from(this) - mediaSession = MediaSession.Builder(applicationContext(), this) - .setId("jukebox") - .setSessionActivity(getPendingIntentToShowPlayer(this)) - .build() - val notification = notificationProvider.createNotification( - mediaSession, - ImmutableList.of(), - JukeboxNotificationActionFactory() - ) {} - - if (Build.VERSION.SDK_INT >= 29) { - startForeground( - notification.notificationId, - notification.notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK - ) - } else { - startForeground( - notification.notificationId, notification.notification - ) - } - - Timber.d("Started Jukebox Service") } + @Suppress("MagicNumber") - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - if (Intent.ACTION_MEDIA_BUTTON != intent?.action) return START_STICKY - - val extras = intent.extras - if ((extras != null) && extras.containsKey(Intent.EXTRA_KEY_EVENT)) { - val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - extras.getParcelable(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) - } else { - @Suppress("DEPRECATION") - extras.getParcelable(Intent.EXTRA_KEY_EVENT) - } - when (event?.keyCode) { - KEYCODE_MEDIA_PLAY -> play() - KEYCODE_MEDIA_PAUSE -> stop() - KEYCODE_MEDIA_STOP -> stop() - KEYCODE_MEDIA_PLAY_PAUSE -> if (isPlaying) stop() else play() - KEYCODE_MEDIA_PREVIOUS -> seekToPrevious() - KEYCODE_MEDIA_NEXT -> seekToNext() - } - } - return START_STICKY - } - - override fun onDestroy() { + override fun release() { tasks.clear() stop() if (!running.get()) return running.set(false) - serviceThread!!.join() + serviceThread?.join() - stopForegroundRemoveNotification() - mediaSession.release() - - super.onDestroy() Timber.d("Stopped Jukebox Service") } - override fun onBind(p0: Intent?): IBinder? { - return null - } - - fun requestStop() { - stopSelf() - } - - private fun updateNotification() { - val notification = notificationProvider.createNotification( - mediaSession, - ImmutableList.of(), - JukeboxNotificationActionFactory() - ) {} - notificationManagerCompat.notify(notification.notificationId, notification.notification) - } - - companion object { - val running = AtomicBoolean() - private var startFuture: SettableFuture? = null - - @JvmStatic - fun requestStart(): ListenableFuture? { - if (running.get()) return null - startFuture = SettableFuture.create() - val context = applicationContext() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService( - Intent(context, JukeboxMediaPlayer::class.java) - ) - } else { - context.startService(Intent(context, JukeboxMediaPlayer::class.java)) - } - Timber.i("JukeboxMediaPlayer starting...") - return startFuture - } - } - override fun addListener(listener: Player.Listener) { listeners.add(listener) } @@ -263,14 +168,20 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } tasks.add(Skip(mediaItemIndex, positionSeconds)) currentIndex = mediaItemIndex + updateAvailableCommands() } override fun seekBack() { - seekTo(0L.coerceAtMost((jukeboxStatus?.positionSeconds ?: 0) - SEEK_INCREMENT_SECONDS)) + seekTo( + 0L.coerceAtMost( + (jukeboxStatus?.positionSeconds ?: 0) - + Settings.seekIntervalMillis + ) + ) } override fun seekForward() { - seekTo((jukeboxStatus?.positionSeconds ?: 0) + SEEK_INCREMENT_SECONDS) + seekTo((jukeboxStatus?.positionSeconds ?: 0) + Settings.seekIntervalMillis) } override fun isCurrentMediaItemSeekable() = true @@ -292,8 +203,11 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { override fun getAvailableCommands(): Player.Commands { val commandsBuilder = Player.Commands.Builder().addAll( - Player.COMMAND_SET_VOLUME, - Player.COMMAND_GET_VOLUME + Player.COMMAND_CHANGE_MEDIA_ITEMS, + Player.COMMAND_GET_TIMELINE, + Player.COMMAND_GET_DEVICE_VOLUME, + Player.COMMAND_ADJUST_DEVICE_VOLUME, + Player.COMMAND_SET_DEVICE_VOLUME ) if (isPlaying) commandsBuilder.add(Player.COMMAND_STOP) if (playlist.isNotEmpty()) { @@ -306,8 +220,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { Player.COMMAND_SEEK_FORWARD, Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, Player.COMMAND_SEEK_TO_MEDIA_ITEM, - ) - if (currentIndex > 0) commandsBuilder.addAll( + // Seeking back is always available Player.COMMAND_SEEK_TO_PREVIOUS, Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM ) @@ -323,6 +236,18 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { return availableCommands.contains(command) } + private fun updateAvailableCommands() { + Handler(Looper.getMainLooper()).post { + listeners.sendEvent( + Player.EVENT_AVAILABLE_COMMANDS_CHANGED + ) { listener: Player.Listener -> + listener.onAvailableCommandsChanged( + availableCommands + ) + } + } + } + override fun getPlayWhenReady(): Boolean { return isPlaying } @@ -358,21 +283,43 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {} - override fun setVolume(volume: Float) { + override fun setDeviceVolume(volume: Int) { gain = volume tasks.remove(SetGain::class.java) - tasks.add(SetGain(volume)) - val context = applicationContext() - if (volumeToast == null) volumeToast = VolumeToast(context) - volumeToast!!.setVolume(volume) + tasks.add(SetGain(floatGain)) + + // We must trigger an event so that the Controller knows the new volume + Handler(Looper.getMainLooper()).post { + listeners.queueEvent(Player.EVENT_DEVICE_VOLUME_CHANGED) { + it.onDeviceVolumeChanged( + gain, + false + ) + } + } + } + + override fun increaseDeviceVolume() { + gain = (gain + 1).coerceAtMost(MAX_GAIN) + deviceVolume = gain + } + + override fun decreaseDeviceVolume() { + gain = (gain - 1).coerceAtLeast(0) + deviceVolume = gain + } + + override fun setDeviceMuted(muted: Boolean) { + gain = 0 + deviceVolume = gain } override fun getVolume(): Float { - return gain + return floatGain } override fun getDeviceVolume(): Int { - return (gain * 100).toInt() + return gain } override fun addMediaItems(index: Int, mediaItems: MutableList) { @@ -444,7 +391,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } override fun seekToPrevious() { - if ((jukeboxStatus?.positionSeconds ?: 0) > SEEK_START_AFTER_SECONDS) { + if ((jukeboxStatus?.positionSeconds ?: 0) > (Settings.seekIntervalMillis)) { seekTo(currentIndex, 0) return } @@ -499,51 +446,63 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { @Suppress("LoopWithTooManyJumpStatements") private fun processTasks() { Timber.d("JukeboxMediaPlayer processTasks starting") - while (true) { + while (running.get()) { // Sleep a bit to spare processor time if we loop a lot sleepQuietly(10) // This is only necessary if Ultrasonic goes offline sooner than the thread stops if (isOffline()) continue var task: JukeboxTask? = null try { - task = tasks.poll() - // If running is false, exit when the queue is empty - if (task == null && !running.get()) break - if (task == null) continue + task = tasks.poll() ?: continue Timber.v("JukeBoxMediaPlayer processTasks processes Task %s", task::class) val status = task.execute() onStatusUpdate(status) - } catch (x: Throwable) { - onError(task, x) + } catch (all: Throwable) { + onError(task, all) } } Timber.d("JukeboxMediaPlayer processTasks stopped") } + // Jukebox status contains data received from the server, we need to validate it! private fun onStatusUpdate(jukeboxStatus: JukeboxStatus) { timeOfLastUpdate.set(System.currentTimeMillis()) previousJukeboxStatus = this.jukeboxStatus this.jukeboxStatus = jukeboxStatus + var shouldUpdateCommands = false + + // Ensure that the index is never smaller than 0 + // If -1 assume that this means we are not playing + if (jukeboxStatus.currentPlayingIndex != null && jukeboxStatus.currentPlayingIndex!! < 0) { + jukeboxStatus.currentPlayingIndex = 0 + jukeboxStatus.isPlaying = false + } currentIndex = jukeboxStatus.currentPlayingIndex ?: currentIndex if (jukeboxStatus.isPlaying != previousJukeboxStatus?.isPlaying) { + shouldUpdateCommands = true Handler(Looper.getMainLooper()).post { - listeners.forEach { + listeners.queueEvent(Player.EVENT_PLAYBACK_STATE_CHANGED) { it.onPlaybackStateChanged( if (jukeboxStatus.isPlaying) Player.STATE_READY else Player.STATE_IDLE ) + } + + listeners.queueEvent(Player.EVENT_IS_PLAYING_CHANGED) { it.onIsPlayingChanged(jukeboxStatus.isPlaying) } } } if (jukeboxStatus.currentPlayingIndex != previousJukeboxStatus?.currentPlayingIndex) { + shouldUpdateCommands = true currentIndex = jukeboxStatus.currentPlayingIndex ?: 0 val currentMedia = if (currentIndex > 0 && currentIndex < playlist.size) playlist[currentIndex] else MediaItem.EMPTY + Handler(Looper.getMainLooper()).post { - listeners.forEach { + listeners.queueEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) { it.onMediaItemTransition( currentMedia, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK @@ -552,44 +511,39 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } } - updateNotification() + if (shouldUpdateCommands) updateAvailableCommands() + + Handler(Looper.getMainLooper()).post { + listeners.flushEvents() + } } private fun onError(task: JukeboxTask?, x: Throwable) { + var exception: PlaybackException? = null if (x is ApiNotSupportedException && task !is Stop) { - Handler(Looper.getMainLooper()).post { - listeners.forEach { - it.onPlayerError( - PlaybackException( - "Jukebox server too old", - null, - R.string.download_jukebox_server_too_old - ) - ) - } - } + exception = PlaybackException( + "Jukebox server too old", + null, + R.string.download_jukebox_server_too_old + ) } else if (x is OfflineException && task !is Stop) { - Handler(Looper.getMainLooper()).post { - listeners.forEach { - it.onPlayerError( - PlaybackException( - "Jukebox offline", - null, - R.string.download_jukebox_offline - ) - ) - } - } + exception = PlaybackException( + "Jukebox offline", + null, + R.string.download_jukebox_offline + ) } else if (x is SubsonicRESTException && x.code == 50 && task !is Stop) { + exception = PlaybackException( + "Jukebox not authorized", + null, + R.string.download_jukebox_not_authorized + ) + } + + if (exception != null) { Handler(Looper.getMainLooper()).post { - listeners.forEach { - it.onPlayerError( - PlaybackException( - "Jukebox not authorized", - null, - R.string.download_jukebox_not_authorized - ) - ) + listeners.sendEvent(Player.EVENT_PLAYER_ERROR) { + it.onPlayerError(exception) } } } else { @@ -608,8 +562,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } tasks.add(SetPlaylist(ids)) Handler(Looper.getMainLooper()).post { - listeners.forEach { - it.onTimelineChanged( + listeners.sendEvent( + Player.EVENT_TIMELINE_CHANGED + ) { listener: Player.Listener -> + listener.onTimelineChanged( PlaylistTimeline(playlist), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED ) @@ -719,25 +675,6 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } } - @SuppressLint("InflateParams") - private class VolumeToast(context: Context) : Toast(context) { - private val progressBar: ProgressBar - fun setVolume(volume: Float) { - progressBar.progress = (100 * volume).roundToInt() - show() - } - - init { - duration = LENGTH_SHORT - val inflater = - context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - val view = inflater.inflate(R.layout.jukebox_volume, null) - progressBar = view.findViewById(R.id.jukebox_volume_progress_bar) as ProgressBar - setView(view) - setGravity(Gravity.TOP, 0, 0) - } - } - // The constants below are necessary so a MediaSession can be built from the Jukebox Service override fun isCurrentMediaItemDynamic(): Boolean { return false @@ -748,15 +685,15 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } override fun getMaxSeekToPreviousPosition(): Long { - return SEEK_START_AFTER_SECONDS * 1000L + return Settings.seekInterval.toLong() } override fun getSeekBackIncrement(): Long { - return SEEK_INCREMENT_SECONDS * 1000L + return Settings.seekInterval.toLong() } override fun getSeekForwardIncrement(): Long { - return SEEK_INCREMENT_SECONDS * 1000L + return Settings.seekInterval.toLong() } override fun isLoading(): Boolean { @@ -779,6 +716,8 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { return AudioAttributes.DEFAULT } + override fun setVolume(volume: Float) {} + override fun getVideoSize(): VideoSize { return VideoSize(0, 0) } @@ -824,7 +763,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } override fun getDeviceInfo(): DeviceInfo { - return DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 1) + return DEVICE_INFO } override fun getPlayerError(): PlaybackException? { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxNotificationActionFactory.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxNotificationActionFactory.kt deleted file mode 100644 index 73bad2cb..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxNotificationActionFactory.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * JukeboxNotificationActionFactory.kt - * Copyright (C) 2009-2022 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ - -package org.moire.ultrasonic.service - -import android.annotation.SuppressLint -import android.app.PendingIntent -import android.content.ComponentName -import android.content.Intent -import android.os.Bundle -import android.view.KeyEvent -import androidx.core.app.NotificationCompat -import androidx.core.graphics.drawable.IconCompat -import androidx.media3.common.Player -import androidx.media3.common.util.Util -import androidx.media3.session.CommandButton -import androidx.media3.session.MediaNotification -import androidx.media3.session.MediaSession -import org.moire.ultrasonic.app.UApp - -/** - * This class creates Intents and Actions to be used with the Media Notification - * of the Jukebox Service - */ -@SuppressLint("UnsafeOptInUsageError") -class JukeboxNotificationActionFactory : MediaNotification.ActionFactory { - override fun createMediaAction( - mediaSession: MediaSession, - icon: IconCompat, - title: CharSequence, - command: Int - ): NotificationCompat.Action { - return NotificationCompat.Action( - icon, title, createMediaActionPendingIntent(mediaSession, command.toLong()) - ) - } - - override fun createCustomAction( - mediaSession: MediaSession, - icon: IconCompat, - title: CharSequence, - customAction: String, - extras: Bundle - ): NotificationCompat.Action { - return NotificationCompat.Action( - icon, title, null - ) - } - - override fun createCustomActionFromCustomCommandButton( - mediaSession: MediaSession, - customCommandButton: CommandButton - ): NotificationCompat.Action { - return NotificationCompat.Action(null, null, null) - } - - @Suppress("MagicNumber") - override fun createMediaActionPendingIntent( - mediaSession: MediaSession, - command: Long - ): PendingIntent { - val keyCode: Int = toKeyCode(command) - val intent = Intent(Intent.ACTION_MEDIA_BUTTON) - intent.component = ComponentName(UApp.applicationContext(), JukeboxMediaPlayer::class.java) - intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keyCode)) - return if (Util.SDK_INT >= 26 && command == Player.COMMAND_PLAY_PAUSE.toLong()) { - return PendingIntent.getForegroundService( - UApp.applicationContext(), keyCode, intent, PendingIntent.FLAG_IMMUTABLE - ) - } else { - PendingIntent.getService( - UApp.applicationContext(), - keyCode, - intent, - if (Util.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0 - ) - } - } - - private fun toKeyCode(action: @Player.Command Long): Int { - return when (action.toInt()) { - Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, - Player.COMMAND_SEEK_TO_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT - Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, - Player.COMMAND_SEEK_TO_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS - Player.COMMAND_STOP -> KeyEvent.KEYCODE_MEDIA_STOP - Player.COMMAND_SEEK_FORWARD -> KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - Player.COMMAND_SEEK_BACK -> KeyEvent.KEYCODE_MEDIA_REWIND - Player.COMMAND_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE - else -> KeyEvent.KEYCODE_UNKNOWN - } - } -} 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 eeba3b52..a22111be 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt @@ -8,7 +8,6 @@ package org.moire.ultrasonic.service import android.annotation.SuppressLint -import android.app.Service import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView @@ -26,7 +25,7 @@ import androidx.media3.common.Tracks */ @Suppress("TooManyFunctions") @SuppressLint("UnsafeOptInUsageError") -abstract class JukeboxUnimplementedFunctions : Service(), Player { +abstract class JukeboxUnimplementedFunctions : Player { override fun setMediaItems(mediaItems: MutableList) { TODO("Not yet implemented") @@ -140,10 +139,6 @@ abstract class JukeboxUnimplementedFunctions : Service(), Player { TODO("Not yet implemented") } - override fun release() { - TODO("Not yet implemented") - } - override fun getCurrentTracks(): Tracks { // TODO Dummy information is returned for now, this seems to work return Tracks.EMPTY @@ -228,20 +223,4 @@ abstract class JukeboxUnimplementedFunctions : Service(), Player { override fun clearVideoTextureView(textureView: TextureView?) { TODO("Not yet implemented") } - - override fun setDeviceVolume(volume: Int) { - TODO("Not yet implemented") - } - - override fun increaseDeviceVolume() { - TODO("Not yet implemented") - } - - override fun decreaseDeviceVolume() { - TODO("Not yet implemented") - } - - override fun setDeviceMuted(muted: Boolean) { - TODO("Not yet implemented") - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 3c5d4df3..8c64d9a0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -32,7 +32,7 @@ import timber.log.Timber class MediaPlayerLifecycleSupport : KoinComponent { private lateinit var ratingManager: RatingManager private val playbackStateSerializer by inject() - private val mediaPlayerController by inject() + private val mediaPlayerManager by inject() private val imageLoaderProvider: ImageLoaderProvider by inject() private var created = false @@ -64,7 +64,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { return } - mediaPlayerController.onCreate { + mediaPlayerManager.onCreate { restoreLastSession(autoPlay, afterRestore) } @@ -81,7 +81,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { Timber.i("Restoring %s songs", it!!.songs.size) - mediaPlayerController.restore( + mediaPlayerManager.restore( it, autoPlay, false @@ -110,7 +110,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { if (intent == null) return val intentAction = intent.action - if (intentAction == null || intentAction.isEmpty()) return + if (intentAction.isNullOrEmpty()) return Timber.i("Received intent: %s", intentAction) @@ -146,15 +146,15 @@ class MediaPlayerLifecycleSupport : KoinComponent { val state = extras.getInt("state") if (state == 0) { - if (!mediaPlayerController.isJukeboxEnabled) { - mediaPlayerController.pause() + if (!mediaPlayerManager.isJukeboxEnabled) { + mediaPlayerManager.pause() } } else if (state == 1) { - if (!mediaPlayerController.isJukeboxEnabled && - Settings.resumePlayOnHeadphonePlug && !mediaPlayerController.isPlaying + if (!mediaPlayerManager.isJukeboxEnabled && + Settings.resumePlayOnHeadphonePlug && !mediaPlayerManager.isPlaying ) { - mediaPlayerController.prepare() - mediaPlayerController.play() + mediaPlayerManager.prepare() + mediaPlayerManager.play() } } } @@ -183,18 +183,18 @@ class MediaPlayerLifecycleSupport : KoinComponent { onCreate(autoStart) { when (keyCode) { KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, - KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause() - KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.seekToPrevious() - KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.seekToNext() - KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop() - KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play() - KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause() - KeyEvent.KEYCODE_1 -> mediaPlayerController.legacySetRating(1) - KeyEvent.KEYCODE_2 -> mediaPlayerController.legacySetRating(2) - KeyEvent.KEYCODE_3 -> mediaPlayerController.legacySetRating(3) - KeyEvent.KEYCODE_4 -> mediaPlayerController.legacySetRating(4) - KeyEvent.KEYCODE_5 -> mediaPlayerController.legacySetRating(5) - KeyEvent.KEYCODE_STAR -> mediaPlayerController.legacyToggleStar() + KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerManager.togglePlayPause() + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerManager.seekToPrevious() + KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerManager.seekToNext() + KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerManager.stop() + KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerManager.play() + KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerManager.pause() + KeyEvent.KEYCODE_1 -> mediaPlayerManager.legacySetRating(1) + KeyEvent.KEYCODE_2 -> mediaPlayerManager.legacySetRating(2) + KeyEvent.KEYCODE_3 -> mediaPlayerManager.legacySetRating(3) + KeyEvent.KEYCODE_4 -> mediaPlayerManager.legacySetRating(4) + KeyEvent.KEYCODE_5 -> mediaPlayerManager.legacySetRating(5) + KeyEvent.KEYCODE_STAR -> mediaPlayerManager.legacyToggleStar() else -> { } } @@ -222,17 +222,17 @@ class MediaPlayerLifecycleSupport : KoinComponent { // We can receive intents when everything is stopped, so we need to start onCreate(autoStart) { when (action) { - Constants.CMD_PLAY -> mediaPlayerController.play() + Constants.CMD_PLAY -> mediaPlayerManager.play() Constants.CMD_RESUME_OR_PLAY -> // If Ultrasonic wasn't running, the autoStart is enough to resume, // no need to call anything - if (isRunning) mediaPlayerController.resumeOrPlay() + if (isRunning) mediaPlayerManager.resumeOrPlay() - Constants.CMD_NEXT -> mediaPlayerController.seekToNext() - Constants.CMD_PREVIOUS -> mediaPlayerController.seekToPrevious() - Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause() - Constants.CMD_STOP -> mediaPlayerController.stop() - Constants.CMD_PAUSE -> mediaPlayerController.pause() + Constants.CMD_NEXT -> mediaPlayerManager.seekToNext() + Constants.CMD_PREVIOUS -> mediaPlayerManager.seekToPrevious() + Constants.CMD_TOGGLEPAUSE -> mediaPlayerManager.togglePlayPause() + Constants.CMD_STOP -> mediaPlayerManager.stop() + Constants.CMD_PAUSE -> mediaPlayerManager.pause() } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt similarity index 87% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt index 2be0f26f..a580b3d3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt @@ -16,8 +16,6 @@ import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player -import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT -import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Rating @@ -50,12 +48,13 @@ private const val CONTROLLER_SWITCH_DELAY = 500L private const val VOLUME_DELTA = 0.05f /** - * The implementation of the Media Player Controller. + * The Media Player Manager can forward commands to the Media3 controller as + * well as switch between different player interfaces (local, remote, cast etc). * This class contains everything that is necessary for the Application UI * to control the Media Player implementation. */ @Suppress("TooManyFunctions") -class MediaPlayerController( +class MediaPlayerManager( private val playbackStateSerializer: PlaybackStateSerializer, private val externalStorageMonitor: ExternalStorageMonitor, val context: Context @@ -97,15 +96,15 @@ class MediaPlayerController( * We run the event through RxBus in order to throttle them */ override fun onTimelineChanged(timeline: Timeline, reason: Int) { - val start = controller?.currentTimeline?.getFirstWindowIndex(isShufflePlayEnabled) + val start = timeline.getFirstWindowIndex(isShufflePlayEnabled) Timber.w("On timeline changed. First shuffle play at index: %s", start) deferredPlay?.let { Timber.w("Executing deferred shuffle play") it() deferredPlay = null } - - RxBus.playlistPublisher.onNext(playlist.map(MediaItem::toTrack)) + val playlist = Util.getPlayListFromTimeline(timeline, false).map(MediaItem::toTrack) + RxBus.playlistPublisher.onNext(playlist) } override fun onPlaybackStateChanged(playbackState: Int) { @@ -179,11 +178,8 @@ class MediaPlayerController( fun onCreate(onCreated: () -> Unit) { if (created) return externalStorageMonitor.onCreate { reset() } - if (activeServerProvider.getActiveServer().jukeboxByDefault) { - switchToJukebox(onCreated) - } else { - switchToLocalPlayer(onCreated) - } + + createMediaController(onCreated) rxBusSubscription += RxBus.activeServerChangingObservable.subscribe { oldServer -> if (oldServer != OFFLINE_DB_ID) { @@ -195,8 +191,7 @@ class MediaPlayerController( 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. - releaseJukebox(controller) - controller = null + switchToLocalPlayer() } } @@ -246,6 +241,22 @@ class MediaPlayerController( Timber.i("MediaPlayerController started") } + private fun createMediaController(onCreated: () -> Unit) { + mediaControllerFuture = MediaController.Builder( + context, + sessionToken + ).buildAsync() + + mediaControllerFuture?.addListener({ + controller = mediaControllerFuture?.get() + + Timber.i("MediaController Instance received") + controller?.addListener(listeners) + onCreated() + Timber.i("MediaPlayerController creation complete") + }, MoreExecutors.directExecutor()) + } + private fun playerStateChangedHandler() { val currentPlaying = controller?.currentMediaItem?.toTrack() ?: return @@ -262,6 +273,10 @@ class MediaPlayerController( } } + fun addListener(listener: Player.Listener) { + controller?.addListener(listener) + } + private fun clearBookmark() { // This method is called just before we update the cachedMediaItem, // so in fact cachedMediaItem will refer to the track that has just finished. @@ -336,7 +351,6 @@ class MediaPlayerController( @Synchronized fun play(index: Int) { controller?.seekTo(index, 0L) - // FIXME CHECK ITS NOT MAKING PROBLEMS controller?.prepare() controller?.play() } @@ -538,7 +552,7 @@ class MediaPlayerController( @Synchronized fun canSeekToPrevious(): Boolean { - return controller?.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS) == true + return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS) == true } @Synchronized @@ -548,7 +562,7 @@ class MediaPlayerController( @Synchronized fun canSeekToNext(): Boolean { - return controller?.isCommandAvailable(COMMAND_SEEK_TO_NEXT) == true + return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT) == true } @Synchronized @@ -580,102 +594,49 @@ class MediaPlayerController( @set:Synchronized var isJukeboxEnabled: Boolean - get() = controller is JukeboxMediaPlayer - set(jukeboxEnabled) { - if (jukeboxEnabled) { - switchToJukebox {} + get() = PlaybackService.actualBackend == PlayerBackend.JUKEBOX + set(shouldEnable) { + if (shouldEnable) { + switchToJukebox() } else { - switchToLocalPlayer {} + switchToLocalPlayer() } } - private fun switchToJukebox(onCreated: () -> Unit) { - if (controller is JukeboxMediaPlayer) return - val currentPlaylist = playlist - val currentIndex = controller?.currentMediaItemIndex ?: 0 - val currentPosition = controller?.currentPosition ?: 0 + private fun switchToJukebox() { + if (isJukeboxEnabled) return + scheduleSwitchTo(PlayerBackend.JUKEBOX) DownloadService.requestStop() controller?.pause() controller?.stop() - val oldController = controller - controller = null // While we switch, the controller shouldn't be available - - // Stop() won't work if we don't give it time to be processed - Handler(Looper.getMainLooper()).postDelayed({ - if (oldController != null) releaseLocalPlayer(oldController) - setupJukebox { - controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition) - onCreated() - } - }, CONTROLLER_SWITCH_DELAY) } - private fun switchToLocalPlayer(onCreated: () -> Unit) { - if (controller is MediaController) return - val currentPlaylist = playlist + private fun switchToLocalPlayer() { + if (!isJukeboxEnabled) return + scheduleSwitchTo(PlayerBackend.LOCAL) + controller?.stop() + } + + private fun scheduleSwitchTo(newBackend: PlayerBackend) { + val currentPlaylist = playlist.toList() val currentIndex = controller?.currentMediaItemIndex ?: 0 val currentPosition = controller?.currentPosition ?: 0 - controller?.stop() - val oldController = controller - controller = null // While we switch, the controller shouldn't be available Handler(Looper.getMainLooper()).postDelayed({ - if (oldController != null) releaseJukebox(oldController) - setupLocalPlayer { - controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition) - onCreated() - } + // Change the backend + PlaybackService.setBackend(newBackend) + // Restore the media items + controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition) }, CONTROLLER_SWITCH_DELAY) } private fun releaseController() { - when (controller) { - null -> return - is JukeboxMediaPlayer -> releaseJukebox(controller) - is MediaController -> releaseLocalPlayer(controller) - } - } - - private fun setupLocalPlayer(onCreated: () -> Unit) { - mediaControllerFuture = MediaController.Builder( - context, - sessionToken - ).buildAsync() - - mediaControllerFuture?.addListener({ - controller = mediaControllerFuture?.get() - - Timber.i("MediaController Instance received") - controller?.addListener(listeners) - onCreated() - Timber.i("MediaPlayerController creation complete") - }, MoreExecutors.directExecutor()) - } - - private fun releaseLocalPlayer(player: Player?) { - player?.removeListener(listeners) - player?.release() + controller?.removeListener(listeners) + controller?.release() if (mediaControllerFuture != null) MediaController.releaseFuture(mediaControllerFuture!!) Timber.i("MediaPlayerController released") } - private fun setupJukebox(onCreated: () -> Unit) { - val jukeboxFuture = JukeboxMediaPlayer.requestStart() - jukeboxFuture?.addListener({ - controller = jukeboxFuture.get() - onCreated() - controller?.addListener(listeners) - Timber.i("JukeboxService creation complete") - }, MoreExecutors.directExecutor()) - } - - private fun releaseJukebox(player: Player?) { - val jukebox = player as JukeboxMediaPlayer? - jukebox?.removeListener(listeners) - jukebox?.requestStop() - Timber.i("JukeboxService released") - } - /** * This function calls the music service directly and * therefore can't be called from the main thread @@ -700,10 +661,6 @@ class MediaPlayerController( controller?.volume = gain } - fun setVolume(volume: Float) { - controller?.volume = volume - } - /* * Sets the rating of the current track */ @@ -841,4 +798,6 @@ class MediaPlayerController( enum class InsertionMode { CLEAR, APPEND, AFTER_CURRENT } + + enum class PlayerBackend { JUKEBOX, LOCAL } } 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 4ba976ed..5acf05be 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -18,7 +18,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.DownloadService -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.executeTaskWithToast @@ -28,7 +28,7 @@ import org.moire.ultrasonic.util.executeTaskWithToast */ @Suppress("LongParameterList") class DownloadHandler( - val mediaPlayerController: MediaPlayerController, + val mediaPlayerManager: MediaPlayerManager, private val networkAndStorageChecker: NetworkAndStorageChecker ) : CoroutineScope by CoroutineScope(Dispatchers.IO) { private val maxSongs = 500 @@ -150,16 +150,16 @@ class DownloadHandler( networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() val insertionMode = when { - append -> MediaPlayerController.InsertionMode.APPEND - playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT - else -> MediaPlayerController.InsertionMode.CLEAR + append -> MediaPlayerManager.InsertionMode.APPEND + playNext -> MediaPlayerManager.InsertionMode.AFTER_CURRENT + else -> MediaPlayerManager.InsertionMode.CLEAR } if (playlistName != null) { - mediaPlayerController.suggestedPlaylistName = playlistName + mediaPlayerManager.suggestedPlaylistName = playlistName } - mediaPlayerController.addToPlaylist( + mediaPlayerManager.addToPlaylist( songs, autoPlay, shuffle, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt index 675979f7..3ec2cee6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt @@ -19,7 +19,7 @@ import org.koin.java.KoinJavaComponent.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Playlist import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.util.FileUtil.getAlbumArtFile import org.moire.ultrasonic.util.FileUtil.getCompleteFile import org.moire.ultrasonic.util.FileUtil.getPartialFile @@ -235,8 +235,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinCompo private fun findFilesToNotDelete(): Set { val filesToNotDelete: MutableSet = HashSet(5) - val mediaController = inject( - MediaPlayerController::class.java + val mediaController = inject( + MediaPlayerManager::class.java ) val playlist = mainScope.future { mediaController.value.playlist }.get() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index e1f421f0..d3aa70f9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -128,6 +128,9 @@ object Settings { var seekInterval by StringIntSetting(getKey(R.string.setting_key_increment_time), 5000) + val seekIntervalMillis: Long + get() = (seekInterval / 1000).toLong() + @JvmStatic var mediaButtonsEnabled by BooleanSetting(getKey(R.string.setting_key_media_buttons), true) diff --git a/ultrasonic/src/main/res/layout/jukebox_volume.xml b/ultrasonic/src/main/res/layout/jukebox_volume.xml deleted file mode 100644 index 81adf6ad..00000000 --- a/ultrasonic/src/main/res/layout/jukebox_volume.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 0d215da8..c06ae0b5 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -48,7 +48,6 @@ Vzdálené ovládání není dostupné v offline módu. Vzdálené ovládání zapnuto. Hudba přehrávána na serveru. Vzdálené ovládání není podporováno. Aktualizujte svůj Subsonic server. - Hlasitost vzdáleného přístroje Ekvalizér Jukebox vypnut Jukebox zapnut diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index bb3b8559..728688b9 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -61,7 +61,6 @@ Fernbedienungs-Modus is Offline nicht verfügbar. Fernbedienung ausgeschaltet. Musik wird auf dem Server wiedergegeben. Fernbedienungs Modus wird nicht unterstützt. Bitte den Subsonic Server aktualisieren. - Entfernte Lautstärke Equalizer Jukebox Aus Jukebox An diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 04ac76bd..1a34b0e9 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -62,7 +62,6 @@ Control remoto no disponible en modo fuera de línea. Control remoto encendido. La música se reproduce en el servidor. Control remoto no soportado. Por favor actualiza tu servidor de Subsonic. - Volumen remoto Ecualizador Apagar Jukebox Encender Jukebox diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index b538dd39..735457b5 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -61,7 +61,6 @@ Le mode jukebox n\'est pas disponible en mode déconnecté. Mode jukebox activé. La musique est jouée sur le serveur Le mode jukebox n\'est pas pris en charge. Mise à jour du serveur Subsonic requise. - Volume sur serveur distant Égaliseur Désactiver le mode jukebox Activer le mode jukebox diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index ed40d385..f85bf6b3 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -54,7 +54,6 @@ A távvezérlés nem lehetséges kapcsolat nélküli módban! Távvezérlés bekapcsolása. A zenelejátszás a kiszolgálón történik. A távvezérlés nem támogatott. Kérjük, frissítse a Subsonic kiszolgálót! - Hangerő távvezérlése Equalizer Jukebox ki Jukebox be diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index c5e1dd60..0b778491 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -45,7 +45,6 @@ Il controllo remoto non è disponibile nella modalità offline. Controllo remoto abilitato. La musica verrà riprodotta sul server. Il controllo remoto non è supportato. Per favore aggiorna la versione del server Airsonic. - Volume remoto Equalizzatore Jukebox spento Jukebox acceso diff --git a/ultrasonic/src/main/res/values-ja/strings.xml b/ultrasonic/src/main/res/values-ja/strings.xml index afe2dcca..b1ee02f1 100644 --- a/ultrasonic/src/main/res/values-ja/strings.xml +++ b/ultrasonic/src/main/res/values-ja/strings.xml @@ -42,7 +42,6 @@ リモートコントロールがオンになりました。サーバーで音楽が再生されます。 リモートコントロールがオフになりました。モバイル端末で音楽が再生されます。 リモートコントロールがサポートされていません。Subsonicサーバーをアップグレードしてください。 - リモート音量 ジュークボックス ON 歌詞 アルバムを表示 diff --git a/ultrasonic/src/main/res/values-nb-rNO/strings.xml b/ultrasonic/src/main/res/values-nb-rNO/strings.xml index 2d0addec..5d530829 100644 --- a/ultrasonic/src/main/res/values-nb-rNO/strings.xml +++ b/ultrasonic/src/main/res/values-nb-rNO/strings.xml @@ -339,7 +339,6 @@ Fjernkontroll er avskrudd. Skru på jukebox-modus i Brukere > Innstillinger på din Subsonic-tjener. Fjernkontroll avskrudd. Musikk spilles på enheten. Fjernkontroll støttes ikke. Oppgrader din Subsonic-tjener. - Fjernkontroll Jukebox avslått Jukebox påslått Omstokking diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index b92aab0f..882ba982 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -63,7 +63,6 @@ Afstandsbediening is niet beschikbaar in offline-modus. Afstandsbediening ingeschakeld; muziek wordt afgespeeld op de server. Afstandsbediening wordt niet ondersteund. Werk je Subsonic-server bij. - Afstandsbedieningvolume Equalizer Jukebox uitgeschakeld Jukebox ingeschakeld diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index c75f5e26..c5f5cef2 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -47,7 +47,6 @@ Pilot jest niedostępny w trybie offline. Tryb pilota jest włączony. Muzyka jest odtwarzana na serwerze. Tryb pilota jest niedostępny. Proszę uaktualnić serwer Subsonic. - Zdalna głośność Korektor dźwięku Jukebox wyłączony Jukebox włączony diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index a6bf5731..b61c8b15 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -62,7 +62,6 @@ Controle remoto não está disponível no modo offline. Controle remoto ligado. Música tocada no servidor. Controle remoto não suportado. Atualize seu servidor Subsonic. - Volume Remoto Equalizador Jukebox Desligado Jukebox Ligado diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index d7c7f4b9..682ecc02 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -47,7 +47,6 @@ Controle remoto não está disponível no modo offline. Controle remoto ligado. Música tocada no servidor. Controle remoto não suportado. Atualize seu servidor Subsonic. - Volume Remoto Equalizador Jukebox Desligado Jukebox Ligado diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 05cb3e20..93eb988a 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -59,7 +59,6 @@ Пульт дистанционного управления недоступен в автономном режиме. Включен пульт управления. Музыка играет на сервере. Пульт дистанционного управления не поддерживается. Пожалуйста, обновите ваш дозвуковой сервер. - Удаленная громкость Эквалайзер Jukebox выключен Jukebox включен diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index fea8126c..50265302 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -60,7 +60,6 @@ 离线模式不支持远程控制。 已打开远程控制,音乐将在服务端播放。 远程控制不支持,请升级您的 Subsonic 服务器。 - 远程音量 均衡器 关闭点唱机 开启点唱机 diff --git a/ultrasonic/src/main/res/values-zh-rTW/strings.xml b/ultrasonic/src/main/res/values-zh-rTW/strings.xml index 8fdfccfd..6b7b0983 100644 --- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml @@ -141,7 +141,6 @@ 固定 傳送 聊天 - 遠端音量 頭像 您真的要刪除目前選取的項目嗎? 無法理解答覆,請檢查伺服器位址。 diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 12c534f1..394091ac 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -63,7 +63,6 @@ Remote control is not available in offline mode. Turned on remote control. Music is played on server. Remote control is not supported. Please upgrade your Subsonic server. - Remote Volume Equalizer Jukebox Off Jukebox On