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