mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-15 17:00:36 +03:00
Merge branch 'cast' into 'develop'
Use the JukeboxPlayer as a Player instead of an Controller See merge request ultrasonic/ultrasonic!1019
This commit is contained in:
commit
71168983b6
@ -1,27 +1,5 @@
|
||||
<?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">
|
||||
|
||||
<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>
|
||||
<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="PluralsCandidate"
|
||||
@ -30,7 +8,7 @@
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="152"
|
||||
line="151"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
@ -48,50 +26,6 @@
|
||||
file="../core/subsonic-api/build/libs/subsonic-api.jar"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedContentProvider"
|
||||
message="Exported content providers can provide access to potentially sensitive data"
|
||||
errorLine1=" <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=" <provider"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="133"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedReceiver"
|
||||
message="Exported receiver does not require permission"
|
||||
errorLine1=" <receiver android:name=".receiver.UltrasonicIntentReceiver""
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="88"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedService"
|
||||
message="Exported service does not require permission"
|
||||
errorLine1=" <service android:name=".playback.PlaybackService""
|
||||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="77"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.drawable.media3_notification_pause` appears to be unused"
|
||||
@ -136,17 +70,6 @@
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.drawable.media3_notification_small_icon` appears to be unused"
|
||||
errorLine1="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
||||
errorLine2="^">
|
||||
<location
|
||||
file="src/main/res/drawable/media3_notification_small_icon.xml"
|
||||
line="1"
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Autofill"
|
||||
message="Missing `autofillHints` attribute"
|
||||
|
@ -66,13 +66,6 @@
|
||||
android:exported="false">
|
||||
</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 -->
|
||||
<service android:name=".playback.PlaybackService"
|
||||
android:label="@string/common.appname"
|
||||
|
@ -122,6 +122,7 @@ private fun Intent.getBluetoothDevice(): BluetoothDevice? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
public class UltrasonicIntentReceiver extends BroadcastReceiver
|
||||
{
|
||||
private final Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
|
||||
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent)
|
||||
|
@ -17,7 +17,6 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.provider.SearchRecentSuggestions
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
@ -55,8 +54,8 @@ import org.moire.ultrasonic.data.ServerSettingDao
|
||||
import org.moire.ultrasonic.fragment.OnBackPressedHandler
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
@ -98,7 +97,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||
|
||||
private val serverSettingsModel: ServerSettingsModel by viewModel()
|
||||
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
|
||||
private val mediaPlayerController: MediaPlayerController by inject()
|
||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
private val serverRepository: ServerSettingDao by inject()
|
||||
|
||||
@ -274,18 +273,6 @@ class NavigationActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
val isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
val isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP
|
||||
val isVolumeAdjust = isVolumeDown || isVolumeUp
|
||||
val isJukebox = mediaPlayerController.isJukeboxEnabled
|
||||
if (isVolumeAdjust && isJukebox) {
|
||||
mediaPlayerController.adjustVolume(isVolumeUp)
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
private fun setupNavigationMenu(navController: NavController) {
|
||||
navigationView?.setupWithNavController(navController)
|
||||
|
||||
@ -308,7 +295,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||
}
|
||||
R.id.menu_exit -> {
|
||||
setResult(Constants.RESULT_CLOSE_ALL)
|
||||
mediaPlayerController.onDestroy()
|
||||
mediaPlayerManager.onDestroy()
|
||||
finish()
|
||||
exit()
|
||||
}
|
||||
@ -475,9 +462,9 @@ class NavigationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (nowPlayingView != null) {
|
||||
val playerState: Int = mediaPlayerController.playbackState
|
||||
val playerState: Int = mediaPlayerManager.playbackState
|
||||
if (playerState == STATE_BUFFERING || playerState == STATE_READY) {
|
||||
val item: MediaItem? = mediaPlayerController.currentMediaItem
|
||||
val item: MediaItem? = mediaPlayerManager.currentMediaItem
|
||||
if (item != null) {
|
||||
nowPlayingView?.visibility = View.VISIBLE
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ package org.moire.ultrasonic.di
|
||||
|
||||
import org.koin.dsl.module
|
||||
import org.moire.ultrasonic.service.ExternalStorageMonitor
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.PlaybackStateSerializer
|
||||
|
||||
/**
|
||||
@ -15,5 +15,5 @@ val mediaPlayerModule = module {
|
||||
single { ExternalStorageMonitor() }
|
||||
|
||||
// TODO Ideally this can be cleaned up when all circular references are removed.
|
||||
single { MediaPlayerController(get(), get(), get()) }
|
||||
single { MediaPlayerManager(get(), get(), get()) }
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ class BookmarksFragment : TrackCollectionFragment() {
|
||||
currentPlayingPosition = songs[0].bookmarkPosition
|
||||
)
|
||||
|
||||
mediaPlayerController.restore(
|
||||
mediaPlayerManager.restore(
|
||||
state = state,
|
||||
autoPlay = true,
|
||||
newPlaylist = true
|
||||
|
@ -25,7 +25,7 @@ import kotlin.math.abs
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.moire.ultrasonic.NavigationGraphDirections
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
@ -48,7 +48,7 @@ class NowPlayingFragment : Fragment() {
|
||||
private var nowPlayingArtist: TextView? = null
|
||||
|
||||
private var rxBusSubscription: Disposable? = null
|
||||
private val mediaPlayerController: MediaPlayerController by inject()
|
||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
private val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -85,13 +85,13 @@ class NowPlayingFragment : Fragment() {
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun update() {
|
||||
try {
|
||||
if (mediaPlayerController.isPlaying) {
|
||||
if (mediaPlayerManager.isPlaying) {
|
||||
playButton!!.setIconResource(R.drawable.media_pause)
|
||||
} else {
|
||||
playButton!!.setIconResource(R.drawable.media_start)
|
||||
}
|
||||
|
||||
val file = mediaPlayerController.currentMediaItem?.toTrack()
|
||||
val file = mediaPlayerManager.currentMediaItem?.toTrack()
|
||||
|
||||
if (file != null) {
|
||||
val title = file.title
|
||||
@ -127,7 +127,7 @@ class NowPlayingFragment : Fragment() {
|
||||
|
||||
// This empty onClickListener is necessary for the onTouchListener to work
|
||||
requireView().setOnClickListener { }
|
||||
playButton!!.setOnClickListener { mediaPlayerController.togglePlayPause() }
|
||||
playButton!!.setOnClickListener { mediaPlayerManager.togglePlayPause() }
|
||||
} catch (all: Exception) {
|
||||
Timber.w(all, "Failed to get notification cover art")
|
||||
}
|
||||
@ -149,10 +149,10 @@ class NowPlayingFragment : Fragment() {
|
||||
if (abs(deltaX) > MIN_DISTANCE) {
|
||||
// left or right
|
||||
if (deltaX < 0) {
|
||||
mediaPlayerController.seekToPrevious()
|
||||
mediaPlayerManager.seekToPrevious()
|
||||
}
|
||||
if (deltaX > 0) {
|
||||
mediaPlayerController.seekToNext()
|
||||
mediaPlayerManager.seekToNext()
|
||||
}
|
||||
} else if (abs(deltaY) > MIN_DISTANCE) {
|
||||
if (deltaY < 0) {
|
||||
|
@ -87,7 +87,7 @@ import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
@ -128,7 +128,7 @@ class PlayerFragment :
|
||||
|
||||
// Data & Services
|
||||
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
|
||||
private val mediaPlayerController: MediaPlayerController by inject()
|
||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
private val shareHandler: ShareHandler by inject()
|
||||
private val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
private var currentSong: Track? = null
|
||||
@ -263,8 +263,8 @@ class PlayerFragment :
|
||||
val previousButton: AutoRepeatButton = view.findViewById(R.id.button_previous)
|
||||
val nextButton: AutoRepeatButton = view.findViewById(R.id.button_next)
|
||||
shuffleButton = view.findViewById(R.id.button_shuffle)
|
||||
updateShuffleButtonState(mediaPlayerController.isShufflePlayEnabled)
|
||||
updateRepeatButtonState(mediaPlayerController.repeatMode)
|
||||
updateShuffleButtonState(mediaPlayerManager.isShufflePlayEnabled)
|
||||
updateRepeatButtonState(mediaPlayerManager.repeatMode)
|
||||
|
||||
val ratingLinearLayout = view.findViewById<LinearLayout>(R.id.song_rating)
|
||||
if (!useFiveStarRating) ratingLinearLayout.isVisible = false
|
||||
@ -286,7 +286,7 @@ class PlayerFragment :
|
||||
previousButton.setOnClickListener {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.seekToPrevious()
|
||||
mediaPlayerManager.seekToPrevious()
|
||||
}
|
||||
}
|
||||
|
||||
@ -297,7 +297,7 @@ class PlayerFragment :
|
||||
nextButton.setOnClickListener {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.seekToNext()
|
||||
mediaPlayerManager.seekToNext()
|
||||
}
|
||||
}
|
||||
|
||||
@ -307,22 +307,22 @@ class PlayerFragment :
|
||||
|
||||
pauseButton.setOnClickListener {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.pause()
|
||||
mediaPlayerManager.pause()
|
||||
}
|
||||
}
|
||||
|
||||
stopButton.setOnClickListener {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.reset()
|
||||
mediaPlayerManager.reset()
|
||||
}
|
||||
}
|
||||
|
||||
playButton.setOnClickListener {
|
||||
if (!mediaPlayerController.isJukeboxEnabled)
|
||||
if (!mediaPlayerManager.isJukeboxEnabled)
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.play()
|
||||
mediaPlayerManager.play()
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,12 +331,12 @@ class PlayerFragment :
|
||||
}
|
||||
|
||||
repeatButton.setOnClickListener {
|
||||
var newRepeat = mediaPlayerController.repeatMode + 1
|
||||
var newRepeat = mediaPlayerManager.repeatMode + 1
|
||||
if (newRepeat == 3) {
|
||||
newRepeat = 0
|
||||
}
|
||||
|
||||
mediaPlayerController.repeatMode = newRepeat
|
||||
mediaPlayerManager.repeatMode = newRepeat
|
||||
|
||||
onPlaylistChanged()
|
||||
|
||||
@ -358,7 +358,7 @@ class PlayerFragment :
|
||||
progressBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.seekTo(progressBar.progress)
|
||||
mediaPlayerManager.seekTo(progressBar.progress)
|
||||
}
|
||||
}
|
||||
|
||||
@ -395,12 +395,19 @@ class PlayerFragment :
|
||||
// Query the Jukebox state in an IO Context
|
||||
ioScope.launch(CommunicationError.getHandler(context)) {
|
||||
try {
|
||||
jukeboxAvailable = mediaPlayerController.isJukeboxAvailable
|
||||
jukeboxAvailable = mediaPlayerManager.isJukeboxAvailable
|
||||
} catch (all: Exception) {
|
||||
Timber.e(all)
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to change in command availability
|
||||
mediaPlayerManager.addListener(object : Player.Listener {
|
||||
override fun onAvailableCommandsChanged(availableCommands: Player.Commands) {
|
||||
updateMediaButtonActivationState()
|
||||
}
|
||||
})
|
||||
|
||||
view.setOnTouchListener { _, event -> gestureScanner.onTouchEvent(event) }
|
||||
}
|
||||
|
||||
@ -432,7 +439,7 @@ class PlayerFragment :
|
||||
}
|
||||
|
||||
private fun toggleShuffle() {
|
||||
val isEnabled = mediaPlayerController.toggleShuffle()
|
||||
val isEnabled = mediaPlayerManager.toggleShuffle()
|
||||
|
||||
if (isEnabled) {
|
||||
Util.toast(activity, R.string.download_menu_shuffle_on)
|
||||
@ -445,7 +452,7 @@ class PlayerFragment :
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (mediaPlayerController.currentMediaItem == null) {
|
||||
if (mediaPlayerManager.currentMediaItem == null) {
|
||||
playlistFlipper.displayedChild = 1
|
||||
} else {
|
||||
// Download list and Album art must be updated when resumed
|
||||
@ -458,7 +465,7 @@ class PlayerFragment :
|
||||
executorService = Executors.newSingleThreadScheduledExecutor()
|
||||
executorService.scheduleWithFixedDelay(runnable, 0L, 500L, TimeUnit.MILLISECONDS)
|
||||
|
||||
if (mediaPlayerController.keepScreenOn) {
|
||||
if (mediaPlayerManager.keepScreenOn) {
|
||||
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
@ -469,7 +476,7 @@ class PlayerFragment :
|
||||
|
||||
// Scroll to current playing.
|
||||
private fun scrollToCurrent() {
|
||||
val index = mediaPlayerController.currentMediaItemIndex
|
||||
val index = mediaPlayerManager.currentMediaItemIndex
|
||||
|
||||
if (index != -1) {
|
||||
val smoothScroller = LinearSmoothScroller(context)
|
||||
@ -557,7 +564,7 @@ class PlayerFragment :
|
||||
equalizerMenuItem.isVisible = isEqualizerAvailable
|
||||
}
|
||||
|
||||
val mediaPlayerController = mediaPlayerController
|
||||
val mediaPlayerController = mediaPlayerManager
|
||||
val track = mediaPlayerController.currentMediaItem?.toTrack()
|
||||
|
||||
if (track != null) {
|
||||
@ -666,12 +673,12 @@ class PlayerFragment :
|
||||
}
|
||||
R.id.menu_item_screen_on_off -> {
|
||||
val window = requireActivity().window
|
||||
if (mediaPlayerController.keepScreenOn) {
|
||||
if (mediaPlayerManager.keepScreenOn) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
mediaPlayerController.keepScreenOn = false
|
||||
mediaPlayerManager.keepScreenOn = false
|
||||
} else {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
mediaPlayerController.keepScreenOn = true
|
||||
mediaPlayerManager.keepScreenOn = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -684,8 +691,8 @@ class PlayerFragment :
|
||||
return true
|
||||
}
|
||||
R.id.menu_item_jukebox -> {
|
||||
val jukeboxEnabled = !mediaPlayerController.isJukeboxEnabled
|
||||
mediaPlayerController.isJukeboxEnabled = jukeboxEnabled
|
||||
val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
|
||||
mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
|
||||
Util.toast(
|
||||
context,
|
||||
if (jukeboxEnabled) R.string.download_jukebox_on
|
||||
@ -699,13 +706,13 @@ class PlayerFragment :
|
||||
return true
|
||||
}
|
||||
R.id.menu_item_clear_playlist -> {
|
||||
mediaPlayerController.isShufflePlayEnabled = false
|
||||
mediaPlayerController.clear()
|
||||
mediaPlayerManager.isShufflePlayEnabled = false
|
||||
mediaPlayerManager.clear()
|
||||
onPlaylistChanged()
|
||||
return true
|
||||
}
|
||||
R.id.menu_item_save_playlist -> {
|
||||
if (mediaPlayerController.playlistSize > 0) {
|
||||
if (mediaPlayerManager.playlistSize > 0) {
|
||||
showSavePlaylistDialog()
|
||||
}
|
||||
return true
|
||||
@ -724,7 +731,7 @@ class PlayerFragment :
|
||||
if (track == null) return true
|
||||
|
||||
val songId = track.id
|
||||
val playerPosition = mediaPlayerController.playerPosition
|
||||
val playerPosition = mediaPlayerManager.playerPosition
|
||||
track.bookmarkPosition = playerPosition
|
||||
val bookmarkTime = Util.formatTotalDuration(playerPosition.toLong(), true)
|
||||
Thread {
|
||||
@ -759,7 +766,7 @@ class PlayerFragment :
|
||||
return true
|
||||
}
|
||||
R.id.menu_item_share -> {
|
||||
val mediaPlayerController = mediaPlayerController
|
||||
val mediaPlayerController = mediaPlayerManager
|
||||
val tracks: MutableList<Track?> = ArrayList()
|
||||
val playlist = mediaPlayerController.playlist
|
||||
for (item in playlist) {
|
||||
@ -794,8 +801,7 @@ class PlayerFragment :
|
||||
|
||||
private fun update(cancel: CancellationToken? = null) {
|
||||
if (cancel?.isCancellationRequested == true) return
|
||||
val mediaPlayerController = mediaPlayerController
|
||||
if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) {
|
||||
if (currentSong?.id != mediaPlayerManager.currentMediaItem?.mediaId) {
|
||||
onTrackChanged()
|
||||
}
|
||||
updateSeekBar()
|
||||
@ -803,10 +809,10 @@ class PlayerFragment :
|
||||
|
||||
private fun savePlaylistInBackground(playlistName: String) {
|
||||
Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName))
|
||||
mediaPlayerController.suggestedPlaylistName = playlistName
|
||||
mediaPlayerManager.suggestedPlaylistName = playlistName
|
||||
|
||||
// The playlist can be acquired only from the main thread
|
||||
val entries = mediaPlayerController.playlist.map {
|
||||
val entries = mediaPlayerManager.playlist.map {
|
||||
it.toTrack()
|
||||
}
|
||||
|
||||
@ -859,8 +865,8 @@ class PlayerFragment :
|
||||
|
||||
// Create listener
|
||||
val clickHandler: ((Track, Int) -> Unit) = { _, listPos ->
|
||||
val mediaIndex = mediaPlayerController.getUnshuffledIndexOf(listPos)
|
||||
mediaPlayerController.play(mediaIndex)
|
||||
val mediaIndex = mediaPlayerManager.getUnshuffledIndexOf(listPos)
|
||||
mediaPlayerManager.play(mediaIndex)
|
||||
}
|
||||
|
||||
viewAdapter.register(
|
||||
@ -924,7 +930,7 @@ class PlayerFragment :
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val pos = viewHolder.bindingAdapterPosition
|
||||
val item = mediaPlayerController.getMediaItemAt(pos)
|
||||
val item = mediaPlayerManager.getMediaItemAt(pos)
|
||||
|
||||
// Remove the item from the list quickly
|
||||
val items = viewAdapter.getCurrentList().toMutableList()
|
||||
@ -940,7 +946,7 @@ class PlayerFragment :
|
||||
Util.toast(context, songRemoved)
|
||||
|
||||
// Remove the item from the playlist
|
||||
mediaPlayerController.removeFromPlaylist(pos)
|
||||
mediaPlayerManager.removeFromPlaylist(pos)
|
||||
}
|
||||
|
||||
override fun onSelectedChanged(
|
||||
@ -960,7 +966,7 @@ class PlayerFragment :
|
||||
dragging = false
|
||||
// Move the item in the playlist separately
|
||||
Timber.i("Moving item %s to %s", startPosition, endPosition)
|
||||
mediaPlayerController.moveItemInPlaylist(startPosition, endPosition)
|
||||
mediaPlayerManager.moveItemInPlaylist(startPosition, endPosition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1038,7 +1044,7 @@ class PlayerFragment :
|
||||
}
|
||||
|
||||
private fun onPlaylistChanged() {
|
||||
val mediaPlayerController = mediaPlayerController
|
||||
val mediaPlayerController = mediaPlayerManager
|
||||
// Try to display playlist in play order
|
||||
val list = mediaPlayerController.playlistInPlayOrder
|
||||
emptyTextView.setText(R.string.playlist_empty)
|
||||
@ -1050,12 +1056,12 @@ class PlayerFragment :
|
||||
}
|
||||
|
||||
private fun onTrackChanged() {
|
||||
currentSong = mediaPlayerController.currentMediaItem?.toTrack()
|
||||
currentSong = mediaPlayerManager.currentMediaItem?.toTrack()
|
||||
|
||||
scrollToCurrent()
|
||||
val totalDuration = mediaPlayerController.playListDuration
|
||||
val totalSongs = mediaPlayerController.playlistSize
|
||||
val currentSongIndex = mediaPlayerController.currentMediaItemIndex + 1
|
||||
val totalDuration = mediaPlayerManager.playListDuration
|
||||
val totalSongs = mediaPlayerManager.playlistSize
|
||||
val currentSongIndex = mediaPlayerManager.currentMediaItemIndex + 1
|
||||
val duration = Util.formatTotalDuration(totalDuration)
|
||||
val trackFormat =
|
||||
String.format(Locale.getDefault(), "%d / %d", currentSongIndex, totalSongs)
|
||||
@ -1110,23 +1116,27 @@ class PlayerFragment :
|
||||
|
||||
updateSongRating()
|
||||
|
||||
nextButton.isEnabled = mediaPlayerController.canSeekToNext()
|
||||
previousButton.isEnabled = mediaPlayerController.canSeekToPrevious()
|
||||
updateMediaButtonActivationState()
|
||||
}
|
||||
|
||||
private fun updateMediaButtonActivationState() {
|
||||
nextButton.isEnabled = mediaPlayerManager.canSeekToNext()
|
||||
previousButton.isEnabled = mediaPlayerManager.canSeekToPrevious()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun updateSeekBar() {
|
||||
val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled
|
||||
val millisPlayed: Int = max(0, mediaPlayerController.playerPosition)
|
||||
val duration: Int = mediaPlayerController.playerDuration
|
||||
val playbackState: Int = mediaPlayerController.playbackState
|
||||
val isJukeboxEnabled: Boolean = mediaPlayerManager.isJukeboxEnabled
|
||||
val millisPlayed: Int = max(0, mediaPlayerManager.playerPosition)
|
||||
val duration: Int = mediaPlayerManager.playerDuration
|
||||
val playbackState: Int = mediaPlayerManager.playbackState
|
||||
|
||||
if (currentSong != null) {
|
||||
positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true)
|
||||
durationTextView.text = Util.formatTotalDuration(duration.toLong(), true)
|
||||
progressBar.max = if (duration == 0) 100 else duration // Work-around for apparent bug.
|
||||
progressBar.progress = millisPlayed
|
||||
progressBar.isEnabled = mediaPlayerController.isPlaying || isJukeboxEnabled
|
||||
progressBar.isEnabled = mediaPlayerManager.isPlaying || isJukeboxEnabled
|
||||
} else {
|
||||
positionTextView.setText(R.string.util_zero_time)
|
||||
durationTextView.setText(R.string.util_no_time)
|
||||
@ -1135,7 +1145,7 @@ class PlayerFragment :
|
||||
progressBar.isEnabled = false
|
||||
}
|
||||
|
||||
val progress = mediaPlayerController.bufferedPercentage
|
||||
val progress = mediaPlayerManager.bufferedPercentage
|
||||
updateBufferProgress(playbackState, progress)
|
||||
}
|
||||
|
||||
@ -1148,7 +1158,7 @@ class PlayerFragment :
|
||||
setTitle(this@PlayerFragment, downloadStatus)
|
||||
}
|
||||
Player.STATE_READY -> {
|
||||
if (mediaPlayerController.isShufflePlayEnabled) {
|
||||
if (mediaPlayerManager.isShufflePlayEnabled) {
|
||||
setTitle(
|
||||
this@PlayerFragment,
|
||||
R.string.download_playerstate_playing_shuffle
|
||||
@ -1172,7 +1182,7 @@ class PlayerFragment :
|
||||
}
|
||||
|
||||
private fun updateButtonStates(playbackState: Int) {
|
||||
val isPlaying = mediaPlayerController.isPlaying
|
||||
val isPlaying = mediaPlayerManager.isPlaying
|
||||
when (playbackState) {
|
||||
Player.STATE_READY -> {
|
||||
pauseButton.isVisible = isPlaying
|
||||
@ -1195,9 +1205,9 @@ class PlayerFragment :
|
||||
private fun seek(forward: Boolean) {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
if (forward) {
|
||||
mediaPlayerController.seekForward()
|
||||
mediaPlayerManager.seekForward()
|
||||
} else {
|
||||
mediaPlayerController.seekBack()
|
||||
mediaPlayerManager.seekBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1223,28 +1233,28 @@ class PlayerFragment :
|
||||
// Right to Left swipe
|
||||
if (e1X - e2X > swipeDistance && absX > swipeVelocity) {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
mediaPlayerController.seekToNext()
|
||||
mediaPlayerManager.seekToNext()
|
||||
return true
|
||||
}
|
||||
|
||||
// Left to Right swipe
|
||||
if (e2X - e1X > swipeDistance && absX > swipeVelocity) {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
mediaPlayerController.seekToPrevious()
|
||||
mediaPlayerManager.seekToPrevious()
|
||||
return true
|
||||
}
|
||||
|
||||
// Top to Bottom swipe
|
||||
if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
mediaPlayerController.seekTo(mediaPlayerController.playerPosition + 30000)
|
||||
mediaPlayerManager.seekTo(mediaPlayerManager.playerPosition + 30000)
|
||||
return true
|
||||
}
|
||||
|
||||
// Bottom to Top swipe
|
||||
if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000)
|
||||
mediaPlayerManager.seekTo(mediaPlayerManager.playerPosition - 8000)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -1309,7 +1319,7 @@ class PlayerFragment :
|
||||
builder.setView(layout)
|
||||
builder.setCancelable(true)
|
||||
val dialog = builder.create()
|
||||
val playlistName = mediaPlayerController.suggestedPlaylistName
|
||||
val playlistName = mediaPlayerManager.suggestedPlaylistName
|
||||
if (playlistName != null) {
|
||||
playlistNameView.setText(playlistName)
|
||||
} else {
|
||||
|
@ -42,7 +42,7 @@ import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.model.SearchListModel
|
||||
import org.moire.ultrasonic.service.DownloadService
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||
import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
|
||||
@ -63,7 +63,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
private var searchRefresh: SwipeRefreshLayout? = null
|
||||
private var searchView: SearchView? = null
|
||||
|
||||
private val mediaPlayerController: MediaPlayerController by inject()
|
||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
|
||||
private val shareHandler: ShareHandler by inject()
|
||||
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
|
||||
@ -305,15 +305,15 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
|
||||
private fun onSongSelected(song: Track, append: Boolean) {
|
||||
if (!append) {
|
||||
mediaPlayerController.clear()
|
||||
mediaPlayerManager.clear()
|
||||
}
|
||||
mediaPlayerController.addToPlaylist(
|
||||
mediaPlayerManager.addToPlaylist(
|
||||
listOf(song),
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
insertionMode = MediaPlayerController.InsertionMode.APPEND
|
||||
insertionMode = MediaPlayerManager.InsertionMode.APPEND
|
||||
)
|
||||
mediaPlayerController.play(mediaPlayerController.mediaItemCount - 1)
|
||||
mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1)
|
||||
toast(context, resources.getQuantityString(R.plurals.select_album_n_songs_added, 1, 1))
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ import org.moire.ultrasonic.log.FileLoggerTree.Companion.getLogFileSizes
|
||||
import org.moire.ultrasonic.log.FileLoggerTree.Companion.plantToTimberForest
|
||||
import org.moire.ultrasonic.log.FileLoggerTree.Companion.uprootFromTimberForest
|
||||
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.util.ConfirmationDialog
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
@ -62,7 +62,7 @@ class SettingsFragment :
|
||||
private var debugLogToFile: CheckBoxPreference? = null
|
||||
private var customCacheLocation: CheckBoxPreference? = null
|
||||
|
||||
private val mediaPlayerController: MediaPlayerController by inject()
|
||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||
@ -342,7 +342,7 @@ class SettingsFragment :
|
||||
Settings.cacheLocationUri = path
|
||||
|
||||
// Clear download queue.
|
||||
mediaPlayerController.clear()
|
||||
mediaPlayerManager.clear()
|
||||
Storage.reset()
|
||||
Storage.ensureRootIsAvailable()
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.model.TrackCollectionModel
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.subsonic.DownloadAction
|
||||
@ -82,7 +82,7 @@ open class TrackCollectionFragment(
|
||||
private var playAllButton: MenuItem? = null
|
||||
private var shareButton: MenuItem? = null
|
||||
|
||||
internal val mediaPlayerController: MediaPlayerController by inject()
|
||||
internal val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
private val shareHandler: ShareHandler by inject()
|
||||
internal var cancellationToken: CancellationToken? = null
|
||||
|
||||
|
@ -10,7 +10,7 @@ package org.moire.ultrasonic.model
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
@ -90,7 +90,7 @@ class EditServerModel(val app: Application) : AndroidViewModel(app), KoinCompone
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun queryFeatureSupport(currentServerSetting: ServerSetting): Flow<FeatureSupport> {
|
||||
val client = buildTestClient(currentServerSetting)
|
||||
// One line of magic:
|
||||
|
@ -19,7 +19,6 @@ import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Rating
|
||||
import androidx.media3.session.LibraryResult
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
@ -47,7 +46,7 @@ import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.SearchCriteria
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.service.RatingManager
|
||||
import org.moire.ultrasonic.util.MainThreadExecutor
|
||||
@ -101,10 +100,10 @@ const val PLAY_COMMAND = "play "
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibraryService) :
|
||||
class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
|
||||
|
||||
private val mediaPlayerController by inject<MediaPlayerController>()
|
||||
private val mediaPlayerManager by inject<MediaPlayerManager>()
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
private val serviceJob = SupervisorJob()
|
||||
@ -241,7 +240,7 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr
|
||||
* is stored in the track.starred value
|
||||
* See https://github.com/androidx/media/issues/33
|
||||
*/
|
||||
val track = mediaPlayerController.currentMediaItem?.toTrack()
|
||||
val track = mediaPlayerManager.currentMediaItem?.toTrack()
|
||||
if (track != null) {
|
||||
customCommandFuture = onSetRating(
|
||||
session,
|
||||
@ -254,12 +253,13 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr
|
||||
override fun onSuccess(result: SessionResult) {
|
||||
track.starred = !track.starred
|
||||
// This needs to be called on the main Thread
|
||||
// TODO: This is a looping reference
|
||||
libraryService.onUpdateNotification(session)
|
||||
}
|
||||
|
||||
override fun onFailure(t: Throwable) {
|
||||
Toast.makeText(
|
||||
mediaPlayerController.context,
|
||||
mediaPlayerManager.context,
|
||||
"There was an error updating the rating",
|
||||
LENGTH_SHORT
|
||||
).show()
|
||||
|
@ -20,7 +20,7 @@ import com.google.common.collect.ImmutableList
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.util.toTrack
|
||||
|
||||
@UnstableApi
|
||||
@ -33,7 +33,7 @@ class CustomNotificationProvider(ctx: Context) :
|
||||
* is stored in the track.starred value. See https://github.com/androidx/media/issues/33
|
||||
* TODO: Once the bug is fixed remove this circular reference!
|
||||
*/
|
||||
private val mediaPlayerController by inject<MediaPlayerController>()
|
||||
private val mediaPlayerManager by inject<MediaPlayerManager>()
|
||||
|
||||
override fun addNotificationActions(
|
||||
mediaSession: MediaSession,
|
||||
@ -48,7 +48,7 @@ class CustomNotificationProvider(ctx: Context) :
|
||||
* is stored in the track.starred value
|
||||
* See https://github.com/androidx/media/issues/33
|
||||
*/
|
||||
val rating = mediaPlayerController.currentMediaItem?.toTrack()?.starred?.let {
|
||||
val rating = mediaPlayerManager.currentMediaItem?.toTrack()?.starred?.let {
|
||||
HeartRating(
|
||||
it
|
||||
)
|
||||
|
@ -26,8 +26,7 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder.UnshuffledShuffleOrder
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
import androidx.media3.session.MediaSession
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
@ -37,6 +36,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.activity.NavigationActivity
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
@ -46,6 +46,8 @@ import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader
|
||||
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider
|
||||
import org.moire.ultrasonic.service.DownloadService
|
||||
import org.moire.ultrasonic.service.JukeboxMediaPlayer
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
@ -61,9 +63,10 @@ class PlaybackService :
|
||||
MediaLibraryService(),
|
||||
KoinComponent,
|
||||
CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var player: Player
|
||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private var equalizer: EqualizerController? = null
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
private lateinit var librarySessionCallback: MediaLibrarySession.Callback
|
||||
|
||||
@ -76,6 +79,7 @@ class PlaybackService :
|
||||
super.onCreate()
|
||||
initializeSessionAndPlayer()
|
||||
setListener(MediaSessionServiceListener())
|
||||
instance = this
|
||||
}
|
||||
|
||||
private fun getWakeModeFlag(): Int {
|
||||
@ -99,6 +103,7 @@ class PlaybackService :
|
||||
}
|
||||
|
||||
private fun releasePlayerAndSession() {
|
||||
Timber.i("Releasing player and session")
|
||||
// Broadcast that the service is being shutdown
|
||||
RxBus.stopServiceCommandPublisher.onNext(Unit)
|
||||
|
||||
@ -127,6 +132,91 @@ class PlaybackService :
|
||||
|
||||
setMediaNotificationProvider(CustomNotificationProvider(UApp.applicationContext()))
|
||||
|
||||
player = if (activeServerProvider.getActiveServer().jukeboxByDefault) {
|
||||
Timber.i("Jukebox enabled by default")
|
||||
getJukeboxPlayer()
|
||||
} else {
|
||||
getLocalPlayer()
|
||||
}
|
||||
|
||||
// Create browser interface
|
||||
librarySessionCallback = AutoMediaBrowserCallback(this)
|
||||
|
||||
// This will need to use the AutoCalls
|
||||
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||
.setSessionActivity(getPendingIntentForContent())
|
||||
.setBitmapLoader(ArtworkBitmapLoader())
|
||||
.build()
|
||||
|
||||
// Set a listener to update the API client when the active server has changed
|
||||
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
|
||||
// Set the player wake mode
|
||||
(player as? ExoPlayer)?.setWakeMode(getWakeModeFlag())
|
||||
}
|
||||
|
||||
// Set a listener to reset the ShuffleOrder
|
||||
rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle ->
|
||||
// This only applies for local playback
|
||||
val exo = if (player is ExoPlayer) {
|
||||
player as ExoPlayer
|
||||
} else {
|
||||
return@subscribe
|
||||
}
|
||||
val len = player.currentTimeline.windowCount
|
||||
|
||||
Timber.i("Resetting shuffle order, isShuffled: %s", shuffle)
|
||||
|
||||
// If disabling Shuffle return early
|
||||
if (!shuffle) {
|
||||
return@subscribe exo.setShuffleOrder(
|
||||
ShuffleOrder.UnshuffledShuffleOrder(len)
|
||||
)
|
||||
}
|
||||
|
||||
// Get the position of the current track in the unshuffled order
|
||||
val cur = player.currentMediaItemIndex
|
||||
val seed = System.currentTimeMillis()
|
||||
val random = Random(seed)
|
||||
|
||||
val list = createShuffleListFromCurrentIndex(cur, len, random)
|
||||
Timber.i("New Shuffle order: %s", list.joinToString { it.toString() })
|
||||
exo.setShuffleOrder(ShuffleOrder.DefaultShuffleOrder(list, seed))
|
||||
}
|
||||
|
||||
// Listen to the shutdown command
|
||||
rxBusSubscription += RxBus.shutdownCommandObservable.subscribe {
|
||||
Timber.i("Received destroy command via Rx")
|
||||
onDestroy()
|
||||
}
|
||||
|
||||
player.addListener(listener)
|
||||
isStarted = true
|
||||
}
|
||||
|
||||
private fun updateBackend(newBackend: MediaPlayerManager.PlayerBackend) {
|
||||
Timber.i("Switching player backends")
|
||||
// Remove old listeners
|
||||
player.removeListener(listener)
|
||||
player.release()
|
||||
|
||||
player = if (newBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) {
|
||||
getJukeboxPlayer()
|
||||
} else {
|
||||
getLocalPlayer()
|
||||
}
|
||||
|
||||
// Add fresh listeners
|
||||
player.addListener(listener)
|
||||
|
||||
mediaLibrarySession.player = player
|
||||
actualBackend = newBackend
|
||||
}
|
||||
|
||||
private fun getJukeboxPlayer(): Player {
|
||||
return JukeboxMediaPlayer()
|
||||
}
|
||||
|
||||
private fun getLocalPlayer(): Player {
|
||||
// Create a new plain OkHttpClient
|
||||
val builder = OkHttpClient.Builder()
|
||||
val client = builder.build()
|
||||
@ -147,7 +237,7 @@ class PlaybackService :
|
||||
renderer.setEnableAudioOffload(true)
|
||||
|
||||
// Create the player
|
||||
player = ExoPlayer.Builder(this)
|
||||
val player = ExoPlayer.Builder(this)
|
||||
.setAudioAttributes(getAudioAttributes(), true)
|
||||
.setWakeMode(getWakeModeFlag())
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
@ -157,59 +247,17 @@ class PlaybackService :
|
||||
.setSeekForwardIncrementMs(Settings.seekInterval.toLong())
|
||||
.build()
|
||||
|
||||
// Setup Equalizer
|
||||
equalizer = EqualizerController.create(player.audioSessionId)
|
||||
|
||||
// Enable audio offload
|
||||
if (Settings.useHwOffload)
|
||||
player.experimentalSetOffloadSchedulingEnabled(true)
|
||||
|
||||
// Create browser interface
|
||||
librarySessionCallback = AutoMediaBrowserCallback(player, this)
|
||||
|
||||
// This will need to use the AutoCalls
|
||||
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||
.setSessionActivity(getPendingIntentForContent())
|
||||
.setBitmapLoader(ArtworkBitmapLoader())
|
||||
.build()
|
||||
|
||||
// Set a listener to update the API client when the active server has changed
|
||||
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
|
||||
// Set the player wake mode
|
||||
player.setWakeMode(getWakeModeFlag())
|
||||
}
|
||||
|
||||
// Set a listener to reset the ShuffleOrder
|
||||
rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle ->
|
||||
val len = player.currentTimeline.windowCount
|
||||
|
||||
Timber.i("Resetting shuffle order, isShuffled: %s", shuffle)
|
||||
|
||||
// If disabling Shuffle return early
|
||||
if (!shuffle) {
|
||||
return@subscribe player.setShuffleOrder(UnshuffledShuffleOrder(len))
|
||||
}
|
||||
|
||||
// Get the position of the current track in the unshuffled order
|
||||
val cur = player.currentMediaItemIndex
|
||||
val seed = System.currentTimeMillis()
|
||||
val random = Random(seed)
|
||||
|
||||
val list = createShuffleListFromCurrentIndex(cur, len, random)
|
||||
Timber.i("New Shuffle order: %s", list.joinToString { it.toString() })
|
||||
player.setShuffleOrder(DefaultShuffleOrder(list, seed))
|
||||
}
|
||||
|
||||
// Listen to the shutdown command
|
||||
rxBusSubscription += RxBus.shutdownCommandObservable.subscribe {
|
||||
Timber.i("Received destroy command via Rx")
|
||||
onDestroy()
|
||||
}
|
||||
|
||||
player.addListener(listener)
|
||||
isStarted = true
|
||||
return player
|
||||
}
|
||||
|
||||
fun createShuffleListFromCurrentIndex(
|
||||
private fun createShuffleListFromCurrentIndex(
|
||||
currentIndex: Int,
|
||||
length: Int,
|
||||
random: Random
|
||||
@ -244,6 +292,7 @@ class PlaybackService :
|
||||
}
|
||||
|
||||
private fun cacheNextSongs() {
|
||||
if (actualBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) return
|
||||
Timber.d("PlaybackService caching the next songs")
|
||||
val nextSongs = Util.getPlayListFromTimeline(
|
||||
player.currentTimeline,
|
||||
@ -333,6 +382,16 @@ class PlaybackService :
|
||||
}
|
||||
|
||||
companion object {
|
||||
var actualBackend: MediaPlayerManager.PlayerBackend? = null
|
||||
|
||||
private var desiredBackend: MediaPlayerManager.PlayerBackend? = null
|
||||
fun setBackend(playerBackend: MediaPlayerManager.PlayerBackend) {
|
||||
desiredBackend = playerBackend
|
||||
instance?.updateBackend(playerBackend)
|
||||
}
|
||||
|
||||
var instance: PlaybackService? = null
|
||||
|
||||
private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic.error"
|
||||
private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic error messages"
|
||||
private const val NOTIFICATION_ID = 3009
|
||||
|
@ -7,29 +7,12 @@
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_NEXT
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_PAUSE
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_PLAY
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_STOP
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.DeviceInfo
|
||||
import androidx.media3.common.FlagSet
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.PlaybackException
|
||||
@ -39,34 +22,27 @@ import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.Clock
|
||||
import androidx.media3.common.util.ListenerSet
|
||||
import androidx.media3.common.util.Size
|
||||
import androidx.media3.session.MediaSession
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.math.roundToInt
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
|
||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.JukeboxStatus
|
||||
import org.moire.ultrasonic.playback.CustomNotificationProvider
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.Util.getPendingIntentToShowPlayer
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util.sleepQuietly
|
||||
import org.moire.ultrasonic.util.Util.stopForegroundRemoveNotification
|
||||
import timber.log.Timber
|
||||
|
||||
private const val STATUS_UPDATE_INTERVAL_SECONDS = 5L
|
||||
private const val SEEK_INCREMENT_SECONDS = 5L
|
||||
private const val SEEK_START_AFTER_SECONDS = 5
|
||||
private const val QUEUE_POLL_INTERVAL_SECONDS = 1L
|
||||
|
||||
/**
|
||||
@ -86,135 +62,64 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
private val timeOfLastUpdate = AtomicLong()
|
||||
private var jukeboxStatus: JukeboxStatus? = null
|
||||
private var previousJukeboxStatus: JukeboxStatus? = null
|
||||
private var gain = 0.5f
|
||||
private var volumeToast: VolumeToast? = null
|
||||
private var gain = (MAX_GAIN / 3)
|
||||
private val floatGain: Float
|
||||
get() = gain.toFloat() / MAX_GAIN
|
||||
|
||||
private var serviceThread: Thread? = null
|
||||
|
||||
private var listeners: MutableList<Player.Listener> = mutableListOf()
|
||||
private var listeners: ListenerSet<Player.Listener>
|
||||
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")
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (running.get()) return
|
||||
private var _currentIndex: Int = 0
|
||||
private var currentIndex: Int
|
||||
get() = _currentIndex
|
||||
set(value) {
|
||||
// This must never be smaller 0
|
||||
_currentIndex = if (value >= 0) value else 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
// This is quite important, by setting the DeviceInfo the player is recognized by
|
||||
// Android as being a remote playback surface
|
||||
val DEVICE_INFO = DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 10)
|
||||
val running = AtomicBoolean()
|
||||
const val MAX_GAIN = 10
|
||||
}
|
||||
|
||||
init {
|
||||
running.set(true)
|
||||
|
||||
listeners = ListenerSet(
|
||||
applicationLooper,
|
||||
Clock.DEFAULT
|
||||
) { listener: Player.Listener, flags: FlagSet? ->
|
||||
listener.onEvents(
|
||||
this,
|
||||
Player.Events(
|
||||
flags!!
|
||||
)
|
||||
)
|
||||
}
|
||||
tasks.clear()
|
||||
updatePlaylist()
|
||||
stop()
|
||||
|
||||
startFuture?.set(this)
|
||||
|
||||
startProcessTasks()
|
||||
|
||||
notificationManagerCompat = NotificationManagerCompat.from(this)
|
||||
mediaSession = MediaSession.Builder(applicationContext(), this)
|
||||
.setId("jukebox")
|
||||
.setSessionActivity(getPendingIntentToShowPlayer(this))
|
||||
.build()
|
||||
val notification = notificationProvider.createNotification(
|
||||
mediaSession,
|
||||
ImmutableList.of(),
|
||||
JukeboxNotificationActionFactory()
|
||||
) {}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
startForeground(
|
||||
notification.notificationId,
|
||||
notification.notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||
)
|
||||
} else {
|
||||
startForeground(
|
||||
notification.notificationId, notification.notification
|
||||
)
|
||||
}
|
||||
|
||||
Timber.d("Started Jukebox Service")
|
||||
}
|
||||
@Suppress("MagicNumber")
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
if (Intent.ACTION_MEDIA_BUTTON != intent?.action) return START_STICKY
|
||||
|
||||
val extras = intent.extras
|
||||
if ((extras != null) && extras.containsKey(Intent.EXTRA_KEY_EVENT)) {
|
||||
val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
extras.getParcelable(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
extras.getParcelable<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() {
|
||||
override fun release() {
|
||||
tasks.clear()
|
||||
stop()
|
||||
|
||||
if (!running.get()) return
|
||||
running.set(false)
|
||||
|
||||
serviceThread!!.join()
|
||||
serviceThread?.join()
|
||||
|
||||
stopForegroundRemoveNotification()
|
||||
mediaSession.release()
|
||||
|
||||
super.onDestroy()
|
||||
Timber.d("Stopped Jukebox Service")
|
||||
}
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun requestStop() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
val notification = notificationProvider.createNotification(
|
||||
mediaSession,
|
||||
ImmutableList.of(),
|
||||
JukeboxNotificationActionFactory()
|
||||
) {}
|
||||
notificationManagerCompat.notify(notification.notificationId, notification.notification)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val running = AtomicBoolean()
|
||||
private var startFuture: SettableFuture<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) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
@ -263,14 +168,20 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
tasks.add(Skip(mediaItemIndex, positionSeconds))
|
||||
currentIndex = mediaItemIndex
|
||||
updateAvailableCommands()
|
||||
}
|
||||
|
||||
override fun seekBack() {
|
||||
seekTo(0L.coerceAtMost((jukeboxStatus?.positionSeconds ?: 0) - SEEK_INCREMENT_SECONDS))
|
||||
seekTo(
|
||||
0L.coerceAtMost(
|
||||
(jukeboxStatus?.positionSeconds ?: 0) -
|
||||
Settings.seekIntervalMillis
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun seekForward() {
|
||||
seekTo((jukeboxStatus?.positionSeconds ?: 0) + SEEK_INCREMENT_SECONDS)
|
||||
seekTo((jukeboxStatus?.positionSeconds ?: 0) + Settings.seekIntervalMillis)
|
||||
}
|
||||
|
||||
override fun isCurrentMediaItemSeekable() = true
|
||||
@ -292,8 +203,11 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
|
||||
override fun getAvailableCommands(): Player.Commands {
|
||||
val commandsBuilder = Player.Commands.Builder().addAll(
|
||||
Player.COMMAND_SET_VOLUME,
|
||||
Player.COMMAND_GET_VOLUME
|
||||
Player.COMMAND_CHANGE_MEDIA_ITEMS,
|
||||
Player.COMMAND_GET_TIMELINE,
|
||||
Player.COMMAND_GET_DEVICE_VOLUME,
|
||||
Player.COMMAND_ADJUST_DEVICE_VOLUME,
|
||||
Player.COMMAND_SET_DEVICE_VOLUME
|
||||
)
|
||||
if (isPlaying) commandsBuilder.add(Player.COMMAND_STOP)
|
||||
if (playlist.isNotEmpty()) {
|
||||
@ -306,8 +220,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
Player.COMMAND_SEEK_FORWARD,
|
||||
Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
|
||||
Player.COMMAND_SEEK_TO_MEDIA_ITEM,
|
||||
)
|
||||
if (currentIndex > 0) commandsBuilder.addAll(
|
||||
// Seeking back is always available
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS,
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
|
||||
)
|
||||
@ -323,6 +236,18 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
return availableCommands.contains(command)
|
||||
}
|
||||
|
||||
private fun updateAvailableCommands() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.sendEvent(
|
||||
Player.EVENT_AVAILABLE_COMMANDS_CHANGED
|
||||
) { listener: Player.Listener ->
|
||||
listener.onAvailableCommandsChanged(
|
||||
availableCommands
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPlayWhenReady(): Boolean {
|
||||
return isPlaying
|
||||
}
|
||||
@ -358,21 +283,43 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
|
||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
|
||||
|
||||
override fun setVolume(volume: Float) {
|
||||
override fun setDeviceVolume(volume: Int) {
|
||||
gain = volume
|
||||
tasks.remove(SetGain::class.java)
|
||||
tasks.add(SetGain(volume))
|
||||
val context = applicationContext()
|
||||
if (volumeToast == null) volumeToast = VolumeToast(context)
|
||||
volumeToast!!.setVolume(volume)
|
||||
tasks.add(SetGain(floatGain))
|
||||
|
||||
// We must trigger an event so that the Controller knows the new volume
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.queueEvent(Player.EVENT_DEVICE_VOLUME_CHANGED) {
|
||||
it.onDeviceVolumeChanged(
|
||||
gain,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun increaseDeviceVolume() {
|
||||
gain = (gain + 1).coerceAtMost(MAX_GAIN)
|
||||
deviceVolume = gain
|
||||
}
|
||||
|
||||
override fun decreaseDeviceVolume() {
|
||||
gain = (gain - 1).coerceAtLeast(0)
|
||||
deviceVolume = gain
|
||||
}
|
||||
|
||||
override fun setDeviceMuted(muted: Boolean) {
|
||||
gain = 0
|
||||
deviceVolume = gain
|
||||
}
|
||||
|
||||
override fun getVolume(): Float {
|
||||
return gain
|
||||
return floatGain
|
||||
}
|
||||
|
||||
override fun getDeviceVolume(): Int {
|
||||
return (gain * 100).toInt()
|
||||
return gain
|
||||
}
|
||||
|
||||
override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) {
|
||||
@ -444,7 +391,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
|
||||
override fun seekToPrevious() {
|
||||
if ((jukeboxStatus?.positionSeconds ?: 0) > SEEK_START_AFTER_SECONDS) {
|
||||
if ((jukeboxStatus?.positionSeconds ?: 0) > (Settings.seekIntervalMillis)) {
|
||||
seekTo(currentIndex, 0)
|
||||
return
|
||||
}
|
||||
@ -499,51 +446,63 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
@Suppress("LoopWithTooManyJumpStatements")
|
||||
private fun processTasks() {
|
||||
Timber.d("JukeboxMediaPlayer processTasks starting")
|
||||
while (true) {
|
||||
while (running.get()) {
|
||||
// Sleep a bit to spare processor time if we loop a lot
|
||||
sleepQuietly(10)
|
||||
// This is only necessary if Ultrasonic goes offline sooner than the thread stops
|
||||
if (isOffline()) continue
|
||||
var task: JukeboxTask? = null
|
||||
try {
|
||||
task = tasks.poll()
|
||||
// If running is false, exit when the queue is empty
|
||||
if (task == null && !running.get()) break
|
||||
if (task == null) continue
|
||||
task = tasks.poll() ?: continue
|
||||
Timber.v("JukeBoxMediaPlayer processTasks processes Task %s", task::class)
|
||||
val status = task.execute()
|
||||
onStatusUpdate(status)
|
||||
} catch (x: Throwable) {
|
||||
onError(task, x)
|
||||
} catch (all: Throwable) {
|
||||
onError(task, all)
|
||||
}
|
||||
}
|
||||
Timber.d("JukeboxMediaPlayer processTasks stopped")
|
||||
}
|
||||
|
||||
// Jukebox status contains data received from the server, we need to validate it!
|
||||
private fun onStatusUpdate(jukeboxStatus: JukeboxStatus) {
|
||||
timeOfLastUpdate.set(System.currentTimeMillis())
|
||||
previousJukeboxStatus = this.jukeboxStatus
|
||||
this.jukeboxStatus = jukeboxStatus
|
||||
var shouldUpdateCommands = false
|
||||
|
||||
// Ensure that the index is never smaller than 0
|
||||
// If -1 assume that this means we are not playing
|
||||
if (jukeboxStatus.currentPlayingIndex != null && jukeboxStatus.currentPlayingIndex!! < 0) {
|
||||
jukeboxStatus.currentPlayingIndex = 0
|
||||
jukeboxStatus.isPlaying = false
|
||||
}
|
||||
currentIndex = jukeboxStatus.currentPlayingIndex ?: currentIndex
|
||||
|
||||
if (jukeboxStatus.isPlaying != previousJukeboxStatus?.isPlaying) {
|
||||
shouldUpdateCommands = true
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
listeners.queueEvent(Player.EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
it.onPlaybackStateChanged(
|
||||
if (jukeboxStatus.isPlaying) Player.STATE_READY else Player.STATE_IDLE
|
||||
)
|
||||
}
|
||||
|
||||
listeners.queueEvent(Player.EVENT_IS_PLAYING_CHANGED) {
|
||||
it.onIsPlayingChanged(jukeboxStatus.isPlaying)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jukeboxStatus.currentPlayingIndex != previousJukeboxStatus?.currentPlayingIndex) {
|
||||
shouldUpdateCommands = true
|
||||
currentIndex = jukeboxStatus.currentPlayingIndex ?: 0
|
||||
val currentMedia =
|
||||
if (currentIndex > 0 && currentIndex < playlist.size) playlist[currentIndex]
|
||||
else MediaItem.EMPTY
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
listeners.queueEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) {
|
||||
it.onMediaItemTransition(
|
||||
currentMedia,
|
||||
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
|
||||
@ -552,44 +511,39 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
}
|
||||
|
||||
updateNotification()
|
||||
if (shouldUpdateCommands) updateAvailableCommands()
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.flushEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError(task: JukeboxTask?, x: Throwable) {
|
||||
var exception: PlaybackException? = null
|
||||
if (x is ApiNotSupportedException && task !is Stop) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onPlayerError(
|
||||
PlaybackException(
|
||||
"Jukebox server too old",
|
||||
null,
|
||||
R.string.download_jukebox_server_too_old
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
exception = PlaybackException(
|
||||
"Jukebox server too old",
|
||||
null,
|
||||
R.string.download_jukebox_server_too_old
|
||||
)
|
||||
} else if (x is OfflineException && task !is Stop) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onPlayerError(
|
||||
PlaybackException(
|
||||
"Jukebox offline",
|
||||
null,
|
||||
R.string.download_jukebox_offline
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
exception = PlaybackException(
|
||||
"Jukebox offline",
|
||||
null,
|
||||
R.string.download_jukebox_offline
|
||||
)
|
||||
} else if (x is SubsonicRESTException && x.code == 50 && task !is Stop) {
|
||||
exception = PlaybackException(
|
||||
"Jukebox not authorized",
|
||||
null,
|
||||
R.string.download_jukebox_not_authorized
|
||||
)
|
||||
}
|
||||
|
||||
if (exception != null) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onPlayerError(
|
||||
PlaybackException(
|
||||
"Jukebox not authorized",
|
||||
null,
|
||||
R.string.download_jukebox_not_authorized
|
||||
)
|
||||
)
|
||||
listeners.sendEvent(Player.EVENT_PLAYER_ERROR) {
|
||||
it.onPlayerError(exception)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -608,8 +562,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
tasks.add(SetPlaylist(ids))
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onTimelineChanged(
|
||||
listeners.sendEvent(
|
||||
Player.EVENT_TIMELINE_CHANGED
|
||||
) { listener: Player.Listener ->
|
||||
listener.onTimelineChanged(
|
||||
PlaylistTimeline(playlist),
|
||||
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED
|
||||
)
|
||||
@ -719,25 +675,6 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private class VolumeToast(context: Context) : Toast(context) {
|
||||
private val progressBar: ProgressBar
|
||||
fun setVolume(volume: Float) {
|
||||
progressBar.progress = (100 * volume).roundToInt()
|
||||
show()
|
||||
}
|
||||
|
||||
init {
|
||||
duration = LENGTH_SHORT
|
||||
val inflater =
|
||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val view = inflater.inflate(R.layout.jukebox_volume, null)
|
||||
progressBar = view.findViewById<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
|
||||
override fun isCurrentMediaItemDynamic(): Boolean {
|
||||
return false
|
||||
@ -748,15 +685,15 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
|
||||
override fun getMaxSeekToPreviousPosition(): Long {
|
||||
return SEEK_START_AFTER_SECONDS * 1000L
|
||||
return Settings.seekInterval.toLong()
|
||||
}
|
||||
|
||||
override fun getSeekBackIncrement(): Long {
|
||||
return SEEK_INCREMENT_SECONDS * 1000L
|
||||
return Settings.seekInterval.toLong()
|
||||
}
|
||||
|
||||
override fun getSeekForwardIncrement(): Long {
|
||||
return SEEK_INCREMENT_SECONDS * 1000L
|
||||
return Settings.seekInterval.toLong()
|
||||
}
|
||||
|
||||
override fun isLoading(): Boolean {
|
||||
@ -779,6 +716,8 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
return AudioAttributes.DEFAULT
|
||||
}
|
||||
|
||||
override fun setVolume(volume: Float) {}
|
||||
|
||||
override fun getVideoSize(): VideoSize {
|
||||
return VideoSize(0, 0)
|
||||
}
|
||||
@ -824,7 +763,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
|
||||
override fun getDeviceInfo(): DeviceInfo {
|
||||
return DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 1)
|
||||
return DEVICE_INFO
|
||||
}
|
||||
|
||||
override fun getPlayerError(): PlaybackException? {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
@ -26,7 +25,7 @@ import androidx.media3.common.Tracks
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
abstract class JukeboxUnimplementedFunctions : Service(), Player {
|
||||
abstract class JukeboxUnimplementedFunctions : Player {
|
||||
|
||||
override fun setMediaItems(mediaItems: MutableList<MediaItem>) {
|
||||
TODO("Not yet implemented")
|
||||
@ -140,10 +139,6 @@ abstract class JukeboxUnimplementedFunctions : Service(), Player {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getCurrentTracks(): Tracks {
|
||||
// TODO Dummy information is returned for now, this seems to work
|
||||
return Tracks.EMPTY
|
||||
@ -228,20 +223,4 @@ abstract class JukeboxUnimplementedFunctions : Service(), Player {
|
||||
override fun clearVideoTextureView(textureView: TextureView?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setDeviceVolume(volume: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun increaseDeviceVolume() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun decreaseDeviceVolume() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setDeviceMuted(muted: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ import timber.log.Timber
|
||||
class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
private lateinit var ratingManager: RatingManager
|
||||
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 var created = false
|
||||
@ -64,7 +64,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
return
|
||||
}
|
||||
|
||||
mediaPlayerController.onCreate {
|
||||
mediaPlayerManager.onCreate {
|
||||
restoreLastSession(autoPlay, afterRestore)
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
|
||||
Timber.i("Restoring %s songs", it!!.songs.size)
|
||||
|
||||
mediaPlayerController.restore(
|
||||
mediaPlayerManager.restore(
|
||||
it,
|
||||
autoPlay,
|
||||
false
|
||||
@ -110,7 +110,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
if (intent == null) return
|
||||
|
||||
val intentAction = intent.action
|
||||
if (intentAction == null || intentAction.isEmpty()) return
|
||||
if (intentAction.isNullOrEmpty()) return
|
||||
|
||||
Timber.i("Received intent: %s", intentAction)
|
||||
|
||||
@ -146,15 +146,15 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
val state = extras.getInt("state")
|
||||
|
||||
if (state == 0) {
|
||||
if (!mediaPlayerController.isJukeboxEnabled) {
|
||||
mediaPlayerController.pause()
|
||||
if (!mediaPlayerManager.isJukeboxEnabled) {
|
||||
mediaPlayerManager.pause()
|
||||
}
|
||||
} else if (state == 1) {
|
||||
if (!mediaPlayerController.isJukeboxEnabled &&
|
||||
Settings.resumePlayOnHeadphonePlug && !mediaPlayerController.isPlaying
|
||||
if (!mediaPlayerManager.isJukeboxEnabled &&
|
||||
Settings.resumePlayOnHeadphonePlug && !mediaPlayerManager.isPlaying
|
||||
) {
|
||||
mediaPlayerController.prepare()
|
||||
mediaPlayerController.play()
|
||||
mediaPlayerManager.prepare()
|
||||
mediaPlayerManager.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -183,18 +183,18 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
onCreate(autoStart) {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
|
||||
KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause()
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.seekToPrevious()
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.seekToNext()
|
||||
KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop()
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play()
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause()
|
||||
KeyEvent.KEYCODE_1 -> mediaPlayerController.legacySetRating(1)
|
||||
KeyEvent.KEYCODE_2 -> mediaPlayerController.legacySetRating(2)
|
||||
KeyEvent.KEYCODE_3 -> mediaPlayerController.legacySetRating(3)
|
||||
KeyEvent.KEYCODE_4 -> mediaPlayerController.legacySetRating(4)
|
||||
KeyEvent.KEYCODE_5 -> mediaPlayerController.legacySetRating(5)
|
||||
KeyEvent.KEYCODE_STAR -> mediaPlayerController.legacyToggleStar()
|
||||
KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerManager.togglePlayPause()
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerManager.seekToPrevious()
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerManager.seekToNext()
|
||||
KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerManager.stop()
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerManager.play()
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerManager.pause()
|
||||
KeyEvent.KEYCODE_1 -> mediaPlayerManager.legacySetRating(1)
|
||||
KeyEvent.KEYCODE_2 -> mediaPlayerManager.legacySetRating(2)
|
||||
KeyEvent.KEYCODE_3 -> mediaPlayerManager.legacySetRating(3)
|
||||
KeyEvent.KEYCODE_4 -> mediaPlayerManager.legacySetRating(4)
|
||||
KeyEvent.KEYCODE_5 -> mediaPlayerManager.legacySetRating(5)
|
||||
KeyEvent.KEYCODE_STAR -> mediaPlayerManager.legacyToggleStar()
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
@ -222,17 +222,17 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
// We can receive intents when everything is stopped, so we need to start
|
||||
onCreate(autoStart) {
|
||||
when (action) {
|
||||
Constants.CMD_PLAY -> mediaPlayerController.play()
|
||||
Constants.CMD_PLAY -> mediaPlayerManager.play()
|
||||
Constants.CMD_RESUME_OR_PLAY ->
|
||||
// If Ultrasonic wasn't running, the autoStart is enough to resume,
|
||||
// no need to call anything
|
||||
if (isRunning) mediaPlayerController.resumeOrPlay()
|
||||
if (isRunning) mediaPlayerManager.resumeOrPlay()
|
||||
|
||||
Constants.CMD_NEXT -> mediaPlayerController.seekToNext()
|
||||
Constants.CMD_PREVIOUS -> mediaPlayerController.seekToPrevious()
|
||||
Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause()
|
||||
Constants.CMD_STOP -> mediaPlayerController.stop()
|
||||
Constants.CMD_PAUSE -> mediaPlayerController.pause()
|
||||
Constants.CMD_NEXT -> mediaPlayerManager.seekToNext()
|
||||
Constants.CMD_PREVIOUS -> mediaPlayerManager.seekToPrevious()
|
||||
Constants.CMD_TOGGLEPAUSE -> mediaPlayerManager.togglePlayPause()
|
||||
Constants.CMD_STOP -> mediaPlayerManager.stop()
|
||||
Constants.CMD_PAUSE -> mediaPlayerManager.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,6 @@ import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS
|
||||
import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO
|
||||
import androidx.media3.common.Player.REPEAT_MODE_OFF
|
||||
import androidx.media3.common.Rating
|
||||
@ -50,12 +48,13 @@ private const val CONTROLLER_SWITCH_DELAY = 500L
|
||||
private const val VOLUME_DELTA = 0.05f
|
||||
|
||||
/**
|
||||
* The implementation of the Media Player Controller.
|
||||
* The Media Player Manager can forward commands to the Media3 controller as
|
||||
* well as switch between different player interfaces (local, remote, cast etc).
|
||||
* This class contains everything that is necessary for the Application UI
|
||||
* to control the Media Player implementation.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class MediaPlayerController(
|
||||
class MediaPlayerManager(
|
||||
private val playbackStateSerializer: PlaybackStateSerializer,
|
||||
private val externalStorageMonitor: ExternalStorageMonitor,
|
||||
val context: Context
|
||||
@ -97,15 +96,15 @@ class MediaPlayerController(
|
||||
* We run the event through RxBus in order to throttle them
|
||||
*/
|
||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||
val start = controller?.currentTimeline?.getFirstWindowIndex(isShufflePlayEnabled)
|
||||
val start = timeline.getFirstWindowIndex(isShufflePlayEnabled)
|
||||
Timber.w("On timeline changed. First shuffle play at index: %s", start)
|
||||
deferredPlay?.let {
|
||||
Timber.w("Executing deferred shuffle play")
|
||||
it()
|
||||
deferredPlay = null
|
||||
}
|
||||
|
||||
RxBus.playlistPublisher.onNext(playlist.map(MediaItem::toTrack))
|
||||
val playlist = Util.getPlayListFromTimeline(timeline, false).map(MediaItem::toTrack)
|
||||
RxBus.playlistPublisher.onNext(playlist)
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
@ -179,11 +178,8 @@ class MediaPlayerController(
|
||||
fun onCreate(onCreated: () -> Unit) {
|
||||
if (created) return
|
||||
externalStorageMonitor.onCreate { reset() }
|
||||
if (activeServerProvider.getActiveServer().jukeboxByDefault) {
|
||||
switchToJukebox(onCreated)
|
||||
} else {
|
||||
switchToLocalPlayer(onCreated)
|
||||
}
|
||||
|
||||
createMediaController(onCreated)
|
||||
|
||||
rxBusSubscription += RxBus.activeServerChangingObservable.subscribe { oldServer ->
|
||||
if (oldServer != OFFLINE_DB_ID) {
|
||||
@ -195,8 +191,7 @@ class MediaPlayerController(
|
||||
if (controller is JukeboxMediaPlayer) {
|
||||
// When the server changes, the Jukebox should be released.
|
||||
// The new server won't understand the jukebox requests of the old one.
|
||||
releaseJukebox(controller)
|
||||
controller = null
|
||||
switchToLocalPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,6 +241,22 @@ class MediaPlayerController(
|
||||
Timber.i("MediaPlayerController started")
|
||||
}
|
||||
|
||||
private fun createMediaController(onCreated: () -> Unit) {
|
||||
mediaControllerFuture = MediaController.Builder(
|
||||
context,
|
||||
sessionToken
|
||||
).buildAsync()
|
||||
|
||||
mediaControllerFuture?.addListener({
|
||||
controller = mediaControllerFuture?.get()
|
||||
|
||||
Timber.i("MediaController Instance received")
|
||||
controller?.addListener(listeners)
|
||||
onCreated()
|
||||
Timber.i("MediaPlayerController creation complete")
|
||||
}, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
private fun playerStateChangedHandler() {
|
||||
val currentPlaying = controller?.currentMediaItem?.toTrack() ?: return
|
||||
|
||||
@ -262,6 +273,10 @@ class MediaPlayerController(
|
||||
}
|
||||
}
|
||||
|
||||
fun addListener(listener: Player.Listener) {
|
||||
controller?.addListener(listener)
|
||||
}
|
||||
|
||||
private fun clearBookmark() {
|
||||
// This method is called just before we update the cachedMediaItem,
|
||||
// so in fact cachedMediaItem will refer to the track that has just finished.
|
||||
@ -336,7 +351,6 @@ class MediaPlayerController(
|
||||
@Synchronized
|
||||
fun play(index: Int) {
|
||||
controller?.seekTo(index, 0L)
|
||||
// FIXME CHECK ITS NOT MAKING PROBLEMS
|
||||
controller?.prepare()
|
||||
controller?.play()
|
||||
}
|
||||
@ -538,7 +552,7 @@ class MediaPlayerController(
|
||||
|
||||
@Synchronized
|
||||
fun canSeekToPrevious(): Boolean {
|
||||
return controller?.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS) == true
|
||||
return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS) == true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@ -548,7 +562,7 @@ class MediaPlayerController(
|
||||
|
||||
@Synchronized
|
||||
fun canSeekToNext(): Boolean {
|
||||
return controller?.isCommandAvailable(COMMAND_SEEK_TO_NEXT) == true
|
||||
return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT) == true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@ -580,102 +594,49 @@ class MediaPlayerController(
|
||||
|
||||
@set:Synchronized
|
||||
var isJukeboxEnabled: Boolean
|
||||
get() = controller is JukeboxMediaPlayer
|
||||
set(jukeboxEnabled) {
|
||||
if (jukeboxEnabled) {
|
||||
switchToJukebox {}
|
||||
get() = PlaybackService.actualBackend == PlayerBackend.JUKEBOX
|
||||
set(shouldEnable) {
|
||||
if (shouldEnable) {
|
||||
switchToJukebox()
|
||||
} else {
|
||||
switchToLocalPlayer {}
|
||||
switchToLocalPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchToJukebox(onCreated: () -> Unit) {
|
||||
if (controller is JukeboxMediaPlayer) return
|
||||
val currentPlaylist = playlist
|
||||
val currentIndex = controller?.currentMediaItemIndex ?: 0
|
||||
val currentPosition = controller?.currentPosition ?: 0
|
||||
private fun switchToJukebox() {
|
||||
if (isJukeboxEnabled) return
|
||||
scheduleSwitchTo(PlayerBackend.JUKEBOX)
|
||||
DownloadService.requestStop()
|
||||
controller?.pause()
|
||||
controller?.stop()
|
||||
val oldController = controller
|
||||
controller = null // While we switch, the controller shouldn't be available
|
||||
|
||||
// Stop() won't work if we don't give it time to be processed
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (oldController != null) releaseLocalPlayer(oldController)
|
||||
setupJukebox {
|
||||
controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition)
|
||||
onCreated()
|
||||
}
|
||||
}, CONTROLLER_SWITCH_DELAY)
|
||||
}
|
||||
|
||||
private fun switchToLocalPlayer(onCreated: () -> Unit) {
|
||||
if (controller is MediaController) return
|
||||
val currentPlaylist = playlist
|
||||
private fun switchToLocalPlayer() {
|
||||
if (!isJukeboxEnabled) return
|
||||
scheduleSwitchTo(PlayerBackend.LOCAL)
|
||||
controller?.stop()
|
||||
}
|
||||
|
||||
private fun scheduleSwitchTo(newBackend: PlayerBackend) {
|
||||
val currentPlaylist = playlist.toList()
|
||||
val currentIndex = controller?.currentMediaItemIndex ?: 0
|
||||
val currentPosition = controller?.currentPosition ?: 0
|
||||
controller?.stop()
|
||||
val oldController = controller
|
||||
controller = null // While we switch, the controller shouldn't be available
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (oldController != null) releaseJukebox(oldController)
|
||||
setupLocalPlayer {
|
||||
controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition)
|
||||
onCreated()
|
||||
}
|
||||
// Change the backend
|
||||
PlaybackService.setBackend(newBackend)
|
||||
// Restore the media items
|
||||
controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition)
|
||||
}, CONTROLLER_SWITCH_DELAY)
|
||||
}
|
||||
|
||||
private fun releaseController() {
|
||||
when (controller) {
|
||||
null -> return
|
||||
is JukeboxMediaPlayer -> releaseJukebox(controller)
|
||||
is MediaController -> releaseLocalPlayer(controller)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupLocalPlayer(onCreated: () -> Unit) {
|
||||
mediaControllerFuture = MediaController.Builder(
|
||||
context,
|
||||
sessionToken
|
||||
).buildAsync()
|
||||
|
||||
mediaControllerFuture?.addListener({
|
||||
controller = mediaControllerFuture?.get()
|
||||
|
||||
Timber.i("MediaController Instance received")
|
||||
controller?.addListener(listeners)
|
||||
onCreated()
|
||||
Timber.i("MediaPlayerController creation complete")
|
||||
}, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
private fun releaseLocalPlayer(player: Player?) {
|
||||
player?.removeListener(listeners)
|
||||
player?.release()
|
||||
controller?.removeListener(listeners)
|
||||
controller?.release()
|
||||
if (mediaControllerFuture != null) MediaController.releaseFuture(mediaControllerFuture!!)
|
||||
Timber.i("MediaPlayerController released")
|
||||
}
|
||||
|
||||
private fun setupJukebox(onCreated: () -> Unit) {
|
||||
val jukeboxFuture = JukeboxMediaPlayer.requestStart()
|
||||
jukeboxFuture?.addListener({
|
||||
controller = jukeboxFuture.get()
|
||||
onCreated()
|
||||
controller?.addListener(listeners)
|
||||
Timber.i("JukeboxService creation complete")
|
||||
}, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
private fun releaseJukebox(player: Player?) {
|
||||
val jukebox = player as JukeboxMediaPlayer?
|
||||
jukebox?.removeListener(listeners)
|
||||
jukebox?.requestStop()
|
||||
Timber.i("JukeboxService released")
|
||||
}
|
||||
|
||||
/**
|
||||
* This function calls the music service directly and
|
||||
* therefore can't be called from the main thread
|
||||
@ -700,10 +661,6 @@ class MediaPlayerController(
|
||||
controller?.volume = gain
|
||||
}
|
||||
|
||||
fun setVolume(volume: Float) {
|
||||
controller?.volume = volume
|
||||
}
|
||||
|
||||
/*
|
||||
* Sets the rating of the current track
|
||||
*/
|
||||
@ -841,4 +798,6 @@ class MediaPlayerController(
|
||||
enum class InsertionMode {
|
||||
CLEAR, APPEND, AFTER_CURRENT
|
||||
}
|
||||
|
||||
enum class PlayerBackend { JUKEBOX, LOCAL }
|
||||
}
|
@ -18,7 +18,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.service.DownloadService
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.executeTaskWithToast
|
||||
@ -28,7 +28,7 @@ import org.moire.ultrasonic.util.executeTaskWithToast
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class DownloadHandler(
|
||||
val mediaPlayerController: MediaPlayerController,
|
||||
val mediaPlayerManager: MediaPlayerManager,
|
||||
private val networkAndStorageChecker: NetworkAndStorageChecker
|
||||
) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
private val maxSongs = 500
|
||||
@ -150,16 +150,16 @@ class DownloadHandler(
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
|
||||
val insertionMode = when {
|
||||
append -> MediaPlayerController.InsertionMode.APPEND
|
||||
playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT
|
||||
else -> MediaPlayerController.InsertionMode.CLEAR
|
||||
append -> MediaPlayerManager.InsertionMode.APPEND
|
||||
playNext -> MediaPlayerManager.InsertionMode.AFTER_CURRENT
|
||||
else -> MediaPlayerManager.InsertionMode.CLEAR
|
||||
}
|
||||
|
||||
if (playlistName != null) {
|
||||
mediaPlayerController.suggestedPlaylistName = playlistName
|
||||
mediaPlayerManager.suggestedPlaylistName = playlistName
|
||||
}
|
||||
|
||||
mediaPlayerController.addToPlaylist(
|
||||
mediaPlayerManager.addToPlaylist(
|
||||
songs,
|
||||
autoPlay,
|
||||
shuffle,
|
||||
|
@ -19,7 +19,7 @@ import org.koin.java.KoinJavaComponent.inject
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.domain.Playlist
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.util.FileUtil.getAlbumArtFile
|
||||
import org.moire.ultrasonic.util.FileUtil.getCompleteFile
|
||||
import org.moire.ultrasonic.util.FileUtil.getPartialFile
|
||||
@ -235,8 +235,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinCompo
|
||||
|
||||
private fun findFilesToNotDelete(): Set<String> {
|
||||
val filesToNotDelete: MutableSet<String> = HashSet(5)
|
||||
val mediaController = inject<MediaPlayerController>(
|
||||
MediaPlayerController::class.java
|
||||
val mediaController = inject<MediaPlayerManager>(
|
||||
MediaPlayerManager::class.java
|
||||
)
|
||||
|
||||
val playlist = mainScope.future { mediaController.value.playlist }.get()
|
||||
|
@ -128,6 +128,9 @@ object Settings {
|
||||
var seekInterval
|
||||
by StringIntSetting(getKey(R.string.setting_key_increment_time), 5000)
|
||||
|
||||
val seekIntervalMillis: Long
|
||||
get() = (seekInterval / 1000).toLong()
|
||||
|
||||
@JvmStatic
|
||||
var mediaButtonsEnabled
|
||||
by BooleanSetting(getKey(R.string.setting_key_media_buttons), true)
|
||||
|
@ -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>
|
@ -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_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_volume">Hlasitost vzdáleného přístroje</string>
|
||||
<string name="download.menu_equalizer">Ekvalizér</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox vypnut</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox zapnut</string>
|
||||
|
@ -61,7 +61,6 @@
|
||||
<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_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_jukebox_off">Jukebox Aus</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox An</string>
|
||||
|
@ -62,7 +62,6 @@
|
||||
<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_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_jukebox_off">Apagar Jukebox</string>
|
||||
<string name="download.menu_jukebox_on">Encender Jukebox</string>
|
||||
|
@ -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_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_volume">Volume sur serveur distant</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_on">Activer le mode jukebox</string>
|
||||
|
@ -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_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_volume">Hangerő távvezérlése</string>
|
||||
<string name="download.menu_equalizer">Equalizer</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox ki</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox be</string>
|
||||
|
@ -45,7 +45,6 @@
|
||||
<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_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_jukebox_off">Jukebox spento</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox acceso</string>
|
||||
|
@ -42,7 +42,6 @@
|
||||
<string name="download.jukebox_on">リモートコントロールがオンになりました。サーバーで音楽が再生されます。</string>
|
||||
<string name="download.jukebox_off">リモートコントロールがオフになりました。モバイル端末で音楽が再生されます。</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_lyrics">歌詞</string>
|
||||
<string name="download.menu_show_album">アルバムを表示</string>
|
||||
|
@ -339,7 +339,6 @@
|
||||
<string name="download.jukebox_not_authorized">Fjernkontroll er avskrudd. Skru på jukebox-modus i <b>Brukere > Innstillinger</b> på din Subsonic-tjener.</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_volume">Fjernkontroll</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_shuffle">Omstokking</string>
|
||||
|
@ -63,7 +63,6 @@
|
||||
<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_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_jukebox_off">Jukebox uitgeschakeld</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox ingeschakeld</string>
|
||||
|
@ -47,7 +47,6 @@
|
||||
<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_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_jukebox_off">Jukebox wyłączony</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox włączony</string>
|
||||
|
@ -62,7 +62,6 @@
|
||||
<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_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_jukebox_off">Jukebox Desligado</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox Ligado</string>
|
||||
|
@ -47,7 +47,6 @@
|
||||
<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_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_jukebox_off">Jukebox Desligado</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox Ligado</string>
|
||||
|
@ -59,7 +59,6 @@
|
||||
<string name="download.jukebox_offline">Пульт дистанционного управления недоступен в автономном режиме.</string>
|
||||
<string name="download.jukebox_on">Включен пульт управления. Музыка играет на сервере.</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_jukebox_off">Jukebox выключен</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox включен</string>
|
||||
|
@ -60,7 +60,6 @@
|
||||
<string name="download.jukebox_offline">离线模式不支持远程控制。</string>
|
||||
<string name="download.jukebox_on">已打开远程控制,音乐将在服务端播放。</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_jukebox_off">关闭点唱机</string>
|
||||
<string name="download.menu_jukebox_on">开启点唱机</string>
|
||||
|
@ -141,7 +141,6 @@
|
||||
<string name="common.pin">固定</string>
|
||||
<string name="chat.send_button">傳送</string>
|
||||
<string name="button_bar.chat">聊天</string>
|
||||
<string name="download.jukebox_volume">遠端音量</string>
|
||||
<string name="chat.user_avatar">頭像</string>
|
||||
<string name="common.delete_selection_confirmation">您真的要刪除目前選取的項目嗎?</string>
|
||||
<string name="background_task.parse_error">無法理解答覆,請檢查伺服器位址。</string>
|
||||
|
@ -63,7 +63,6 @@
|
||||
<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_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_jukebox_off">Jukebox Off</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox On</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user