Compare commits

..

2 Commits

Author SHA1 Message Date
birdbird
71168983b6 Merge branch 'cast' into 'develop'
Use the JukeboxPlayer as a Player instead of an Controller

See merge request ultrasonic/ultrasonic!1019
2023-05-19 21:37:31 +00:00
birdbird
bdcb1a505b Use the JukeboxPlayer as a Player instead of an Controller 2023-05-19 21:37:31 +00:00
41 changed files with 470 additions and 758 deletions

View File

@ -1,27 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 7.4.0" type="baseline" client="gradle" dependencies="true" name="AGP (7.4.0)" variant="all" version="7.4.0"> <issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="true" name="AGP (8.0.1)" variant="all" version="8.0.1">
<issue
id="MissingPermission"
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
errorLine1=" manager.notify(NOTIFICATION_ID, notification)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt"
line="260"
column="17"/>
</issue>
<issue
id="MissingPermission"
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
errorLine1=" notificationManagerCompat.notify(notification.notificationId, notification.notification)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt"
line="194"
column="9"/>
</issue>
<issue <issue
id="PluralsCandidate" id="PluralsCandidate"
@ -30,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="152" line="151"
column="5"/> column="5"/>
</issue> </issue>
@ -48,50 +26,6 @@
file="../core/subsonic-api/build/libs/subsonic-api.jar"/> file="../core/subsonic-api/build/libs/subsonic-api.jar"/>
</issue> </issue>
<issue
id="ExportedContentProvider"
message="Exported content providers can provide access to potentially sensitive data"
errorLine1=" &lt;provider"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="128"
column="10"/>
</issue>
<issue
id="ExportedContentProvider"
message="Exported content providers can provide access to potentially sensitive data"
errorLine1=" &lt;provider"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="133"
column="10"/>
</issue>
<issue
id="ExportedReceiver"
message="Exported receiver does not require permission"
errorLine1=" &lt;receiver android:name=&quot;.receiver.UltrasonicIntentReceiver&quot;"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="88"
column="10"/>
</issue>
<issue
id="ExportedService"
message="Exported service does not require permission"
errorLine1=" &lt;service android:name=&quot;.playback.PlaybackService&quot;"
errorLine2=" ~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="77"
column="10"/>
</issue>
<issue <issue
id="UnusedResources" id="UnusedResources"
message="The resource `R.drawable.media3_notification_pause` appears to be unused" message="The resource `R.drawable.media3_notification_pause` appears to be unused"
@ -136,17 +70,6 @@
column="1"/> column="1"/>
</issue> </issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.media3_notification_small_icon` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/media3_notification_small_icon.xml"
line="1"
column="1"/>
</issue>
<issue <issue
id="Autofill" id="Autofill"
message="Missing `autofillHints` attribute" message="Missing `autofillHints` attribute"

View File

@ -66,13 +66,6 @@
android:exported="false"> android:exported="false">
</service> </service>
<service
android:name=".service.JukeboxMediaPlayer"
android:label="Ultrasonic Jukebox Media Player Service"
android:foregroundServiceType="mediaPlayback"
android:exported="false">
</service>
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md --> <!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
<service android:name=".playback.PlaybackService" <service android:name=".playback.PlaybackService"
android:label="@string/common.appname" android:label="@string/common.appname"

View File

@ -122,6 +122,7 @@ private fun Intent.getBluetoothDevice(): BluetoothDevice? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
} else { } else {
@Suppress("DEPRECATION")
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
} }
} }

View File

@ -13,7 +13,7 @@ import static org.koin.java.KoinJavaComponent.inject;
public class UltrasonicIntentReceiver extends BroadcastReceiver public class UltrasonicIntentReceiver extends BroadcastReceiver
{ {
private final Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
@Override @Override
public void onReceive(Context context, Intent intent) public void onReceive(Context context, Intent intent)

View File

@ -17,7 +17,6 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.SearchRecentSuggestions import android.provider.SearchRecentSuggestions
import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -55,8 +54,8 @@ import org.moire.ultrasonic.data.ServerSettingDao
import org.moire.ultrasonic.fragment.OnBackPressedHandler import org.moire.ultrasonic.fragment.OnBackPressedHandler
import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.provider.SearchSuggestionProvider import org.moire.ultrasonic.provider.SearchSuggestionProvider
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
@ -98,7 +97,7 @@ class NavigationActivity : AppCompatActivity() {
private val serverSettingsModel: ServerSettingsModel by viewModel() private val serverSettingsModel: ServerSettingsModel by viewModel()
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() 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 activeServerProvider: ActiveServerProvider by inject()
private val serverRepository: ServerSettingDao 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) { private fun setupNavigationMenu(navController: NavController) {
navigationView?.setupWithNavController(navController) navigationView?.setupWithNavController(navController)
@ -308,7 +295,7 @@ class NavigationActivity : AppCompatActivity() {
} }
R.id.menu_exit -> { R.id.menu_exit -> {
setResult(Constants.RESULT_CLOSE_ALL) setResult(Constants.RESULT_CLOSE_ALL)
mediaPlayerController.onDestroy() mediaPlayerManager.onDestroy()
finish() finish()
exit() exit()
} }
@ -475,9 +462,9 @@ class NavigationActivity : AppCompatActivity() {
} }
if (nowPlayingView != null) { if (nowPlayingView != null) {
val playerState: Int = mediaPlayerController.playbackState val playerState: Int = mediaPlayerManager.playbackState
if (playerState == STATE_BUFFERING || playerState == STATE_READY) { if (playerState == STATE_BUFFERING || playerState == STATE_READY) {
val item: MediaItem? = mediaPlayerController.currentMediaItem val item: MediaItem? = mediaPlayerManager.currentMediaItem
if (item != null) { if (item != null) {
nowPlayingView?.visibility = View.VISIBLE nowPlayingView?.visibility = View.VISIBLE
} }

View File

@ -2,8 +2,8 @@ package org.moire.ultrasonic.di
import org.koin.dsl.module import org.koin.dsl.module
import org.moire.ultrasonic.service.ExternalStorageMonitor import org.moire.ultrasonic.service.ExternalStorageMonitor
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.PlaybackStateSerializer import org.moire.ultrasonic.service.PlaybackStateSerializer
/** /**
@ -15,5 +15,5 @@ val mediaPlayerModule = module {
single { ExternalStorageMonitor() } single { ExternalStorageMonitor() }
// TODO Ideally this can be cleaned up when all circular references are removed. // TODO Ideally this can be cleaned up when all circular references are removed.
single { MediaPlayerController(get(), get(), get()) } single { MediaPlayerManager(get(), get(), get()) }
} }

View File

@ -72,7 +72,7 @@ class BookmarksFragment : TrackCollectionFragment() {
currentPlayingPosition = songs[0].bookmarkPosition currentPlayingPosition = songs[0].bookmarkPosition
) )
mediaPlayerController.restore( mediaPlayerManager.restore(
state = state, state = state,
autoPlay = true, autoPlay = true,
newPlaylist = true newPlaylist = true

View File

@ -25,7 +25,7 @@ import kotlin.math.abs
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
@ -48,7 +48,7 @@ class NowPlayingFragment : Fragment() {
private var nowPlayingArtist: TextView? = null private var nowPlayingArtist: TextView? = null
private var rxBusSubscription: Disposable? = null private var rxBusSubscription: Disposable? = null
private val mediaPlayerController: MediaPlayerController by inject() private val mediaPlayerManager: MediaPlayerManager by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject() private val imageLoaderProvider: ImageLoaderProvider by inject()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -85,13 +85,13 @@ class NowPlayingFragment : Fragment() {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun update() { private fun update() {
try { try {
if (mediaPlayerController.isPlaying) { if (mediaPlayerManager.isPlaying) {
playButton!!.setIconResource(R.drawable.media_pause) playButton!!.setIconResource(R.drawable.media_pause)
} else { } else {
playButton!!.setIconResource(R.drawable.media_start) playButton!!.setIconResource(R.drawable.media_start)
} }
val file = mediaPlayerController.currentMediaItem?.toTrack() val file = mediaPlayerManager.currentMediaItem?.toTrack()
if (file != null) { if (file != null) {
val title = file.title val title = file.title
@ -127,7 +127,7 @@ class NowPlayingFragment : Fragment() {
// This empty onClickListener is necessary for the onTouchListener to work // This empty onClickListener is necessary for the onTouchListener to work
requireView().setOnClickListener { } requireView().setOnClickListener { }
playButton!!.setOnClickListener { mediaPlayerController.togglePlayPause() } playButton!!.setOnClickListener { mediaPlayerManager.togglePlayPause() }
} catch (all: Exception) { } catch (all: Exception) {
Timber.w(all, "Failed to get notification cover art") Timber.w(all, "Failed to get notification cover art")
} }
@ -149,10 +149,10 @@ class NowPlayingFragment : Fragment() {
if (abs(deltaX) > MIN_DISTANCE) { if (abs(deltaX) > MIN_DISTANCE) {
// left or right // left or right
if (deltaX < 0) { if (deltaX < 0) {
mediaPlayerController.seekToPrevious() mediaPlayerManager.seekToPrevious()
} }
if (deltaX > 0) { if (deltaX > 0) {
mediaPlayerController.seekToNext() mediaPlayerManager.seekToNext()
} }
} else if (abs(deltaY) > MIN_DISTANCE) { } else if (abs(deltaY) > MIN_DISTANCE) {
if (deltaY < 0) { if (deltaY < 0) {

View File

@ -87,7 +87,7 @@ import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.service.plusAssign
@ -128,7 +128,7 @@ class PlayerFragment :
// Data & Services // Data & Services
private val networkAndStorageChecker: NetworkAndStorageChecker by inject() 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 shareHandler: ShareHandler by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject() private val imageLoaderProvider: ImageLoaderProvider by inject()
private var currentSong: Track? = null private var currentSong: Track? = null
@ -263,8 +263,8 @@ class PlayerFragment :
val previousButton: AutoRepeatButton = view.findViewById(R.id.button_previous) val previousButton: AutoRepeatButton = view.findViewById(R.id.button_previous)
val nextButton: AutoRepeatButton = view.findViewById(R.id.button_next) val nextButton: AutoRepeatButton = view.findViewById(R.id.button_next)
shuffleButton = view.findViewById(R.id.button_shuffle) shuffleButton = view.findViewById(R.id.button_shuffle)
updateShuffleButtonState(mediaPlayerController.isShufflePlayEnabled) updateShuffleButtonState(mediaPlayerManager.isShufflePlayEnabled)
updateRepeatButtonState(mediaPlayerController.repeatMode) updateRepeatButtonState(mediaPlayerManager.repeatMode)
val ratingLinearLayout = view.findViewById<LinearLayout>(R.id.song_rating) val ratingLinearLayout = view.findViewById<LinearLayout>(R.id.song_rating)
if (!useFiveStarRating) ratingLinearLayout.isVisible = false if (!useFiveStarRating) ratingLinearLayout.isVisible = false
@ -286,7 +286,7 @@ class PlayerFragment :
previousButton.setOnClickListener { previousButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
mediaPlayerController.seekToPrevious() mediaPlayerManager.seekToPrevious()
} }
} }
@ -297,7 +297,7 @@ class PlayerFragment :
nextButton.setOnClickListener { nextButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
mediaPlayerController.seekToNext() mediaPlayerManager.seekToNext()
} }
} }
@ -307,22 +307,22 @@ class PlayerFragment :
pauseButton.setOnClickListener { pauseButton.setOnClickListener {
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
mediaPlayerController.pause() mediaPlayerManager.pause()
} }
} }
stopButton.setOnClickListener { stopButton.setOnClickListener {
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
mediaPlayerController.reset() mediaPlayerManager.reset()
} }
} }
playButton.setOnClickListener { playButton.setOnClickListener {
if (!mediaPlayerController.isJukeboxEnabled) if (!mediaPlayerManager.isJukeboxEnabled)
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
mediaPlayerController.play() mediaPlayerManager.play()
} }
} }
@ -331,12 +331,12 @@ class PlayerFragment :
} }
repeatButton.setOnClickListener { repeatButton.setOnClickListener {
var newRepeat = mediaPlayerController.repeatMode + 1 var newRepeat = mediaPlayerManager.repeatMode + 1
if (newRepeat == 3) { if (newRepeat == 3) {
newRepeat = 0 newRepeat = 0
} }
mediaPlayerController.repeatMode = newRepeat mediaPlayerManager.repeatMode = newRepeat
onPlaylistChanged() onPlaylistChanged()
@ -358,7 +358,7 @@ class PlayerFragment :
progressBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { progressBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onStopTrackingTouch(seekBar: SeekBar) { override fun onStopTrackingTouch(seekBar: SeekBar) {
launch(CommunicationError.getHandler(context)) { 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 // Query the Jukebox state in an IO Context
ioScope.launch(CommunicationError.getHandler(context)) { ioScope.launch(CommunicationError.getHandler(context)) {
try { try {
jukeboxAvailable = mediaPlayerController.isJukeboxAvailable jukeboxAvailable = mediaPlayerManager.isJukeboxAvailable
} catch (all: Exception) { } catch (all: Exception) {
Timber.e(all) 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) } view.setOnTouchListener { _, event -> gestureScanner.onTouchEvent(event) }
} }
@ -432,7 +439,7 @@ class PlayerFragment :
} }
private fun toggleShuffle() { private fun toggleShuffle() {
val isEnabled = mediaPlayerController.toggleShuffle() val isEnabled = mediaPlayerManager.toggleShuffle()
if (isEnabled) { if (isEnabled) {
Util.toast(activity, R.string.download_menu_shuffle_on) Util.toast(activity, R.string.download_menu_shuffle_on)
@ -445,7 +452,7 @@ class PlayerFragment :
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (mediaPlayerController.currentMediaItem == null) { if (mediaPlayerManager.currentMediaItem == null) {
playlistFlipper.displayedChild = 1 playlistFlipper.displayedChild = 1
} else { } else {
// Download list and Album art must be updated when resumed // Download list and Album art must be updated when resumed
@ -458,7 +465,7 @@ class PlayerFragment :
executorService = Executors.newSingleThreadScheduledExecutor() executorService = Executors.newSingleThreadScheduledExecutor()
executorService.scheduleWithFixedDelay(runnable, 0L, 500L, TimeUnit.MILLISECONDS) executorService.scheduleWithFixedDelay(runnable, 0L, 500L, TimeUnit.MILLISECONDS)
if (mediaPlayerController.keepScreenOn) { if (mediaPlayerManager.keepScreenOn) {
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else { } else {
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@ -469,7 +476,7 @@ class PlayerFragment :
// Scroll to current playing. // Scroll to current playing.
private fun scrollToCurrent() { private fun scrollToCurrent() {
val index = mediaPlayerController.currentMediaItemIndex val index = mediaPlayerManager.currentMediaItemIndex
if (index != -1) { if (index != -1) {
val smoothScroller = LinearSmoothScroller(context) val smoothScroller = LinearSmoothScroller(context)
@ -557,7 +564,7 @@ class PlayerFragment :
equalizerMenuItem.isVisible = isEqualizerAvailable equalizerMenuItem.isVisible = isEqualizerAvailable
} }
val mediaPlayerController = mediaPlayerController val mediaPlayerController = mediaPlayerManager
val track = mediaPlayerController.currentMediaItem?.toTrack() val track = mediaPlayerController.currentMediaItem?.toTrack()
if (track != null) { if (track != null) {
@ -666,12 +673,12 @@ class PlayerFragment :
} }
R.id.menu_item_screen_on_off -> { R.id.menu_item_screen_on_off -> {
val window = requireActivity().window val window = requireActivity().window
if (mediaPlayerController.keepScreenOn) { if (mediaPlayerManager.keepScreenOn) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
mediaPlayerController.keepScreenOn = false mediaPlayerManager.keepScreenOn = false
} else { } else {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
mediaPlayerController.keepScreenOn = true mediaPlayerManager.keepScreenOn = true
} }
return true return true
} }
@ -684,8 +691,8 @@ class PlayerFragment :
return true return true
} }
R.id.menu_item_jukebox -> { R.id.menu_item_jukebox -> {
val jukeboxEnabled = !mediaPlayerController.isJukeboxEnabled val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
mediaPlayerController.isJukeboxEnabled = jukeboxEnabled mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
Util.toast( Util.toast(
context, context,
if (jukeboxEnabled) R.string.download_jukebox_on if (jukeboxEnabled) R.string.download_jukebox_on
@ -699,13 +706,13 @@ class PlayerFragment :
return true return true
} }
R.id.menu_item_clear_playlist -> { R.id.menu_item_clear_playlist -> {
mediaPlayerController.isShufflePlayEnabled = false mediaPlayerManager.isShufflePlayEnabled = false
mediaPlayerController.clear() mediaPlayerManager.clear()
onPlaylistChanged() onPlaylistChanged()
return true return true
} }
R.id.menu_item_save_playlist -> { R.id.menu_item_save_playlist -> {
if (mediaPlayerController.playlistSize > 0) { if (mediaPlayerManager.playlistSize > 0) {
showSavePlaylistDialog() showSavePlaylistDialog()
} }
return true return true
@ -724,7 +731,7 @@ class PlayerFragment :
if (track == null) return true if (track == null) return true
val songId = track.id val songId = track.id
val playerPosition = mediaPlayerController.playerPosition val playerPosition = mediaPlayerManager.playerPosition
track.bookmarkPosition = playerPosition track.bookmarkPosition = playerPosition
val bookmarkTime = Util.formatTotalDuration(playerPosition.toLong(), true) val bookmarkTime = Util.formatTotalDuration(playerPosition.toLong(), true)
Thread { Thread {
@ -759,7 +766,7 @@ class PlayerFragment :
return true return true
} }
R.id.menu_item_share -> { R.id.menu_item_share -> {
val mediaPlayerController = mediaPlayerController val mediaPlayerController = mediaPlayerManager
val tracks: MutableList<Track?> = ArrayList() val tracks: MutableList<Track?> = ArrayList()
val playlist = mediaPlayerController.playlist val playlist = mediaPlayerController.playlist
for (item in playlist) { for (item in playlist) {
@ -794,8 +801,7 @@ class PlayerFragment :
private fun update(cancel: CancellationToken? = null) { private fun update(cancel: CancellationToken? = null) {
if (cancel?.isCancellationRequested == true) return if (cancel?.isCancellationRequested == true) return
val mediaPlayerController = mediaPlayerController if (currentSong?.id != mediaPlayerManager.currentMediaItem?.mediaId) {
if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) {
onTrackChanged() onTrackChanged()
} }
updateSeekBar() updateSeekBar()
@ -803,10 +809,10 @@ class PlayerFragment :
private fun savePlaylistInBackground(playlistName: String) { private fun savePlaylistInBackground(playlistName: String) {
Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName)) 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 // The playlist can be acquired only from the main thread
val entries = mediaPlayerController.playlist.map { val entries = mediaPlayerManager.playlist.map {
it.toTrack() it.toTrack()
} }
@ -859,8 +865,8 @@ class PlayerFragment :
// Create listener // Create listener
val clickHandler: ((Track, Int) -> Unit) = { _, listPos -> val clickHandler: ((Track, Int) -> Unit) = { _, listPos ->
val mediaIndex = mediaPlayerController.getUnshuffledIndexOf(listPos) val mediaIndex = mediaPlayerManager.getUnshuffledIndexOf(listPos)
mediaPlayerController.play(mediaIndex) mediaPlayerManager.play(mediaIndex)
} }
viewAdapter.register( viewAdapter.register(
@ -924,7 +930,7 @@ class PlayerFragment :
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val pos = viewHolder.bindingAdapterPosition val pos = viewHolder.bindingAdapterPosition
val item = mediaPlayerController.getMediaItemAt(pos) val item = mediaPlayerManager.getMediaItemAt(pos)
// Remove the item from the list quickly // Remove the item from the list quickly
val items = viewAdapter.getCurrentList().toMutableList() val items = viewAdapter.getCurrentList().toMutableList()
@ -940,7 +946,7 @@ class PlayerFragment :
Util.toast(context, songRemoved) Util.toast(context, songRemoved)
// Remove the item from the playlist // Remove the item from the playlist
mediaPlayerController.removeFromPlaylist(pos) mediaPlayerManager.removeFromPlaylist(pos)
} }
override fun onSelectedChanged( override fun onSelectedChanged(
@ -960,7 +966,7 @@ class PlayerFragment :
dragging = false dragging = false
// Move the item in the playlist separately // Move the item in the playlist separately
Timber.i("Moving item %s to %s", startPosition, endPosition) 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() { private fun onPlaylistChanged() {
val mediaPlayerController = mediaPlayerController val mediaPlayerController = mediaPlayerManager
// Try to display playlist in play order // Try to display playlist in play order
val list = mediaPlayerController.playlistInPlayOrder val list = mediaPlayerController.playlistInPlayOrder
emptyTextView.setText(R.string.playlist_empty) emptyTextView.setText(R.string.playlist_empty)
@ -1050,12 +1056,12 @@ class PlayerFragment :
} }
private fun onTrackChanged() { private fun onTrackChanged() {
currentSong = mediaPlayerController.currentMediaItem?.toTrack() currentSong = mediaPlayerManager.currentMediaItem?.toTrack()
scrollToCurrent() scrollToCurrent()
val totalDuration = mediaPlayerController.playListDuration val totalDuration = mediaPlayerManager.playListDuration
val totalSongs = mediaPlayerController.playlistSize val totalSongs = mediaPlayerManager.playlistSize
val currentSongIndex = mediaPlayerController.currentMediaItemIndex + 1 val currentSongIndex = mediaPlayerManager.currentMediaItemIndex + 1
val duration = Util.formatTotalDuration(totalDuration) val duration = Util.formatTotalDuration(totalDuration)
val trackFormat = val trackFormat =
String.format(Locale.getDefault(), "%d / %d", currentSongIndex, totalSongs) String.format(Locale.getDefault(), "%d / %d", currentSongIndex, totalSongs)
@ -1110,23 +1116,27 @@ class PlayerFragment :
updateSongRating() updateSongRating()
nextButton.isEnabled = mediaPlayerController.canSeekToNext() updateMediaButtonActivationState()
previousButton.isEnabled = mediaPlayerController.canSeekToPrevious() }
private fun updateMediaButtonActivationState() {
nextButton.isEnabled = mediaPlayerManager.canSeekToNext()
previousButton.isEnabled = mediaPlayerManager.canSeekToPrevious()
} }
@Synchronized @Synchronized
private fun updateSeekBar() { private fun updateSeekBar() {
val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled val isJukeboxEnabled: Boolean = mediaPlayerManager.isJukeboxEnabled
val millisPlayed: Int = max(0, mediaPlayerController.playerPosition) val millisPlayed: Int = max(0, mediaPlayerManager.playerPosition)
val duration: Int = mediaPlayerController.playerDuration val duration: Int = mediaPlayerManager.playerDuration
val playbackState: Int = mediaPlayerController.playbackState val playbackState: Int = mediaPlayerManager.playbackState
if (currentSong != null) { if (currentSong != null) {
positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true) positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true)
durationTextView.text = Util.formatTotalDuration(duration.toLong(), true) durationTextView.text = Util.formatTotalDuration(duration.toLong(), true)
progressBar.max = if (duration == 0) 100 else duration // Work-around for apparent bug. progressBar.max = if (duration == 0) 100 else duration // Work-around for apparent bug.
progressBar.progress = millisPlayed progressBar.progress = millisPlayed
progressBar.isEnabled = mediaPlayerController.isPlaying || isJukeboxEnabled progressBar.isEnabled = mediaPlayerManager.isPlaying || isJukeboxEnabled
} else { } else {
positionTextView.setText(R.string.util_zero_time) positionTextView.setText(R.string.util_zero_time)
durationTextView.setText(R.string.util_no_time) durationTextView.setText(R.string.util_no_time)
@ -1135,7 +1145,7 @@ class PlayerFragment :
progressBar.isEnabled = false progressBar.isEnabled = false
} }
val progress = mediaPlayerController.bufferedPercentage val progress = mediaPlayerManager.bufferedPercentage
updateBufferProgress(playbackState, progress) updateBufferProgress(playbackState, progress)
} }
@ -1148,7 +1158,7 @@ class PlayerFragment :
setTitle(this@PlayerFragment, downloadStatus) setTitle(this@PlayerFragment, downloadStatus)
} }
Player.STATE_READY -> { Player.STATE_READY -> {
if (mediaPlayerController.isShufflePlayEnabled) { if (mediaPlayerManager.isShufflePlayEnabled) {
setTitle( setTitle(
this@PlayerFragment, this@PlayerFragment,
R.string.download_playerstate_playing_shuffle R.string.download_playerstate_playing_shuffle
@ -1172,7 +1182,7 @@ class PlayerFragment :
} }
private fun updateButtonStates(playbackState: Int) { private fun updateButtonStates(playbackState: Int) {
val isPlaying = mediaPlayerController.isPlaying val isPlaying = mediaPlayerManager.isPlaying
when (playbackState) { when (playbackState) {
Player.STATE_READY -> { Player.STATE_READY -> {
pauseButton.isVisible = isPlaying pauseButton.isVisible = isPlaying
@ -1195,9 +1205,9 @@ class PlayerFragment :
private fun seek(forward: Boolean) { private fun seek(forward: Boolean) {
launch(CommunicationError.getHandler(context)) { launch(CommunicationError.getHandler(context)) {
if (forward) { if (forward) {
mediaPlayerController.seekForward() mediaPlayerManager.seekForward()
} else { } else {
mediaPlayerController.seekBack() mediaPlayerManager.seekBack()
} }
} }
} }
@ -1223,28 +1233,28 @@ class PlayerFragment :
// Right to Left swipe // Right to Left swipe
if (e1X - e2X > swipeDistance && absX > swipeVelocity) { if (e1X - e2X > swipeDistance && absX > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.seekToNext() mediaPlayerManager.seekToNext()
return true return true
} }
// Left to Right swipe // Left to Right swipe
if (e2X - e1X > swipeDistance && absX > swipeVelocity) { if (e2X - e1X > swipeDistance && absX > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.seekToPrevious() mediaPlayerManager.seekToPrevious()
return true return true
} }
// Top to Bottom swipe // Top to Bottom swipe
if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) { if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.seekTo(mediaPlayerController.playerPosition + 30000) mediaPlayerManager.seekTo(mediaPlayerManager.playerPosition + 30000)
return true return true
} }
// Bottom to Top swipe // Bottom to Top swipe
if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) { if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000) mediaPlayerManager.seekTo(mediaPlayerManager.playerPosition - 8000)
return true return true
} }
return false return false
@ -1309,7 +1319,7 @@ class PlayerFragment :
builder.setView(layout) builder.setView(layout)
builder.setCancelable(true) builder.setCancelable(true)
val dialog = builder.create() val dialog = builder.create()
val playlistName = mediaPlayerController.suggestedPlaylistName val playlistName = mediaPlayerManager.suggestedPlaylistName
if (playlistName != null) { if (playlistName != null) {
playlistNameView.setText(playlistName) playlistNameView.setText(playlistName)
} else { } else {

View File

@ -42,7 +42,7 @@ import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.model.SearchListModel import org.moire.ultrasonic.model.SearchListModel
import org.moire.ultrasonic.service.DownloadService 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.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
@ -63,7 +63,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
private var searchRefresh: SwipeRefreshLayout? = null private var searchRefresh: SwipeRefreshLayout? = null
private var searchView: SearchView? = 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 shareHandler: ShareHandler by inject()
private val networkAndStorageChecker: NetworkAndStorageChecker by inject() private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
@ -305,15 +305,15 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
private fun onSongSelected(song: Track, append: Boolean) { private fun onSongSelected(song: Track, append: Boolean) {
if (!append) { if (!append) {
mediaPlayerController.clear() mediaPlayerManager.clear()
} }
mediaPlayerController.addToPlaylist( mediaPlayerManager.addToPlaylist(
listOf(song), listOf(song),
autoPlay = false, autoPlay = false,
shuffle = 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)) toast(context, resources.getQuantityString(R.plurals.select_album_n_songs_added, 1, 1))
} }

View File

@ -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.plantToTimberForest
import org.moire.ultrasonic.log.FileLoggerTree.Companion.uprootFromTimberForest import org.moire.ultrasonic.log.FileLoggerTree.Companion.uprootFromTimberForest
import org.moire.ultrasonic.provider.SearchSuggestionProvider 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.service.RxBus
import org.moire.ultrasonic.util.ConfirmationDialog import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
@ -62,7 +62,7 @@ class SettingsFragment :
private var debugLogToFile: CheckBoxPreference? = null private var debugLogToFile: CheckBoxPreference? = null
private var customCacheLocation: 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?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey) setPreferencesFromResource(R.xml.settings, rootKey)
@ -342,7 +342,7 @@ class SettingsFragment :
Settings.cacheLocationUri = path Settings.cacheLocationUri = path
// Clear download queue. // Clear download queue.
mediaPlayerController.clear() mediaPlayerManager.clear()
Storage.reset() Storage.reset()
Storage.ensureRootIsAvailable() Storage.ensureRootIsAvailable()
} }

View File

@ -40,7 +40,7 @@ import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.model.TrackCollectionModel import org.moire.ultrasonic.model.TrackCollectionModel
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.subsonic.DownloadAction
@ -82,7 +82,7 @@ open class TrackCollectionFragment(
private var playAllButton: MenuItem? = null private var playAllButton: MenuItem? = null
private var shareButton: 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() private val shareHandler: ShareHandler by inject()
internal var cancellationToken: CancellationToken? = null internal var cancellationToken: CancellationToken? = null

View File

@ -10,7 +10,7 @@ package org.moire.ultrasonic.model
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import java.io.IOException import java.io.IOException
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flatMapMerge 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<FeatureSupport> { suspend fun queryFeatureSupport(currentServerSetting: ServerSetting): Flow<FeatureSupport> {
val client = buildTestClient(currentServerSetting) val client = buildTestClient(currentServerSetting)
// One line of magic: // One line of magic:

View File

@ -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_MIXED
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES
import androidx.media3.common.Player
import androidx.media3.common.Rating import androidx.media3.common.Rating
import androidx.media3.session.LibraryResult import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService 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.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.domain.Track 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.MusicServiceFactory
import org.moire.ultrasonic.service.RatingManager import org.moire.ultrasonic.service.RatingManager
import org.moire.ultrasonic.util.MainThreadExecutor import org.moire.ultrasonic.util.MainThreadExecutor
@ -101,10 +100,10 @@ const val PLAY_COMMAND = "play "
*/ */
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember") @Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibraryService) : class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
MediaLibraryService.MediaLibrarySession.Callback, KoinComponent { MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
private val mediaPlayerController by inject<MediaPlayerController>() private val mediaPlayerManager by inject<MediaPlayerManager>()
private val activeServerProvider: ActiveServerProvider by inject() private val activeServerProvider: ActiveServerProvider by inject()
private val serviceJob = SupervisorJob() private val serviceJob = SupervisorJob()
@ -241,7 +240,7 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr
* is stored in the track.starred value * is stored in the track.starred value
* See https://github.com/androidx/media/issues/33 * See https://github.com/androidx/media/issues/33
*/ */
val track = mediaPlayerController.currentMediaItem?.toTrack() val track = mediaPlayerManager.currentMediaItem?.toTrack()
if (track != null) { if (track != null) {
customCommandFuture = onSetRating( customCommandFuture = onSetRating(
session, session,
@ -254,12 +253,13 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr
override fun onSuccess(result: SessionResult) { override fun onSuccess(result: SessionResult) {
track.starred = !track.starred track.starred = !track.starred
// This needs to be called on the main Thread // This needs to be called on the main Thread
// TODO: This is a looping reference
libraryService.onUpdateNotification(session) libraryService.onUpdateNotification(session)
} }
override fun onFailure(t: Throwable) { override fun onFailure(t: Throwable) {
Toast.makeText( Toast.makeText(
mediaPlayerController.context, mediaPlayerManager.context,
"There was an error updating the rating", "There was an error updating the rating",
LENGTH_SHORT LENGTH_SHORT
).show() ).show()

View File

@ -20,7 +20,7 @@ import com.google.common.collect.ImmutableList
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.util.toTrack import org.moire.ultrasonic.util.toTrack
@UnstableApi @UnstableApi
@ -33,7 +33,7 @@ class CustomNotificationProvider(ctx: Context) :
* is stored in the track.starred value. See https://github.com/androidx/media/issues/33 * 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! * TODO: Once the bug is fixed remove this circular reference!
*/ */
private val mediaPlayerController by inject<MediaPlayerController>() private val mediaPlayerManager by inject<MediaPlayerManager>()
override fun addNotificationActions( override fun addNotificationActions(
mediaSession: MediaSession, mediaSession: MediaSession,
@ -48,7 +48,7 @@ class CustomNotificationProvider(ctx: Context) :
* is stored in the track.starred value * is stored in the track.starred value
* See https://github.com/androidx/media/issues/33 * See https://github.com/androidx/media/issues/33
*/ */
val rating = mediaPlayerController.currentMediaItem?.toTrack()?.starred?.let { val rating = mediaPlayerManager.currentMediaItem?.toTrack()?.starred?.let {
HeartRating( HeartRating(
it it
) )

View File

@ -26,8 +26,7 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder import androidx.media3.exoplayer.source.ShuffleOrder
import androidx.media3.exoplayer.source.ShuffleOrder.UnshuffledShuffleOrder
import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
@ -37,6 +36,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.app.UApp 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.imageloader.ArtworkBitmapLoader
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider
import org.moire.ultrasonic.service.DownloadService 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.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.service.plusAssign
@ -61,9 +63,10 @@ class PlaybackService :
MediaLibraryService(), MediaLibraryService(),
KoinComponent, KoinComponent,
CoroutineScope by CoroutineScope(Dispatchers.IO) { CoroutineScope by CoroutineScope(Dispatchers.IO) {
private lateinit var player: ExoPlayer private lateinit var player: Player
private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var mediaLibrarySession: MediaLibrarySession
private var equalizer: EqualizerController? = null private var equalizer: EqualizerController? = null
private val activeServerProvider: ActiveServerProvider by inject()
private lateinit var librarySessionCallback: MediaLibrarySession.Callback private lateinit var librarySessionCallback: MediaLibrarySession.Callback
@ -76,6 +79,7 @@ class PlaybackService :
super.onCreate() super.onCreate()
initializeSessionAndPlayer() initializeSessionAndPlayer()
setListener(MediaSessionServiceListener()) setListener(MediaSessionServiceListener())
instance = this
} }
private fun getWakeModeFlag(): Int { private fun getWakeModeFlag(): Int {
@ -99,6 +103,7 @@ class PlaybackService :
} }
private fun releasePlayerAndSession() { private fun releasePlayerAndSession() {
Timber.i("Releasing player and session")
// Broadcast that the service is being shutdown // Broadcast that the service is being shutdown
RxBus.stopServiceCommandPublisher.onNext(Unit) RxBus.stopServiceCommandPublisher.onNext(Unit)
@ -127,6 +132,91 @@ class PlaybackService :
setMediaNotificationProvider(CustomNotificationProvider(UApp.applicationContext())) 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 // Create a new plain OkHttpClient
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
val client = builder.build() val client = builder.build()
@ -147,7 +237,7 @@ class PlaybackService :
renderer.setEnableAudioOffload(true) renderer.setEnableAudioOffload(true)
// Create the player // Create the player
player = ExoPlayer.Builder(this) val player = ExoPlayer.Builder(this)
.setAudioAttributes(getAudioAttributes(), true) .setAudioAttributes(getAudioAttributes(), true)
.setWakeMode(getWakeModeFlag()) .setWakeMode(getWakeModeFlag())
.setHandleAudioBecomingNoisy(true) .setHandleAudioBecomingNoisy(true)
@ -157,59 +247,17 @@ class PlaybackService :
.setSeekForwardIncrementMs(Settings.seekInterval.toLong()) .setSeekForwardIncrementMs(Settings.seekInterval.toLong())
.build() .build()
// Setup Equalizer
equalizer = EqualizerController.create(player.audioSessionId) equalizer = EqualizerController.create(player.audioSessionId)
// Enable audio offload // Enable audio offload
if (Settings.useHwOffload) if (Settings.useHwOffload)
player.experimentalSetOffloadSchedulingEnabled(true) player.experimentalSetOffloadSchedulingEnabled(true)
// Create browser interface return player
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
} }
fun createShuffleListFromCurrentIndex( private fun createShuffleListFromCurrentIndex(
currentIndex: Int, currentIndex: Int,
length: Int, length: Int,
random: Random random: Random
@ -244,6 +292,7 @@ class PlaybackService :
} }
private fun cacheNextSongs() { private fun cacheNextSongs() {
if (actualBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) return
Timber.d("PlaybackService caching the next songs") Timber.d("PlaybackService caching the next songs")
val nextSongs = Util.getPlayListFromTimeline( val nextSongs = Util.getPlayListFromTimeline(
player.currentTimeline, player.currentTimeline,
@ -333,6 +382,16 @@ class PlaybackService :
} }
companion object { 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_ID = "org.moire.ultrasonic.error"
private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic error messages" private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic error messages"
private const val NOTIFICATION_ID = 3009 private const val NOTIFICATION_ID = 3009

View File

@ -7,29 +7,12 @@
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.annotation.SuppressLint 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.Handler
import android.os.IBinder
import android.os.Looper 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.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.DeviceInfo import androidx.media3.common.DeviceInfo
import androidx.media3.common.FlagSet
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
@ -39,34 +22,27 @@ import androidx.media3.common.Timeline
import androidx.media3.common.TrackSelectionParameters import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.VideoSize import androidx.media3.common.VideoSize
import androidx.media3.common.text.CueGroup 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.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.Executors
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ScheduledFuture import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
import kotlin.math.roundToInt
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
import org.moire.ultrasonic.app.UApp.Companion.applicationContext import org.moire.ultrasonic.app.UApp.Companion.applicationContext
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.JukeboxStatus
import org.moire.ultrasonic.playback.CustomNotificationProvider
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService 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.sleepQuietly
import org.moire.ultrasonic.util.Util.stopForegroundRemoveNotification
import timber.log.Timber import timber.log.Timber
private const val STATUS_UPDATE_INTERVAL_SECONDS = 5L 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 private const val QUEUE_POLL_INTERVAL_SECONDS = 1L
/** /**
@ -86,135 +62,64 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
private val timeOfLastUpdate = AtomicLong() private val timeOfLastUpdate = AtomicLong()
private var jukeboxStatus: JukeboxStatus? = null private var jukeboxStatus: JukeboxStatus? = null
private var previousJukeboxStatus: JukeboxStatus? = null private var previousJukeboxStatus: JukeboxStatus? = null
private var gain = 0.5f private var gain = (MAX_GAIN / 3)
private var volumeToast: VolumeToast? = null private val floatGain: Float
get() = gain.toFloat() / MAX_GAIN
private var serviceThread: Thread? = null private var serviceThread: Thread? = null
private var listeners: MutableList<Player.Listener> = mutableListOf() private var listeners: ListenerSet<Player.Listener>
private val playlist: MutableList<MediaItem> = mutableListOf() private val playlist: MutableList<MediaItem> = mutableListOf()
private var currentIndex: Int = 0
private val notificationProvider = CustomNotificationProvider(applicationContext())
private lateinit var mediaSession: MediaSession
private lateinit var notificationManagerCompat: NotificationManagerCompat
@Suppress("MagicNumber") private var _currentIndex: Int = 0
override fun onCreate() { private var currentIndex: Int
super.onCreate() get() = _currentIndex
if (running.get()) return 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) running.set(true)
listeners = ListenerSet(
applicationLooper,
Clock.DEFAULT
) { listener: Player.Listener, flags: FlagSet? ->
listener.onEvents(
this,
Player.Events(
flags!!
)
)
}
tasks.clear() tasks.clear()
updatePlaylist() updatePlaylist()
stop() stop()
startFuture?.set(this)
startProcessTasks() 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 { override fun release() {
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<KeyEvent>(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() {
tasks.clear() tasks.clear()
stop() stop()
if (!running.get()) return if (!running.get()) return
running.set(false) running.set(false)
serviceThread!!.join() serviceThread?.join()
stopForegroundRemoveNotification()
mediaSession.release()
super.onDestroy()
Timber.d("Stopped Jukebox Service") 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<JukeboxMediaPlayer>? = null
@JvmStatic
fun requestStart(): ListenableFuture<JukeboxMediaPlayer>? {
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) { override fun addListener(listener: Player.Listener) {
listeners.add(listener) listeners.add(listener)
} }
@ -263,14 +168,20 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
tasks.add(Skip(mediaItemIndex, positionSeconds)) tasks.add(Skip(mediaItemIndex, positionSeconds))
currentIndex = mediaItemIndex currentIndex = mediaItemIndex
updateAvailableCommands()
} }
override fun seekBack() { override fun seekBack() {
seekTo(0L.coerceAtMost((jukeboxStatus?.positionSeconds ?: 0) - SEEK_INCREMENT_SECONDS)) seekTo(
0L.coerceAtMost(
(jukeboxStatus?.positionSeconds ?: 0) -
Settings.seekIntervalMillis
)
)
} }
override fun seekForward() { override fun seekForward() {
seekTo((jukeboxStatus?.positionSeconds ?: 0) + SEEK_INCREMENT_SECONDS) seekTo((jukeboxStatus?.positionSeconds ?: 0) + Settings.seekIntervalMillis)
} }
override fun isCurrentMediaItemSeekable() = true override fun isCurrentMediaItemSeekable() = true
@ -292,8 +203,11 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
override fun getAvailableCommands(): Player.Commands { override fun getAvailableCommands(): Player.Commands {
val commandsBuilder = Player.Commands.Builder().addAll( val commandsBuilder = Player.Commands.Builder().addAll(
Player.COMMAND_SET_VOLUME, Player.COMMAND_CHANGE_MEDIA_ITEMS,
Player.COMMAND_GET_VOLUME 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 (isPlaying) commandsBuilder.add(Player.COMMAND_STOP)
if (playlist.isNotEmpty()) { if (playlist.isNotEmpty()) {
@ -306,8 +220,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
Player.COMMAND_SEEK_FORWARD, Player.COMMAND_SEEK_FORWARD,
Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
Player.COMMAND_SEEK_TO_MEDIA_ITEM, Player.COMMAND_SEEK_TO_MEDIA_ITEM,
) // Seeking back is always available
if (currentIndex > 0) commandsBuilder.addAll(
Player.COMMAND_SEEK_TO_PREVIOUS, Player.COMMAND_SEEK_TO_PREVIOUS,
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
) )
@ -323,6 +236,18 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
return availableCommands.contains(command) 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 { override fun getPlayWhenReady(): Boolean {
return isPlaying return isPlaying
} }
@ -358,21 +283,43 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {} override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
override fun setVolume(volume: Float) { override fun setDeviceVolume(volume: Int) {
gain = volume gain = volume
tasks.remove(SetGain::class.java) tasks.remove(SetGain::class.java)
tasks.add(SetGain(volume)) tasks.add(SetGain(floatGain))
val context = applicationContext()
if (volumeToast == null) volumeToast = VolumeToast(context) // We must trigger an event so that the Controller knows the new volume
volumeToast!!.setVolume(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 { override fun getVolume(): Float {
return gain return floatGain
} }
override fun getDeviceVolume(): Int { override fun getDeviceVolume(): Int {
return (gain * 100).toInt() return gain
} }
override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) { override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) {
@ -444,7 +391,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
override fun seekToPrevious() { override fun seekToPrevious() {
if ((jukeboxStatus?.positionSeconds ?: 0) > SEEK_START_AFTER_SECONDS) { if ((jukeboxStatus?.positionSeconds ?: 0) > (Settings.seekIntervalMillis)) {
seekTo(currentIndex, 0) seekTo(currentIndex, 0)
return return
} }
@ -499,51 +446,63 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
@Suppress("LoopWithTooManyJumpStatements") @Suppress("LoopWithTooManyJumpStatements")
private fun processTasks() { private fun processTasks() {
Timber.d("JukeboxMediaPlayer processTasks starting") Timber.d("JukeboxMediaPlayer processTasks starting")
while (true) { while (running.get()) {
// Sleep a bit to spare processor time if we loop a lot // Sleep a bit to spare processor time if we loop a lot
sleepQuietly(10) sleepQuietly(10)
// This is only necessary if Ultrasonic goes offline sooner than the thread stops // This is only necessary if Ultrasonic goes offline sooner than the thread stops
if (isOffline()) continue if (isOffline()) continue
var task: JukeboxTask? = null var task: JukeboxTask? = null
try { try {
task = tasks.poll() task = tasks.poll() ?: continue
// If running is false, exit when the queue is empty
if (task == null && !running.get()) break
if (task == null) continue
Timber.v("JukeBoxMediaPlayer processTasks processes Task %s", task::class) Timber.v("JukeBoxMediaPlayer processTasks processes Task %s", task::class)
val status = task.execute() val status = task.execute()
onStatusUpdate(status) onStatusUpdate(status)
} catch (x: Throwable) { } catch (all: Throwable) {
onError(task, x) onError(task, all)
} }
} }
Timber.d("JukeboxMediaPlayer processTasks stopped") Timber.d("JukeboxMediaPlayer processTasks stopped")
} }
// Jukebox status contains data received from the server, we need to validate it!
private fun onStatusUpdate(jukeboxStatus: JukeboxStatus) { private fun onStatusUpdate(jukeboxStatus: JukeboxStatus) {
timeOfLastUpdate.set(System.currentTimeMillis()) timeOfLastUpdate.set(System.currentTimeMillis())
previousJukeboxStatus = this.jukeboxStatus previousJukeboxStatus = this.jukeboxStatus
this.jukeboxStatus = 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 currentIndex = jukeboxStatus.currentPlayingIndex ?: currentIndex
if (jukeboxStatus.isPlaying != previousJukeboxStatus?.isPlaying) { if (jukeboxStatus.isPlaying != previousJukeboxStatus?.isPlaying) {
shouldUpdateCommands = true
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
listeners.forEach { listeners.queueEvent(Player.EVENT_PLAYBACK_STATE_CHANGED) {
it.onPlaybackStateChanged( it.onPlaybackStateChanged(
if (jukeboxStatus.isPlaying) Player.STATE_READY else Player.STATE_IDLE if (jukeboxStatus.isPlaying) Player.STATE_READY else Player.STATE_IDLE
) )
}
listeners.queueEvent(Player.EVENT_IS_PLAYING_CHANGED) {
it.onIsPlayingChanged(jukeboxStatus.isPlaying) it.onIsPlayingChanged(jukeboxStatus.isPlaying)
} }
} }
} }
if (jukeboxStatus.currentPlayingIndex != previousJukeboxStatus?.currentPlayingIndex) { if (jukeboxStatus.currentPlayingIndex != previousJukeboxStatus?.currentPlayingIndex) {
shouldUpdateCommands = true
currentIndex = jukeboxStatus.currentPlayingIndex ?: 0 currentIndex = jukeboxStatus.currentPlayingIndex ?: 0
val currentMedia = val currentMedia =
if (currentIndex > 0 && currentIndex < playlist.size) playlist[currentIndex] if (currentIndex > 0 && currentIndex < playlist.size) playlist[currentIndex]
else MediaItem.EMPTY else MediaItem.EMPTY
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
listeners.forEach { listeners.queueEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) {
it.onMediaItemTransition( it.onMediaItemTransition(
currentMedia, currentMedia,
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK 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) { private fun onError(task: JukeboxTask?, x: Throwable) {
var exception: PlaybackException? = null
if (x is ApiNotSupportedException && task !is Stop) { if (x is ApiNotSupportedException && task !is Stop) {
Handler(Looper.getMainLooper()).post { exception = PlaybackException(
listeners.forEach { "Jukebox server too old",
it.onPlayerError( null,
PlaybackException( R.string.download_jukebox_server_too_old
"Jukebox server too old", )
null,
R.string.download_jukebox_server_too_old
)
)
}
}
} else if (x is OfflineException && task !is Stop) { } else if (x is OfflineException && task !is Stop) {
Handler(Looper.getMainLooper()).post { exception = PlaybackException(
listeners.forEach { "Jukebox offline",
it.onPlayerError( null,
PlaybackException( R.string.download_jukebox_offline
"Jukebox offline", )
null,
R.string.download_jukebox_offline
)
)
}
}
} else if (x is SubsonicRESTException && x.code == 50 && task !is Stop) { } 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 { Handler(Looper.getMainLooper()).post {
listeners.forEach { listeners.sendEvent(Player.EVENT_PLAYER_ERROR) {
it.onPlayerError( it.onPlayerError(exception)
PlaybackException(
"Jukebox not authorized",
null,
R.string.download_jukebox_not_authorized
)
)
} }
} }
} else { } else {
@ -608,8 +562,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
tasks.add(SetPlaylist(ids)) tasks.add(SetPlaylist(ids))
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
listeners.forEach { listeners.sendEvent(
it.onTimelineChanged( Player.EVENT_TIMELINE_CHANGED
) { listener: Player.Listener ->
listener.onTimelineChanged(
PlaylistTimeline(playlist), PlaylistTimeline(playlist),
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED 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<View>(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 // The constants below are necessary so a MediaSession can be built from the Jukebox Service
override fun isCurrentMediaItemDynamic(): Boolean { override fun isCurrentMediaItemDynamic(): Boolean {
return false return false
@ -748,15 +685,15 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
override fun getMaxSeekToPreviousPosition(): Long { override fun getMaxSeekToPreviousPosition(): Long {
return SEEK_START_AFTER_SECONDS * 1000L return Settings.seekInterval.toLong()
} }
override fun getSeekBackIncrement(): Long { override fun getSeekBackIncrement(): Long {
return SEEK_INCREMENT_SECONDS * 1000L return Settings.seekInterval.toLong()
} }
override fun getSeekForwardIncrement(): Long { override fun getSeekForwardIncrement(): Long {
return SEEK_INCREMENT_SECONDS * 1000L return Settings.seekInterval.toLong()
} }
override fun isLoading(): Boolean { override fun isLoading(): Boolean {
@ -779,6 +716,8 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
return AudioAttributes.DEFAULT return AudioAttributes.DEFAULT
} }
override fun setVolume(volume: Float) {}
override fun getVideoSize(): VideoSize { override fun getVideoSize(): VideoSize {
return VideoSize(0, 0) return VideoSize(0, 0)
} }
@ -824,7 +763,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
override fun getDeviceInfo(): DeviceInfo { override fun getDeviceInfo(): DeviceInfo {
return DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 1) return DEVICE_INFO
} }
override fun getPlayerError(): PlaybackException? { override fun getPlayerError(): PlaybackException? {

View File

@ -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
}
}
}

View File

@ -8,7 +8,6 @@
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Service
import android.view.Surface import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
@ -26,7 +25,7 @@ import androidx.media3.common.Tracks
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
abstract class JukeboxUnimplementedFunctions : Service(), Player { abstract class JukeboxUnimplementedFunctions : Player {
override fun setMediaItems(mediaItems: MutableList<MediaItem>) { override fun setMediaItems(mediaItems: MutableList<MediaItem>) {
TODO("Not yet implemented") TODO("Not yet implemented")
@ -140,10 +139,6 @@ abstract class JukeboxUnimplementedFunctions : Service(), Player {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun release() {
TODO("Not yet implemented")
}
override fun getCurrentTracks(): Tracks { override fun getCurrentTracks(): Tracks {
// TODO Dummy information is returned for now, this seems to work // TODO Dummy information is returned for now, this seems to work
return Tracks.EMPTY return Tracks.EMPTY
@ -228,20 +223,4 @@ abstract class JukeboxUnimplementedFunctions : Service(), Player {
override fun clearVideoTextureView(textureView: TextureView?) { override fun clearVideoTextureView(textureView: TextureView?) {
TODO("Not yet implemented") 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")
}
} }

View File

@ -32,7 +32,7 @@ import timber.log.Timber
class MediaPlayerLifecycleSupport : KoinComponent { class MediaPlayerLifecycleSupport : KoinComponent {
private lateinit var ratingManager: RatingManager private lateinit var ratingManager: RatingManager
private val playbackStateSerializer by inject<PlaybackStateSerializer>() private val playbackStateSerializer by inject<PlaybackStateSerializer>()
private val mediaPlayerController by inject<MediaPlayerController>() private val mediaPlayerManager by inject<MediaPlayerManager>()
private val imageLoaderProvider: ImageLoaderProvider by inject() private val imageLoaderProvider: ImageLoaderProvider by inject()
private var created = false private var created = false
@ -64,7 +64,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
return return
} }
mediaPlayerController.onCreate { mediaPlayerManager.onCreate {
restoreLastSession(autoPlay, afterRestore) restoreLastSession(autoPlay, afterRestore)
} }
@ -81,7 +81,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
Timber.i("Restoring %s songs", it!!.songs.size) Timber.i("Restoring %s songs", it!!.songs.size)
mediaPlayerController.restore( mediaPlayerManager.restore(
it, it,
autoPlay, autoPlay,
false false
@ -110,7 +110,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
if (intent == null) return if (intent == null) return
val intentAction = intent.action val intentAction = intent.action
if (intentAction == null || intentAction.isEmpty()) return if (intentAction.isNullOrEmpty()) return
Timber.i("Received intent: %s", intentAction) Timber.i("Received intent: %s", intentAction)
@ -146,15 +146,15 @@ class MediaPlayerLifecycleSupport : KoinComponent {
val state = extras.getInt("state") val state = extras.getInt("state")
if (state == 0) { if (state == 0) {
if (!mediaPlayerController.isJukeboxEnabled) { if (!mediaPlayerManager.isJukeboxEnabled) {
mediaPlayerController.pause() mediaPlayerManager.pause()
} }
} else if (state == 1) { } else if (state == 1) {
if (!mediaPlayerController.isJukeboxEnabled && if (!mediaPlayerManager.isJukeboxEnabled &&
Settings.resumePlayOnHeadphonePlug && !mediaPlayerController.isPlaying Settings.resumePlayOnHeadphonePlug && !mediaPlayerManager.isPlaying
) { ) {
mediaPlayerController.prepare() mediaPlayerManager.prepare()
mediaPlayerController.play() mediaPlayerManager.play()
} }
} }
} }
@ -183,18 +183,18 @@ class MediaPlayerLifecycleSupport : KoinComponent {
onCreate(autoStart) { onCreate(autoStart) {
when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause() KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerManager.togglePlayPause()
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.seekToPrevious() KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerManager.seekToPrevious()
KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.seekToNext() KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerManager.seekToNext()
KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop() KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerManager.stop()
KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play() KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerManager.play()
KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause() KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerManager.pause()
KeyEvent.KEYCODE_1 -> mediaPlayerController.legacySetRating(1) KeyEvent.KEYCODE_1 -> mediaPlayerManager.legacySetRating(1)
KeyEvent.KEYCODE_2 -> mediaPlayerController.legacySetRating(2) KeyEvent.KEYCODE_2 -> mediaPlayerManager.legacySetRating(2)
KeyEvent.KEYCODE_3 -> mediaPlayerController.legacySetRating(3) KeyEvent.KEYCODE_3 -> mediaPlayerManager.legacySetRating(3)
KeyEvent.KEYCODE_4 -> mediaPlayerController.legacySetRating(4) KeyEvent.KEYCODE_4 -> mediaPlayerManager.legacySetRating(4)
KeyEvent.KEYCODE_5 -> mediaPlayerController.legacySetRating(5) KeyEvent.KEYCODE_5 -> mediaPlayerManager.legacySetRating(5)
KeyEvent.KEYCODE_STAR -> mediaPlayerController.legacyToggleStar() KeyEvent.KEYCODE_STAR -> mediaPlayerManager.legacyToggleStar()
else -> { else -> {
} }
} }
@ -222,17 +222,17 @@ class MediaPlayerLifecycleSupport : KoinComponent {
// We can receive intents when everything is stopped, so we need to start // We can receive intents when everything is stopped, so we need to start
onCreate(autoStart) { onCreate(autoStart) {
when (action) { when (action) {
Constants.CMD_PLAY -> mediaPlayerController.play() Constants.CMD_PLAY -> mediaPlayerManager.play()
Constants.CMD_RESUME_OR_PLAY -> Constants.CMD_RESUME_OR_PLAY ->
// If Ultrasonic wasn't running, the autoStart is enough to resume, // If Ultrasonic wasn't running, the autoStart is enough to resume,
// no need to call anything // no need to call anything
if (isRunning) mediaPlayerController.resumeOrPlay() if (isRunning) mediaPlayerManager.resumeOrPlay()
Constants.CMD_NEXT -> mediaPlayerController.seekToNext() Constants.CMD_NEXT -> mediaPlayerManager.seekToNext()
Constants.CMD_PREVIOUS -> mediaPlayerController.seekToPrevious() Constants.CMD_PREVIOUS -> mediaPlayerManager.seekToPrevious()
Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause() Constants.CMD_TOGGLEPAUSE -> mediaPlayerManager.togglePlayPause()
Constants.CMD_STOP -> mediaPlayerController.stop() Constants.CMD_STOP -> mediaPlayerManager.stop()
Constants.CMD_PAUSE -> mediaPlayerController.pause() Constants.CMD_PAUSE -> mediaPlayerManager.pause()
} }
} }
} }

View File

@ -16,8 +16,6 @@ import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player 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.MEDIA_ITEM_TRANSITION_REASON_AUTO
import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Player.REPEAT_MODE_OFF
import androidx.media3.common.Rating import androidx.media3.common.Rating
@ -50,12 +48,13 @@ private const val CONTROLLER_SWITCH_DELAY = 500L
private const val VOLUME_DELTA = 0.05f 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 * This class contains everything that is necessary for the Application UI
* to control the Media Player implementation. * to control the Media Player implementation.
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class MediaPlayerController( class MediaPlayerManager(
private val playbackStateSerializer: PlaybackStateSerializer, private val playbackStateSerializer: PlaybackStateSerializer,
private val externalStorageMonitor: ExternalStorageMonitor, private val externalStorageMonitor: ExternalStorageMonitor,
val context: Context val context: Context
@ -97,15 +96,15 @@ class MediaPlayerController(
* We run the event through RxBus in order to throttle them * We run the event through RxBus in order to throttle them
*/ */
override fun onTimelineChanged(timeline: Timeline, reason: Int) { 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) Timber.w("On timeline changed. First shuffle play at index: %s", start)
deferredPlay?.let { deferredPlay?.let {
Timber.w("Executing deferred shuffle play") Timber.w("Executing deferred shuffle play")
it() it()
deferredPlay = null deferredPlay = null
} }
val playlist = Util.getPlayListFromTimeline(timeline, false).map(MediaItem::toTrack)
RxBus.playlistPublisher.onNext(playlist.map(MediaItem::toTrack)) RxBus.playlistPublisher.onNext(playlist)
} }
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
@ -179,11 +178,8 @@ class MediaPlayerController(
fun onCreate(onCreated: () -> Unit) { fun onCreate(onCreated: () -> Unit) {
if (created) return if (created) return
externalStorageMonitor.onCreate { reset() } externalStorageMonitor.onCreate { reset() }
if (activeServerProvider.getActiveServer().jukeboxByDefault) {
switchToJukebox(onCreated) createMediaController(onCreated)
} else {
switchToLocalPlayer(onCreated)
}
rxBusSubscription += RxBus.activeServerChangingObservable.subscribe { oldServer -> rxBusSubscription += RxBus.activeServerChangingObservable.subscribe { oldServer ->
if (oldServer != OFFLINE_DB_ID) { if (oldServer != OFFLINE_DB_ID) {
@ -195,8 +191,7 @@ class MediaPlayerController(
if (controller is JukeboxMediaPlayer) { if (controller is JukeboxMediaPlayer) {
// When the server changes, the Jukebox should be released. // When the server changes, the Jukebox should be released.
// The new server won't understand the jukebox requests of the old one. // The new server won't understand the jukebox requests of the old one.
releaseJukebox(controller) switchToLocalPlayer()
controller = null
} }
} }
@ -246,6 +241,22 @@ class MediaPlayerController(
Timber.i("MediaPlayerController started") 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() { private fun playerStateChangedHandler() {
val currentPlaying = controller?.currentMediaItem?.toTrack() ?: return val currentPlaying = controller?.currentMediaItem?.toTrack() ?: return
@ -262,6 +273,10 @@ class MediaPlayerController(
} }
} }
fun addListener(listener: Player.Listener) {
controller?.addListener(listener)
}
private fun clearBookmark() { private fun clearBookmark() {
// This method is called just before we update the cachedMediaItem, // This method is called just before we update the cachedMediaItem,
// so in fact cachedMediaItem will refer to the track that has just finished. // so in fact cachedMediaItem will refer to the track that has just finished.
@ -336,7 +351,6 @@ class MediaPlayerController(
@Synchronized @Synchronized
fun play(index: Int) { fun play(index: Int) {
controller?.seekTo(index, 0L) controller?.seekTo(index, 0L)
// FIXME CHECK ITS NOT MAKING PROBLEMS
controller?.prepare() controller?.prepare()
controller?.play() controller?.play()
} }
@ -538,7 +552,7 @@ class MediaPlayerController(
@Synchronized @Synchronized
fun canSeekToPrevious(): Boolean { fun canSeekToPrevious(): Boolean {
return controller?.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS) == true return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS) == true
} }
@Synchronized @Synchronized
@ -548,7 +562,7 @@ class MediaPlayerController(
@Synchronized @Synchronized
fun canSeekToNext(): Boolean { fun canSeekToNext(): Boolean {
return controller?.isCommandAvailable(COMMAND_SEEK_TO_NEXT) == true return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT) == true
} }
@Synchronized @Synchronized
@ -580,102 +594,49 @@ class MediaPlayerController(
@set:Synchronized @set:Synchronized
var isJukeboxEnabled: Boolean var isJukeboxEnabled: Boolean
get() = controller is JukeboxMediaPlayer get() = PlaybackService.actualBackend == PlayerBackend.JUKEBOX
set(jukeboxEnabled) { set(shouldEnable) {
if (jukeboxEnabled) { if (shouldEnable) {
switchToJukebox {} switchToJukebox()
} else { } else {
switchToLocalPlayer {} switchToLocalPlayer()
} }
} }
private fun switchToJukebox(onCreated: () -> Unit) { private fun switchToJukebox() {
if (controller is JukeboxMediaPlayer) return if (isJukeboxEnabled) return
val currentPlaylist = playlist scheduleSwitchTo(PlayerBackend.JUKEBOX)
val currentIndex = controller?.currentMediaItemIndex ?: 0
val currentPosition = controller?.currentPosition ?: 0
DownloadService.requestStop() DownloadService.requestStop()
controller?.pause() controller?.pause()
controller?.stop() 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) { private fun switchToLocalPlayer() {
if (controller is MediaController) return if (!isJukeboxEnabled) return
val currentPlaylist = playlist scheduleSwitchTo(PlayerBackend.LOCAL)
controller?.stop()
}
private fun scheduleSwitchTo(newBackend: PlayerBackend) {
val currentPlaylist = playlist.toList()
val currentIndex = controller?.currentMediaItemIndex ?: 0 val currentIndex = controller?.currentMediaItemIndex ?: 0
val currentPosition = controller?.currentPosition ?: 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({ Handler(Looper.getMainLooper()).postDelayed({
if (oldController != null) releaseJukebox(oldController) // Change the backend
setupLocalPlayer { PlaybackService.setBackend(newBackend)
controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition) // Restore the media items
onCreated() controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition)
}
}, CONTROLLER_SWITCH_DELAY) }, CONTROLLER_SWITCH_DELAY)
} }
private fun releaseController() { private fun releaseController() {
when (controller) { controller?.removeListener(listeners)
null -> return controller?.release()
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()
if (mediaControllerFuture != null) MediaController.releaseFuture(mediaControllerFuture!!) if (mediaControllerFuture != null) MediaController.releaseFuture(mediaControllerFuture!!)
Timber.i("MediaPlayerController released") 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 * This function calls the music service directly and
* therefore can't be called from the main thread * therefore can't be called from the main thread
@ -700,10 +661,6 @@ class MediaPlayerController(
controller?.volume = gain controller?.volume = gain
} }
fun setVolume(volume: Float) {
controller?.volume = volume
}
/* /*
* Sets the rating of the current track * Sets the rating of the current track
*/ */
@ -841,4 +798,6 @@ class MediaPlayerController(
enum class InsertionMode { enum class InsertionMode {
CLEAR, APPEND, AFTER_CURRENT CLEAR, APPEND, AFTER_CURRENT
} }
enum class PlayerBackend { JUKEBOX, LOCAL }
} }

View File

@ -18,7 +18,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.DownloadService import org.moire.ultrasonic.service.DownloadService
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.executeTaskWithToast import org.moire.ultrasonic.util.executeTaskWithToast
@ -28,7 +28,7 @@ import org.moire.ultrasonic.util.executeTaskWithToast
*/ */
@Suppress("LongParameterList") @Suppress("LongParameterList")
class DownloadHandler( class DownloadHandler(
val mediaPlayerController: MediaPlayerController, val mediaPlayerManager: MediaPlayerManager,
private val networkAndStorageChecker: NetworkAndStorageChecker private val networkAndStorageChecker: NetworkAndStorageChecker
) : CoroutineScope by CoroutineScope(Dispatchers.IO) { ) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
private val maxSongs = 500 private val maxSongs = 500
@ -150,16 +150,16 @@ class DownloadHandler(
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
val insertionMode = when { val insertionMode = when {
append -> MediaPlayerController.InsertionMode.APPEND append -> MediaPlayerManager.InsertionMode.APPEND
playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT playNext -> MediaPlayerManager.InsertionMode.AFTER_CURRENT
else -> MediaPlayerController.InsertionMode.CLEAR else -> MediaPlayerManager.InsertionMode.CLEAR
} }
if (playlistName != null) { if (playlistName != null) {
mediaPlayerController.suggestedPlaylistName = playlistName mediaPlayerManager.suggestedPlaylistName = playlistName
} }
mediaPlayerController.addToPlaylist( mediaPlayerManager.addToPlaylist(
songs, songs,
autoPlay, autoPlay,
shuffle, shuffle,

View File

@ -19,7 +19,7 @@ import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Playlist import org.moire.ultrasonic.domain.Playlist
import org.moire.ultrasonic.domain.Track 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.getAlbumArtFile
import org.moire.ultrasonic.util.FileUtil.getCompleteFile import org.moire.ultrasonic.util.FileUtil.getCompleteFile
import org.moire.ultrasonic.util.FileUtil.getPartialFile import org.moire.ultrasonic.util.FileUtil.getPartialFile
@ -235,8 +235,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinCompo
private fun findFilesToNotDelete(): Set<String> { private fun findFilesToNotDelete(): Set<String> {
val filesToNotDelete: MutableSet<String> = HashSet(5) val filesToNotDelete: MutableSet<String> = HashSet(5)
val mediaController = inject<MediaPlayerController>( val mediaController = inject<MediaPlayerManager>(
MediaPlayerController::class.java MediaPlayerManager::class.java
) )
val playlist = mainScope.future { mediaController.value.playlist }.get() val playlist = mainScope.future { mediaController.value.playlist }.get()

View File

@ -128,6 +128,9 @@ object Settings {
var seekInterval var seekInterval
by StringIntSetting(getKey(R.string.setting_key_increment_time), 5000) by StringIntSetting(getKey(R.string.setting_key_increment_time), 5000)
val seekIntervalMillis: Long
get() = (seekInterval / 1000).toLong()
@JvmStatic @JvmStatic
var mediaButtonsEnabled var mediaButtonsEnabled
by BooleanSetting(getKey(R.string.setting_key_media_buttons), true) by BooleanSetting(getKey(R.string.setting_key_media_buttons), true)

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:id="@+id/toast_layout_root"
a:orientation="vertical"
a:layout_width="fill_parent"
a:layout_height="fill_parent"
a:background="@android:drawable/toast_frame">
<TextView
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:text="@string/download.jukebox_volume"
a:textAppearance="?android:attr/textAppearanceMedium"
a:textColor="#ffffffff"
a:shadowColor="#bb000000"
a:shadowRadius="2.75"
a:paddingStart="32dp"
a:paddingEnd="32dp"
a:paddingBottom="12dp"
/>
<ProgressBar a:id="@+id/jukebox_volume_progress_bar"
style="@android:style/Widget.Holo.Light.ProgressBar.Horizontal"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:paddingBottom="3dp" />
</LinearLayout>

View File

@ -48,7 +48,6 @@
<string name="download.jukebox_offline">Vzdálené ovládání není dostupné v offline módu.</string> <string name="download.jukebox_offline">Vzdálené ovládání není dostupné v offline módu.</string>
<string name="download.jukebox_on">Vzdálené ovládání zapnuto. Hudba přehrávána na serveru.</string> <string name="download.jukebox_on">Vzdálené ovládání zapnuto. Hudba přehrávána na serveru.</string>
<string name="download.jukebox_server_too_old">Vzdálené ovládání není podporováno. Aktualizujte svůj Subsonic server.</string> <string name="download.jukebox_server_too_old">Vzdálené ovládání není podporováno. Aktualizujte svůj Subsonic server.</string>
<string name="download.jukebox_volume">Hlasitost vzdáleného přístroje</string>
<string name="download.menu_equalizer">Ekvalizér</string> <string name="download.menu_equalizer">Ekvalizér</string>
<string name="download.menu_jukebox_off">Jukebox vypnut</string> <string name="download.menu_jukebox_off">Jukebox vypnut</string>
<string name="download.menu_jukebox_on">Jukebox zapnut</string> <string name="download.menu_jukebox_on">Jukebox zapnut</string>

View File

@ -61,7 +61,6 @@
<string name="download.jukebox_offline">Fernbedienungs-Modus is Offline nicht verfügbar.</string> <string name="download.jukebox_offline">Fernbedienungs-Modus is Offline nicht verfügbar.</string>
<string name="download.jukebox_on">Fernbedienung ausgeschaltet. Musik wird auf dem Server wiedergegeben.</string> <string name="download.jukebox_on">Fernbedienung ausgeschaltet. Musik wird auf dem Server wiedergegeben.</string>
<string name="download.jukebox_server_too_old">Fernbedienungs Modus wird nicht unterstützt. Bitte den Subsonic Server aktualisieren.</string> <string name="download.jukebox_server_too_old">Fernbedienungs Modus wird nicht unterstützt. Bitte den Subsonic Server aktualisieren.</string>
<string name="download.jukebox_volume">Entfernte Lautstärke</string>
<string name="download.menu_equalizer">Equalizer</string> <string name="download.menu_equalizer">Equalizer</string>
<string name="download.menu_jukebox_off">Jukebox Aus</string> <string name="download.menu_jukebox_off">Jukebox Aus</string>
<string name="download.menu_jukebox_on">Jukebox An</string> <string name="download.menu_jukebox_on">Jukebox An</string>

View File

@ -62,7 +62,6 @@
<string name="download.jukebox_offline">Control remoto no disponible en modo fuera de línea.</string> <string name="download.jukebox_offline">Control remoto no disponible en modo fuera de línea.</string>
<string name="download.jukebox_on">Control remoto encendido. La música se reproduce en el servidor.</string> <string name="download.jukebox_on">Control remoto encendido. La música se reproduce en el servidor.</string>
<string name="download.jukebox_server_too_old">Control remoto no soportado. Por favor actualiza tu servidor de Subsonic.</string> <string name="download.jukebox_server_too_old">Control remoto no soportado. Por favor actualiza tu servidor de Subsonic.</string>
<string name="download.jukebox_volume">Volumen remoto</string>
<string name="download.menu_equalizer">Ecualizador</string> <string name="download.menu_equalizer">Ecualizador</string>
<string name="download.menu_jukebox_off">Apagar Jukebox</string> <string name="download.menu_jukebox_off">Apagar Jukebox</string>
<string name="download.menu_jukebox_on">Encender Jukebox</string> <string name="download.menu_jukebox_on">Encender Jukebox</string>

View File

@ -61,7 +61,6 @@
<string name="download.jukebox_offline">Le mode jukebox n\'est pas disponible en mode déconnecté.</string> <string name="download.jukebox_offline">Le mode jukebox n\'est pas disponible en mode déconnecté.</string>
<string name="download.jukebox_on">Mode jukebox activé. La musique est jouée sur le serveur</string> <string name="download.jukebox_on">Mode jukebox activé. La musique est jouée sur le serveur</string>
<string name="download.jukebox_server_too_old">Le mode jukebox n\'est pas pris en charge. Mise à jour du serveur Subsonic requise.</string> <string name="download.jukebox_server_too_old">Le mode jukebox n\'est pas pris en charge. Mise à jour du serveur Subsonic requise.</string>
<string name="download.jukebox_volume">Volume sur serveur distant</string>
<string name="download.menu_equalizer">Égaliseur</string> <string name="download.menu_equalizer">Égaliseur</string>
<string name="download.menu_jukebox_off">Désactiver le mode jukebox</string> <string name="download.menu_jukebox_off">Désactiver le mode jukebox</string>
<string name="download.menu_jukebox_on">Activer le mode jukebox</string> <string name="download.menu_jukebox_on">Activer le mode jukebox</string>

View File

@ -54,7 +54,6 @@
<string name="download.jukebox_offline">A távvezérlés nem lehetséges kapcsolat nélküli módban!</string> <string name="download.jukebox_offline">A távvezérlés nem lehetséges kapcsolat nélküli módban!</string>
<string name="download.jukebox_on">Távvezérlés bekapcsolása. A zenelejátszás a kiszolgálón történik.</string> <string name="download.jukebox_on">Távvezérlés bekapcsolása. A zenelejátszás a kiszolgálón történik.</string>
<string name="download.jukebox_server_too_old">A távvezérlés nem támogatott. Kérjük, frissítse a Subsonic kiszolgálót!</string> <string name="download.jukebox_server_too_old">A távvezérlés nem támogatott. Kérjük, frissítse a Subsonic kiszolgálót!</string>
<string name="download.jukebox_volume">Hangerő távvezérlése</string>
<string name="download.menu_equalizer">Equalizer</string> <string name="download.menu_equalizer">Equalizer</string>
<string name="download.menu_jukebox_off">Jukebox ki</string> <string name="download.menu_jukebox_off">Jukebox ki</string>
<string name="download.menu_jukebox_on">Jukebox be</string> <string name="download.menu_jukebox_on">Jukebox be</string>

View File

@ -45,7 +45,6 @@
<string name="download.jukebox_offline">Il controllo remoto non è disponibile nella modalità offline. </string> <string name="download.jukebox_offline">Il controllo remoto non è disponibile nella modalità offline. </string>
<string name="download.jukebox_on">Controllo remoto abilitato. La musica verrà riprodotta sul server.</string> <string name="download.jukebox_on">Controllo remoto abilitato. La musica verrà riprodotta sul server.</string>
<string name="download.jukebox_server_too_old">Il controllo remoto non è supportato. Per favore aggiorna la versione del server Airsonic.</string> <string name="download.jukebox_server_too_old">Il controllo remoto non è supportato. Per favore aggiorna la versione del server Airsonic.</string>
<string name="download.jukebox_volume">Volume remoto</string>
<string name="download.menu_equalizer">Equalizzatore</string> <string name="download.menu_equalizer">Equalizzatore</string>
<string name="download.menu_jukebox_off">Jukebox spento</string> <string name="download.menu_jukebox_off">Jukebox spento</string>
<string name="download.menu_jukebox_on">Jukebox acceso</string> <string name="download.menu_jukebox_on">Jukebox acceso</string>

View File

@ -42,7 +42,6 @@
<string name="download.jukebox_on">リモートコントロールがオンになりました。サーバーで音楽が再生されます。</string> <string name="download.jukebox_on">リモートコントロールがオンになりました。サーバーで音楽が再生されます。</string>
<string name="download.jukebox_off">リモートコントロールがオフになりました。モバイル端末で音楽が再生されます。</string> <string name="download.jukebox_off">リモートコントロールがオフになりました。モバイル端末で音楽が再生されます。</string>
<string name="download.jukebox_server_too_old">リモートコントロールがサポートされていません。Subsonicサーバーをアップグレードしてください。</string> <string name="download.jukebox_server_too_old">リモートコントロールがサポートされていません。Subsonicサーバーをアップグレードしてください。</string>
<string name="download.jukebox_volume">リモート音量</string>
<string name="download.menu_jukebox_on">ジュークボックス ON</string> <string name="download.menu_jukebox_on">ジュークボックス ON</string>
<string name="download.menu_lyrics">歌詞</string> <string name="download.menu_lyrics">歌詞</string>
<string name="download.menu_show_album">アルバムを表示</string> <string name="download.menu_show_album">アルバムを表示</string>

View File

@ -339,7 +339,6 @@
<string name="download.jukebox_not_authorized">Fjernkontroll er avskrudd. Skru på jukebox-modus i <b>Brukere &gt; Innstillinger</b> på din Subsonic-tjener.</string> <string name="download.jukebox_not_authorized">Fjernkontroll er avskrudd. Skru på jukebox-modus i <b>Brukere &gt; Innstillinger</b> på din Subsonic-tjener.</string>
<string name="download.jukebox_off">Fjernkontroll avskrudd. Musikk spilles på enheten.</string> <string name="download.jukebox_off">Fjernkontroll avskrudd. Musikk spilles på enheten.</string>
<string name="download.jukebox_server_too_old">Fjernkontroll støttes ikke. Oppgrader din Subsonic-tjener.</string> <string name="download.jukebox_server_too_old">Fjernkontroll støttes ikke. Oppgrader din Subsonic-tjener.</string>
<string name="download.jukebox_volume">Fjernkontroll</string>
<string name="download.menu_jukebox_off">Jukebox avslått</string> <string name="download.menu_jukebox_off">Jukebox avslått</string>
<string name="download.menu_jukebox_on">Jukebox påslått</string> <string name="download.menu_jukebox_on">Jukebox påslått</string>
<string name="download.menu_shuffle">Omstokking</string> <string name="download.menu_shuffle">Omstokking</string>

View File

@ -63,7 +63,6 @@
<string name="download.jukebox_offline">Afstandsbediening is niet beschikbaar in offline-modus.</string> <string name="download.jukebox_offline">Afstandsbediening is niet beschikbaar in offline-modus.</string>
<string name="download.jukebox_on">Afstandsbediening ingeschakeld; muziek wordt afgespeeld op de server.</string> <string name="download.jukebox_on">Afstandsbediening ingeschakeld; muziek wordt afgespeeld op de server.</string>
<string name="download.jukebox_server_too_old">Afstandsbediening wordt niet ondersteund. Werk je Subsonic-server bij.</string> <string name="download.jukebox_server_too_old">Afstandsbediening wordt niet ondersteund. Werk je Subsonic-server bij.</string>
<string name="download.jukebox_volume">Afstandsbedieningvolume</string>
<string name="download.menu_equalizer">Equalizer</string> <string name="download.menu_equalizer">Equalizer</string>
<string name="download.menu_jukebox_off">Jukebox uitgeschakeld</string> <string name="download.menu_jukebox_off">Jukebox uitgeschakeld</string>
<string name="download.menu_jukebox_on">Jukebox ingeschakeld</string> <string name="download.menu_jukebox_on">Jukebox ingeschakeld</string>

View File

@ -47,7 +47,6 @@
<string name="download.jukebox_offline">Pilot jest niedostępny w trybie offline.</string> <string name="download.jukebox_offline">Pilot jest niedostępny w trybie offline.</string>
<string name="download.jukebox_on">Tryb pilota jest włączony. Muzyka jest odtwarzana na serwerze.</string> <string name="download.jukebox_on">Tryb pilota jest włączony. Muzyka jest odtwarzana na serwerze.</string>
<string name="download.jukebox_server_too_old">Tryb pilota jest niedostępny. Proszę uaktualnić serwer Subsonic.</string> <string name="download.jukebox_server_too_old">Tryb pilota jest niedostępny. Proszę uaktualnić serwer Subsonic.</string>
<string name="download.jukebox_volume">Zdalna głośność</string>
<string name="download.menu_equalizer">Korektor dźwięku</string> <string name="download.menu_equalizer">Korektor dźwięku</string>
<string name="download.menu_jukebox_off">Jukebox wyłączony</string> <string name="download.menu_jukebox_off">Jukebox wyłączony</string>
<string name="download.menu_jukebox_on">Jukebox włączony</string> <string name="download.menu_jukebox_on">Jukebox włączony</string>

View File

@ -62,7 +62,6 @@
<string name="download.jukebox_offline">Controle remoto não está disponível no modo offline.</string> <string name="download.jukebox_offline">Controle remoto não está disponível no modo offline.</string>
<string name="download.jukebox_on">Controle remoto ligado. Música tocada no servidor.</string> <string name="download.jukebox_on">Controle remoto ligado. Música tocada no servidor.</string>
<string name="download.jukebox_server_too_old">Controle remoto não suportado. Atualize seu servidor Subsonic.</string> <string name="download.jukebox_server_too_old">Controle remoto não suportado. Atualize seu servidor Subsonic.</string>
<string name="download.jukebox_volume">Volume Remoto</string>
<string name="download.menu_equalizer">Equalizador</string> <string name="download.menu_equalizer">Equalizador</string>
<string name="download.menu_jukebox_off">Jukebox Desligado</string> <string name="download.menu_jukebox_off">Jukebox Desligado</string>
<string name="download.menu_jukebox_on">Jukebox Ligado</string> <string name="download.menu_jukebox_on">Jukebox Ligado</string>

View File

@ -47,7 +47,6 @@
<string name="download.jukebox_offline">Controle remoto não está disponível no modo offline.</string> <string name="download.jukebox_offline">Controle remoto não está disponível no modo offline.</string>
<string name="download.jukebox_on">Controle remoto ligado. Música tocada no servidor.</string> <string name="download.jukebox_on">Controle remoto ligado. Música tocada no servidor.</string>
<string name="download.jukebox_server_too_old">Controle remoto não suportado. Atualize seu servidor Subsonic.</string> <string name="download.jukebox_server_too_old">Controle remoto não suportado. Atualize seu servidor Subsonic.</string>
<string name="download.jukebox_volume">Volume Remoto</string>
<string name="download.menu_equalizer">Equalizador</string> <string name="download.menu_equalizer">Equalizador</string>
<string name="download.menu_jukebox_off">Jukebox Desligado</string> <string name="download.menu_jukebox_off">Jukebox Desligado</string>
<string name="download.menu_jukebox_on">Jukebox Ligado</string> <string name="download.menu_jukebox_on">Jukebox Ligado</string>

View File

@ -59,7 +59,6 @@
<string name="download.jukebox_offline">Пульт дистанционного управления недоступен в автономном режиме.</string> <string name="download.jukebox_offline">Пульт дистанционного управления недоступен в автономном режиме.</string>
<string name="download.jukebox_on">Включен пульт управления. Музыка играет на сервере.</string> <string name="download.jukebox_on">Включен пульт управления. Музыка играет на сервере.</string>
<string name="download.jukebox_server_too_old">Пульт дистанционного управления не поддерживается. Пожалуйста, обновите ваш дозвуковой сервер.</string> <string name="download.jukebox_server_too_old">Пульт дистанционного управления не поддерживается. Пожалуйста, обновите ваш дозвуковой сервер.</string>
<string name="download.jukebox_volume">Удаленная громкость</string>
<string name="download.menu_equalizer">Эквалайзер</string> <string name="download.menu_equalizer">Эквалайзер</string>
<string name="download.menu_jukebox_off">Jukebox выключен</string> <string name="download.menu_jukebox_off">Jukebox выключен</string>
<string name="download.menu_jukebox_on">Jukebox включен</string> <string name="download.menu_jukebox_on">Jukebox включен</string>

View File

@ -60,7 +60,6 @@
<string name="download.jukebox_offline">离线模式不支持远程控制。</string> <string name="download.jukebox_offline">离线模式不支持远程控制。</string>
<string name="download.jukebox_on">已打开远程控制,音乐将在服务端播放。</string> <string name="download.jukebox_on">已打开远程控制,音乐将在服务端播放。</string>
<string name="download.jukebox_server_too_old">远程控制不支持,请升级您的 Subsonic 服务器。</string> <string name="download.jukebox_server_too_old">远程控制不支持,请升级您的 Subsonic 服务器。</string>
<string name="download.jukebox_volume">远程音量</string>
<string name="download.menu_equalizer">均衡器</string> <string name="download.menu_equalizer">均衡器</string>
<string name="download.menu_jukebox_off">关闭点唱机</string> <string name="download.menu_jukebox_off">关闭点唱机</string>
<string name="download.menu_jukebox_on">开启点唱机</string> <string name="download.menu_jukebox_on">开启点唱机</string>

View File

@ -141,7 +141,6 @@
<string name="common.pin">固定</string> <string name="common.pin">固定</string>
<string name="chat.send_button">傳送</string> <string name="chat.send_button">傳送</string>
<string name="button_bar.chat">聊天</string> <string name="button_bar.chat">聊天</string>
<string name="download.jukebox_volume">遠端音量</string>
<string name="chat.user_avatar">頭像</string> <string name="chat.user_avatar">頭像</string>
<string name="common.delete_selection_confirmation">您真的要刪除目前選取的項目嗎?</string> <string name="common.delete_selection_confirmation">您真的要刪除目前選取的項目嗎?</string>
<string name="background_task.parse_error">無法理解答覆,請檢查伺服器位址。</string> <string name="background_task.parse_error">無法理解答覆,請檢查伺服器位址。</string>

View File

@ -63,7 +63,6 @@
<string name="download.jukebox_offline">Remote control is not available in offline mode.</string> <string name="download.jukebox_offline">Remote control is not available in offline mode.</string>
<string name="download.jukebox_on">Turned on remote control. Music is played on server.</string> <string name="download.jukebox_on">Turned on remote control. Music is played on server.</string>
<string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string> <string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string>
<string name="download.jukebox_volume">Remote Volume</string>
<string name="download.menu_equalizer">Equalizer</string> <string name="download.menu_equalizer">Equalizer</string>
<string name="download.menu_jukebox_off">Jukebox Off</string> <string name="download.menu_jukebox_off">Jukebox Off</string>
<string name="download.menu_jukebox_on">Jukebox On</string> <string name="download.menu_jukebox_on">Jukebox On</string>