mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-07-22 03:12:13 +03:00
Compare commits
No commits in common. "71168983b64d289e3a5b34c820863067193e86e2" and "238d91c1673707428486d473c3af6b10a755c4e6" have entirely different histories.
71168983b6
...
238d91c167
@ -1,5 +1,27 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<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">
|
<issues format="6" by="lint 7.4.0" type="baseline" client="gradle" dependencies="true" name="AGP (7.4.0)" variant="all" version="7.4.0">
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="MissingPermission"
|
||||||
|
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
|
||||||
|
errorLine1=" manager.notify(NOTIFICATION_ID, notification)"
|
||||||
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt"
|
||||||
|
line="260"
|
||||||
|
column="17"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="MissingPermission"
|
||||||
|
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
|
||||||
|
errorLine1=" notificationManagerCompat.notify(notification.notificationId, notification.notification)"
|
||||||
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt"
|
||||||
|
line="194"
|
||||||
|
column="9"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PluralsCandidate"
|
id="PluralsCandidate"
|
||||||
@ -8,7 +30,7 @@
|
|||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="151"
|
line="152"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
@ -26,6 +48,50 @@
|
|||||||
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"
|
||||||
@ -70,6 +136,17 @@
|
|||||||
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,6 +66,13 @@
|
|||||||
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,7 +122,6 @@ 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 Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
|
private final Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent)
|
public void onReceive(Context context, Intent intent)
|
||||||
|
@ -17,6 +17,7 @@ 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
|
||||||
@ -54,8 +55,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
|
||||||
@ -97,7 +98,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 mediaPlayerManager: MediaPlayerManager by inject()
|
private val mediaPlayerController: MediaPlayerController 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()
|
||||||
|
|
||||||
@ -273,6 +274,18 @@ 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)
|
||||||
|
|
||||||
@ -295,7 +308,7 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
R.id.menu_exit -> {
|
R.id.menu_exit -> {
|
||||||
setResult(Constants.RESULT_CLOSE_ALL)
|
setResult(Constants.RESULT_CLOSE_ALL)
|
||||||
mediaPlayerManager.onDestroy()
|
mediaPlayerController.onDestroy()
|
||||||
finish()
|
finish()
|
||||||
exit()
|
exit()
|
||||||
}
|
}
|
||||||
@ -462,9 +475,9 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nowPlayingView != null) {
|
if (nowPlayingView != null) {
|
||||||
val playerState: Int = mediaPlayerManager.playbackState
|
val playerState: Int = mediaPlayerController.playbackState
|
||||||
if (playerState == STATE_BUFFERING || playerState == STATE_READY) {
|
if (playerState == STATE_BUFFERING || playerState == STATE_READY) {
|
||||||
val item: MediaItem? = mediaPlayerManager.currentMediaItem
|
val item: MediaItem? = mediaPlayerController.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 { MediaPlayerManager(get(), get(), get()) }
|
single { MediaPlayerController(get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ class BookmarksFragment : TrackCollectionFragment() {
|
|||||||
currentPlayingPosition = songs[0].bookmarkPosition
|
currentPlayingPosition = songs[0].bookmarkPosition
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaPlayerManager.restore(
|
mediaPlayerController.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.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
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 mediaPlayerManager: MediaPlayerManager by inject()
|
private val mediaPlayerController: MediaPlayerController 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 (mediaPlayerManager.isPlaying) {
|
if (mediaPlayerController.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 = mediaPlayerManager.currentMediaItem?.toTrack()
|
val file = mediaPlayerController.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 { mediaPlayerManager.togglePlayPause() }
|
playButton!!.setOnClickListener { mediaPlayerController.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) {
|
||||||
mediaPlayerManager.seekToPrevious()
|
mediaPlayerController.seekToPrevious()
|
||||||
}
|
}
|
||||||
if (deltaX > 0) {
|
if (deltaX > 0) {
|
||||||
mediaPlayerManager.seekToNext()
|
mediaPlayerController.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.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
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 mediaPlayerManager: MediaPlayerManager by inject()
|
private val mediaPlayerController: MediaPlayerController 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(mediaPlayerManager.isShufflePlayEnabled)
|
updateShuffleButtonState(mediaPlayerController.isShufflePlayEnabled)
|
||||||
updateRepeatButtonState(mediaPlayerManager.repeatMode)
|
updateRepeatButtonState(mediaPlayerController.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)) {
|
||||||
mediaPlayerManager.seekToPrevious()
|
mediaPlayerController.seekToPrevious()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,7 +297,7 @@ class PlayerFragment :
|
|||||||
nextButton.setOnClickListener {
|
nextButton.setOnClickListener {
|
||||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||||
launch(CommunicationError.getHandler(context)) {
|
launch(CommunicationError.getHandler(context)) {
|
||||||
mediaPlayerManager.seekToNext()
|
mediaPlayerController.seekToNext()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,22 +307,22 @@ class PlayerFragment :
|
|||||||
|
|
||||||
pauseButton.setOnClickListener {
|
pauseButton.setOnClickListener {
|
||||||
launch(CommunicationError.getHandler(context)) {
|
launch(CommunicationError.getHandler(context)) {
|
||||||
mediaPlayerManager.pause()
|
mediaPlayerController.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopButton.setOnClickListener {
|
stopButton.setOnClickListener {
|
||||||
launch(CommunicationError.getHandler(context)) {
|
launch(CommunicationError.getHandler(context)) {
|
||||||
mediaPlayerManager.reset()
|
mediaPlayerController.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playButton.setOnClickListener {
|
playButton.setOnClickListener {
|
||||||
if (!mediaPlayerManager.isJukeboxEnabled)
|
if (!mediaPlayerController.isJukeboxEnabled)
|
||||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||||
|
|
||||||
launch(CommunicationError.getHandler(context)) {
|
launch(CommunicationError.getHandler(context)) {
|
||||||
mediaPlayerManager.play()
|
mediaPlayerController.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -331,12 +331,12 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
repeatButton.setOnClickListener {
|
repeatButton.setOnClickListener {
|
||||||
var newRepeat = mediaPlayerManager.repeatMode + 1
|
var newRepeat = mediaPlayerController.repeatMode + 1
|
||||||
if (newRepeat == 3) {
|
if (newRepeat == 3) {
|
||||||
newRepeat = 0
|
newRepeat = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaPlayerManager.repeatMode = newRepeat
|
mediaPlayerController.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)) {
|
||||||
mediaPlayerManager.seekTo(progressBar.progress)
|
mediaPlayerController.seekTo(progressBar.progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,19 +395,12 @@ 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 = mediaPlayerManager.isJukeboxAvailable
|
jukeboxAvailable = mediaPlayerController.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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,7 +432,7 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleShuffle() {
|
private fun toggleShuffle() {
|
||||||
val isEnabled = mediaPlayerManager.toggleShuffle()
|
val isEnabled = mediaPlayerController.toggleShuffle()
|
||||||
|
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
Util.toast(activity, R.string.download_menu_shuffle_on)
|
Util.toast(activity, R.string.download_menu_shuffle_on)
|
||||||
@ -452,7 +445,7 @@ class PlayerFragment :
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (mediaPlayerManager.currentMediaItem == null) {
|
if (mediaPlayerController.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
|
||||||
@ -465,7 +458,7 @@ class PlayerFragment :
|
|||||||
executorService = Executors.newSingleThreadScheduledExecutor()
|
executorService = Executors.newSingleThreadScheduledExecutor()
|
||||||
executorService.scheduleWithFixedDelay(runnable, 0L, 500L, TimeUnit.MILLISECONDS)
|
executorService.scheduleWithFixedDelay(runnable, 0L, 500L, TimeUnit.MILLISECONDS)
|
||||||
|
|
||||||
if (mediaPlayerManager.keepScreenOn) {
|
if (mediaPlayerController.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)
|
||||||
@ -476,7 +469,7 @@ class PlayerFragment :
|
|||||||
|
|
||||||
// Scroll to current playing.
|
// Scroll to current playing.
|
||||||
private fun scrollToCurrent() {
|
private fun scrollToCurrent() {
|
||||||
val index = mediaPlayerManager.currentMediaItemIndex
|
val index = mediaPlayerController.currentMediaItemIndex
|
||||||
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
val smoothScroller = LinearSmoothScroller(context)
|
val smoothScroller = LinearSmoothScroller(context)
|
||||||
@ -564,7 +557,7 @@ class PlayerFragment :
|
|||||||
equalizerMenuItem.isVisible = isEqualizerAvailable
|
equalizerMenuItem.isVisible = isEqualizerAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
val mediaPlayerController = mediaPlayerManager
|
val mediaPlayerController = mediaPlayerController
|
||||||
val track = mediaPlayerController.currentMediaItem?.toTrack()
|
val track = mediaPlayerController.currentMediaItem?.toTrack()
|
||||||
|
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
@ -673,12 +666,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 (mediaPlayerManager.keepScreenOn) {
|
if (mediaPlayerController.keepScreenOn) {
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
mediaPlayerManager.keepScreenOn = false
|
mediaPlayerController.keepScreenOn = false
|
||||||
} else {
|
} else {
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
mediaPlayerManager.keepScreenOn = true
|
mediaPlayerController.keepScreenOn = true
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -691,8 +684,8 @@ class PlayerFragment :
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_item_jukebox -> {
|
R.id.menu_item_jukebox -> {
|
||||||
val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
|
val jukeboxEnabled = !mediaPlayerController.isJukeboxEnabled
|
||||||
mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
|
mediaPlayerController.isJukeboxEnabled = jukeboxEnabled
|
||||||
Util.toast(
|
Util.toast(
|
||||||
context,
|
context,
|
||||||
if (jukeboxEnabled) R.string.download_jukebox_on
|
if (jukeboxEnabled) R.string.download_jukebox_on
|
||||||
@ -706,13 +699,13 @@ class PlayerFragment :
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_item_clear_playlist -> {
|
R.id.menu_item_clear_playlist -> {
|
||||||
mediaPlayerManager.isShufflePlayEnabled = false
|
mediaPlayerController.isShufflePlayEnabled = false
|
||||||
mediaPlayerManager.clear()
|
mediaPlayerController.clear()
|
||||||
onPlaylistChanged()
|
onPlaylistChanged()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_item_save_playlist -> {
|
R.id.menu_item_save_playlist -> {
|
||||||
if (mediaPlayerManager.playlistSize > 0) {
|
if (mediaPlayerController.playlistSize > 0) {
|
||||||
showSavePlaylistDialog()
|
showSavePlaylistDialog()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -731,7 +724,7 @@ class PlayerFragment :
|
|||||||
if (track == null) return true
|
if (track == null) return true
|
||||||
|
|
||||||
val songId = track.id
|
val songId = track.id
|
||||||
val playerPosition = mediaPlayerManager.playerPosition
|
val playerPosition = mediaPlayerController.playerPosition
|
||||||
track.bookmarkPosition = playerPosition
|
track.bookmarkPosition = playerPosition
|
||||||
val bookmarkTime = Util.formatTotalDuration(playerPosition.toLong(), true)
|
val bookmarkTime = Util.formatTotalDuration(playerPosition.toLong(), true)
|
||||||
Thread {
|
Thread {
|
||||||
@ -766,7 +759,7 @@ class PlayerFragment :
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_item_share -> {
|
R.id.menu_item_share -> {
|
||||||
val mediaPlayerController = mediaPlayerManager
|
val mediaPlayerController = mediaPlayerController
|
||||||
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) {
|
||||||
@ -801,7 +794,8 @@ class PlayerFragment :
|
|||||||
|
|
||||||
private fun update(cancel: CancellationToken? = null) {
|
private fun update(cancel: CancellationToken? = null) {
|
||||||
if (cancel?.isCancellationRequested == true) return
|
if (cancel?.isCancellationRequested == true) return
|
||||||
if (currentSong?.id != mediaPlayerManager.currentMediaItem?.mediaId) {
|
val mediaPlayerController = mediaPlayerController
|
||||||
|
if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) {
|
||||||
onTrackChanged()
|
onTrackChanged()
|
||||||
}
|
}
|
||||||
updateSeekBar()
|
updateSeekBar()
|
||||||
@ -809,10 +803,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))
|
||||||
mediaPlayerManager.suggestedPlaylistName = playlistName
|
mediaPlayerController.suggestedPlaylistName = playlistName
|
||||||
|
|
||||||
// The playlist can be acquired only from the main thread
|
// The playlist can be acquired only from the main thread
|
||||||
val entries = mediaPlayerManager.playlist.map {
|
val entries = mediaPlayerController.playlist.map {
|
||||||
it.toTrack()
|
it.toTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -865,8 +859,8 @@ class PlayerFragment :
|
|||||||
|
|
||||||
// Create listener
|
// Create listener
|
||||||
val clickHandler: ((Track, Int) -> Unit) = { _, listPos ->
|
val clickHandler: ((Track, Int) -> Unit) = { _, listPos ->
|
||||||
val mediaIndex = mediaPlayerManager.getUnshuffledIndexOf(listPos)
|
val mediaIndex = mediaPlayerController.getUnshuffledIndexOf(listPos)
|
||||||
mediaPlayerManager.play(mediaIndex)
|
mediaPlayerController.play(mediaIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewAdapter.register(
|
viewAdapter.register(
|
||||||
@ -930,7 +924,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 = mediaPlayerManager.getMediaItemAt(pos)
|
val item = mediaPlayerController.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()
|
||||||
@ -946,7 +940,7 @@ class PlayerFragment :
|
|||||||
Util.toast(context, songRemoved)
|
Util.toast(context, songRemoved)
|
||||||
|
|
||||||
// Remove the item from the playlist
|
// Remove the item from the playlist
|
||||||
mediaPlayerManager.removeFromPlaylist(pos)
|
mediaPlayerController.removeFromPlaylist(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSelectedChanged(
|
override fun onSelectedChanged(
|
||||||
@ -966,7 +960,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)
|
||||||
mediaPlayerManager.moveItemInPlaylist(startPosition, endPosition)
|
mediaPlayerController.moveItemInPlaylist(startPosition, endPosition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1044,7 +1038,7 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onPlaylistChanged() {
|
private fun onPlaylistChanged() {
|
||||||
val mediaPlayerController = mediaPlayerManager
|
val mediaPlayerController = mediaPlayerController
|
||||||
// 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)
|
||||||
@ -1056,12 +1050,12 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onTrackChanged() {
|
private fun onTrackChanged() {
|
||||||
currentSong = mediaPlayerManager.currentMediaItem?.toTrack()
|
currentSong = mediaPlayerController.currentMediaItem?.toTrack()
|
||||||
|
|
||||||
scrollToCurrent()
|
scrollToCurrent()
|
||||||
val totalDuration = mediaPlayerManager.playListDuration
|
val totalDuration = mediaPlayerController.playListDuration
|
||||||
val totalSongs = mediaPlayerManager.playlistSize
|
val totalSongs = mediaPlayerController.playlistSize
|
||||||
val currentSongIndex = mediaPlayerManager.currentMediaItemIndex + 1
|
val currentSongIndex = mediaPlayerController.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)
|
||||||
@ -1116,27 +1110,23 @@ class PlayerFragment :
|
|||||||
|
|
||||||
updateSongRating()
|
updateSongRating()
|
||||||
|
|
||||||
updateMediaButtonActivationState()
|
nextButton.isEnabled = mediaPlayerController.canSeekToNext()
|
||||||
}
|
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 = mediaPlayerManager.isJukeboxEnabled
|
val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled
|
||||||
val millisPlayed: Int = max(0, mediaPlayerManager.playerPosition)
|
val millisPlayed: Int = max(0, mediaPlayerController.playerPosition)
|
||||||
val duration: Int = mediaPlayerManager.playerDuration
|
val duration: Int = mediaPlayerController.playerDuration
|
||||||
val playbackState: Int = mediaPlayerManager.playbackState
|
val playbackState: Int = mediaPlayerController.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 = mediaPlayerManager.isPlaying || isJukeboxEnabled
|
progressBar.isEnabled = mediaPlayerController.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)
|
||||||
@ -1145,7 +1135,7 @@ class PlayerFragment :
|
|||||||
progressBar.isEnabled = false
|
progressBar.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
val progress = mediaPlayerManager.bufferedPercentage
|
val progress = mediaPlayerController.bufferedPercentage
|
||||||
updateBufferProgress(playbackState, progress)
|
updateBufferProgress(playbackState, progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1158,7 +1148,7 @@ class PlayerFragment :
|
|||||||
setTitle(this@PlayerFragment, downloadStatus)
|
setTitle(this@PlayerFragment, downloadStatus)
|
||||||
}
|
}
|
||||||
Player.STATE_READY -> {
|
Player.STATE_READY -> {
|
||||||
if (mediaPlayerManager.isShufflePlayEnabled) {
|
if (mediaPlayerController.isShufflePlayEnabled) {
|
||||||
setTitle(
|
setTitle(
|
||||||
this@PlayerFragment,
|
this@PlayerFragment,
|
||||||
R.string.download_playerstate_playing_shuffle
|
R.string.download_playerstate_playing_shuffle
|
||||||
@ -1182,7 +1172,7 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateButtonStates(playbackState: Int) {
|
private fun updateButtonStates(playbackState: Int) {
|
||||||
val isPlaying = mediaPlayerManager.isPlaying
|
val isPlaying = mediaPlayerController.isPlaying
|
||||||
when (playbackState) {
|
when (playbackState) {
|
||||||
Player.STATE_READY -> {
|
Player.STATE_READY -> {
|
||||||
pauseButton.isVisible = isPlaying
|
pauseButton.isVisible = isPlaying
|
||||||
@ -1205,9 +1195,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) {
|
||||||
mediaPlayerManager.seekForward()
|
mediaPlayerController.seekForward()
|
||||||
} else {
|
} else {
|
||||||
mediaPlayerManager.seekBack()
|
mediaPlayerController.seekBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1233,28 +1223,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()
|
||||||
mediaPlayerManager.seekToNext()
|
mediaPlayerController.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()
|
||||||
mediaPlayerManager.seekToPrevious()
|
mediaPlayerController.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()
|
||||||
mediaPlayerManager.seekTo(mediaPlayerManager.playerPosition + 30000)
|
mediaPlayerController.seekTo(mediaPlayerController.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()
|
||||||
mediaPlayerManager.seekTo(mediaPlayerManager.playerPosition - 8000)
|
mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -1319,7 +1309,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 = mediaPlayerManager.suggestedPlaylistName
|
val playlistName = mediaPlayerController.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.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
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 mediaPlayerManager: MediaPlayerManager by inject()
|
private val mediaPlayerController: MediaPlayerController 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) {
|
||||||
mediaPlayerManager.clear()
|
mediaPlayerController.clear()
|
||||||
}
|
}
|
||||||
mediaPlayerManager.addToPlaylist(
|
mediaPlayerController.addToPlaylist(
|
||||||
listOf(song),
|
listOf(song),
|
||||||
autoPlay = false,
|
autoPlay = false,
|
||||||
shuffle = false,
|
shuffle = false,
|
||||||
insertionMode = MediaPlayerManager.InsertionMode.APPEND
|
insertionMode = MediaPlayerController.InsertionMode.APPEND
|
||||||
)
|
)
|
||||||
mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1)
|
mediaPlayerController.play(mediaPlayerController.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.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
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 mediaPlayerManager: MediaPlayerManager by inject()
|
private val mediaPlayerController: MediaPlayerController 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.
|
||||||
mediaPlayerManager.clear()
|
mediaPlayerController.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.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
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 mediaPlayerManager: MediaPlayerManager by inject()
|
internal val mediaPlayerController: MediaPlayerController 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.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.FlowPreview
|
||||||
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(ExperimentalCoroutinesApi::class)
|
@OptIn(FlowPreview::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,6 +19,7 @@ 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
|
||||||
@ -46,7 +47,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.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
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
|
||||||
@ -100,10 +101,10 @@ const val PLAY_COMMAND = "play "
|
|||||||
*/
|
*/
|
||||||
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
|
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibraryService) :
|
||||||
MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
|
MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
|
||||||
|
|
||||||
private val mediaPlayerManager by inject<MediaPlayerManager>()
|
private val mediaPlayerController by inject<MediaPlayerController>()
|
||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
|
|
||||||
private val serviceJob = SupervisorJob()
|
private val serviceJob = SupervisorJob()
|
||||||
@ -240,7 +241,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
* 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 = mediaPlayerManager.currentMediaItem?.toTrack()
|
val track = mediaPlayerController.currentMediaItem?.toTrack()
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
customCommandFuture = onSetRating(
|
customCommandFuture = onSetRating(
|
||||||
session,
|
session,
|
||||||
@ -253,13 +254,12 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
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(
|
||||||
mediaPlayerManager.context,
|
mediaPlayerController.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.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
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 mediaPlayerManager by inject<MediaPlayerManager>()
|
private val mediaPlayerController by inject<MediaPlayerController>()
|
||||||
|
|
||||||
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 = mediaPlayerManager.currentMediaItem?.toTrack()?.starred?.let {
|
val rating = mediaPlayerController.currentMediaItem?.toTrack()?.starred?.let {
|
||||||
HeartRating(
|
HeartRating(
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
|
@ -26,7 +26,8 @@ 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
|
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
|
||||||
|
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
|
||||||
@ -36,7 +37,6 @@ 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,8 +46,6 @@ 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
|
||||||
@ -63,10 +61,9 @@ class PlaybackService :
|
|||||||
MediaLibraryService(),
|
MediaLibraryService(),
|
||||||
KoinComponent,
|
KoinComponent,
|
||||||
CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||||
private lateinit var player: Player
|
private lateinit var player: ExoPlayer
|
||||||
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
|
||||||
|
|
||||||
@ -79,7 +76,6 @@ class PlaybackService :
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
initializeSessionAndPlayer()
|
initializeSessionAndPlayer()
|
||||||
setListener(MediaSessionServiceListener())
|
setListener(MediaSessionServiceListener())
|
||||||
instance = this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getWakeModeFlag(): Int {
|
private fun getWakeModeFlag(): Int {
|
||||||
@ -103,7 +99,6 @@ 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)
|
||||||
|
|
||||||
@ -132,91 +127,6 @@ 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()
|
||||||
@ -237,7 +147,7 @@ class PlaybackService :
|
|||||||
renderer.setEnableAudioOffload(true)
|
renderer.setEnableAudioOffload(true)
|
||||||
|
|
||||||
// Create the player
|
// Create the player
|
||||||
val player = ExoPlayer.Builder(this)
|
player = ExoPlayer.Builder(this)
|
||||||
.setAudioAttributes(getAudioAttributes(), true)
|
.setAudioAttributes(getAudioAttributes(), true)
|
||||||
.setWakeMode(getWakeModeFlag())
|
.setWakeMode(getWakeModeFlag())
|
||||||
.setHandleAudioBecomingNoisy(true)
|
.setHandleAudioBecomingNoisy(true)
|
||||||
@ -247,17 +157,59 @@ 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)
|
||||||
|
|
||||||
return player
|
// Create browser interface
|
||||||
|
librarySessionCallback = AutoMediaBrowserCallback(player, this)
|
||||||
|
|
||||||
|
// This will need to use the AutoCalls
|
||||||
|
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||||
|
.setSessionActivity(getPendingIntentForContent())
|
||||||
|
.setBitmapLoader(ArtworkBitmapLoader())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Set a listener to update the API client when the active server has changed
|
||||||
|
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
|
||||||
|
// Set the player wake mode
|
||||||
|
player.setWakeMode(getWakeModeFlag())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a listener to reset the ShuffleOrder
|
||||||
|
rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle ->
|
||||||
|
val len = player.currentTimeline.windowCount
|
||||||
|
|
||||||
|
Timber.i("Resetting shuffle order, isShuffled: %s", shuffle)
|
||||||
|
|
||||||
|
// If disabling Shuffle return early
|
||||||
|
if (!shuffle) {
|
||||||
|
return@subscribe player.setShuffleOrder(UnshuffledShuffleOrder(len))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the position of the current track in the unshuffled order
|
||||||
|
val cur = player.currentMediaItemIndex
|
||||||
|
val seed = System.currentTimeMillis()
|
||||||
|
val random = Random(seed)
|
||||||
|
|
||||||
|
val list = createShuffleListFromCurrentIndex(cur, len, random)
|
||||||
|
Timber.i("New Shuffle order: %s", list.joinToString { it.toString() })
|
||||||
|
player.setShuffleOrder(DefaultShuffleOrder(list, seed))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to the shutdown command
|
||||||
|
rxBusSubscription += RxBus.shutdownCommandObservable.subscribe {
|
||||||
|
Timber.i("Received destroy command via Rx")
|
||||||
|
onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
player.addListener(listener)
|
||||||
|
isStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createShuffleListFromCurrentIndex(
|
fun createShuffleListFromCurrentIndex(
|
||||||
currentIndex: Int,
|
currentIndex: Int,
|
||||||
length: Int,
|
length: Int,
|
||||||
random: Random
|
random: Random
|
||||||
@ -292,7 +244,6 @@ 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,
|
||||||
@ -382,16 +333,6 @@ 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,12 +7,29 @@
|
|||||||
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
|
||||||
@ -22,27 +39,34 @@ 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.Settings
|
import org.moire.ultrasonic.util.Util.getPendingIntentToShowPlayer
|
||||||
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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,64 +86,135 @@ 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 = (MAX_GAIN / 3)
|
private var gain = 0.5f
|
||||||
private val floatGain: Float
|
private var volumeToast: VolumeToast? = null
|
||||||
get() = gain.toFloat() / MAX_GAIN
|
|
||||||
|
|
||||||
private var serviceThread: Thread? = null
|
private var serviceThread: Thread? = null
|
||||||
|
|
||||||
private var listeners: ListenerSet<Player.Listener>
|
private var listeners: MutableList<Player.Listener> = mutableListOf()
|
||||||
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
|
||||||
|
|
||||||
private var _currentIndex: Int = 0
|
@Suppress("MagicNumber")
|
||||||
private var currentIndex: Int
|
override fun onCreate() {
|
||||||
get() = _currentIndex
|
super.onCreate()
|
||||||
set(value) {
|
if (running.get()) return
|
||||||
// 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()
|
||||||
startProcessTasks()
|
|
||||||
}
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
|
|
||||||
override fun release() {
|
startFuture?.set(this)
|
||||||
|
|
||||||
|
startProcessTasks()
|
||||||
|
|
||||||
|
notificationManagerCompat = NotificationManagerCompat.from(this)
|
||||||
|
mediaSession = MediaSession.Builder(applicationContext(), this)
|
||||||
|
.setId("jukebox")
|
||||||
|
.setSessionActivity(getPendingIntentToShowPlayer(this))
|
||||||
|
.build()
|
||||||
|
val notification = notificationProvider.createNotification(
|
||||||
|
mediaSession,
|
||||||
|
ImmutableList.of(),
|
||||||
|
JukeboxNotificationActionFactory()
|
||||||
|
) {}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
|
startForeground(
|
||||||
|
notification.notificationId,
|
||||||
|
notification.notification,
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
startForeground(
|
||||||
|
notification.notificationId, notification.notification
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Started Jukebox Service")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
if (Intent.ACTION_MEDIA_BUTTON != intent?.action) return START_STICKY
|
||||||
|
|
||||||
|
val extras = intent.extras
|
||||||
|
if ((extras != null) && extras.containsKey(Intent.EXTRA_KEY_EVENT)) {
|
||||||
|
val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
extras.getParcelable(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
extras.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)
|
||||||
|
}
|
||||||
|
when (event?.keyCode) {
|
||||||
|
KEYCODE_MEDIA_PLAY -> play()
|
||||||
|
KEYCODE_MEDIA_PAUSE -> stop()
|
||||||
|
KEYCODE_MEDIA_STOP -> stop()
|
||||||
|
KEYCODE_MEDIA_PLAY_PAUSE -> if (isPlaying) stop() else play()
|
||||||
|
KEYCODE_MEDIA_PREVIOUS -> seekToPrevious()
|
||||||
|
KEYCODE_MEDIA_NEXT -> seekToNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -168,20 +263,14 @@ 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(
|
seekTo(0L.coerceAtMost((jukeboxStatus?.positionSeconds ?: 0) - SEEK_INCREMENT_SECONDS))
|
||||||
0L.coerceAtMost(
|
|
||||||
(jukeboxStatus?.positionSeconds ?: 0) -
|
|
||||||
Settings.seekIntervalMillis
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekForward() {
|
override fun seekForward() {
|
||||||
seekTo((jukeboxStatus?.positionSeconds ?: 0) + Settings.seekIntervalMillis)
|
seekTo((jukeboxStatus?.positionSeconds ?: 0) + SEEK_INCREMENT_SECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isCurrentMediaItemSeekable() = true
|
override fun isCurrentMediaItemSeekable() = true
|
||||||
@ -203,11 +292,8 @@ 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_CHANGE_MEDIA_ITEMS,
|
Player.COMMAND_SET_VOLUME,
|
||||||
Player.COMMAND_GET_TIMELINE,
|
Player.COMMAND_GET_VOLUME
|
||||||
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()) {
|
||||||
@ -220,7 +306,8 @@ 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
|
||||||
)
|
)
|
||||||
@ -236,18 +323,6 @@ 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
|
||||||
}
|
}
|
||||||
@ -283,43 +358,21 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
|
|
||||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
|
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
|
||||||
|
|
||||||
override fun setDeviceVolume(volume: Int) {
|
override fun setVolume(volume: Float) {
|
||||||
gain = volume
|
gain = volume
|
||||||
tasks.remove(SetGain::class.java)
|
tasks.remove(SetGain::class.java)
|
||||||
tasks.add(SetGain(floatGain))
|
tasks.add(SetGain(volume))
|
||||||
|
val context = applicationContext()
|
||||||
// We must trigger an event so that the Controller knows the new volume
|
if (volumeToast == null) volumeToast = VolumeToast(context)
|
||||||
Handler(Looper.getMainLooper()).post {
|
volumeToast!!.setVolume(volume)
|
||||||
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 floatGain
|
return gain
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDeviceVolume(): Int {
|
override fun getDeviceVolume(): Int {
|
||||||
return gain
|
return (gain * 100).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) {
|
override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) {
|
||||||
@ -391,7 +444,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun seekToPrevious() {
|
override fun seekToPrevious() {
|
||||||
if ((jukeboxStatus?.positionSeconds ?: 0) > (Settings.seekIntervalMillis)) {
|
if ((jukeboxStatus?.positionSeconds ?: 0) > SEEK_START_AFTER_SECONDS) {
|
||||||
seekTo(currentIndex, 0)
|
seekTo(currentIndex, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -446,63 +499,51 @@ 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 (running.get()) {
|
while (true) {
|
||||||
// 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() ?: continue
|
task = tasks.poll()
|
||||||
|
// 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 (all: Throwable) {
|
} catch (x: Throwable) {
|
||||||
onError(task, all)
|
onError(task, x)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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.queueEvent(Player.EVENT_PLAYBACK_STATE_CHANGED) {
|
listeners.forEach {
|
||||||
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.queueEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) {
|
listeners.forEach {
|
||||||
it.onMediaItemTransition(
|
it.onMediaItemTransition(
|
||||||
currentMedia,
|
currentMedia,
|
||||||
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
|
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
|
||||||
@ -511,39 +552,44 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldUpdateCommands) updateAvailableCommands()
|
updateNotification()
|
||||||
|
|
||||||
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) {
|
||||||
exception = PlaybackException(
|
|
||||||
"Jukebox server too old",
|
|
||||||
null,
|
|
||||||
R.string.download_jukebox_server_too_old
|
|
||||||
)
|
|
||||||
} else if (x is OfflineException && task !is Stop) {
|
|
||||||
exception = PlaybackException(
|
|
||||||
"Jukebox offline",
|
|
||||||
null,
|
|
||||||
R.string.download_jukebox_offline
|
|
||||||
)
|
|
||||||
} else if (x is SubsonicRESTException && x.code == 50 && task !is Stop) {
|
|
||||||
exception = PlaybackException(
|
|
||||||
"Jukebox not authorized",
|
|
||||||
null,
|
|
||||||
R.string.download_jukebox_not_authorized
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exception != null) {
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
listeners.sendEvent(Player.EVENT_PLAYER_ERROR) {
|
listeners.forEach {
|
||||||
it.onPlayerError(exception)
|
it.onPlayerError(
|
||||||
|
PlaybackException(
|
||||||
|
"Jukebox server too old",
|
||||||
|
null,
|
||||||
|
R.string.download_jukebox_server_too_old
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (x is OfflineException && task !is Stop) {
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
listeners.forEach {
|
||||||
|
it.onPlayerError(
|
||||||
|
PlaybackException(
|
||||||
|
"Jukebox offline",
|
||||||
|
null,
|
||||||
|
R.string.download_jukebox_offline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (x is SubsonicRESTException && x.code == 50 && task !is Stop) {
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
listeners.forEach {
|
||||||
|
it.onPlayerError(
|
||||||
|
PlaybackException(
|
||||||
|
"Jukebox not authorized",
|
||||||
|
null,
|
||||||
|
R.string.download_jukebox_not_authorized
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -562,10 +608,8 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
}
|
}
|
||||||
tasks.add(SetPlaylist(ids))
|
tasks.add(SetPlaylist(ids))
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
listeners.sendEvent(
|
listeners.forEach {
|
||||||
Player.EVENT_TIMELINE_CHANGED
|
it.onTimelineChanged(
|
||||||
) { listener: Player.Listener ->
|
|
||||||
listener.onTimelineChanged(
|
|
||||||
PlaylistTimeline(playlist),
|
PlaylistTimeline(playlist),
|
||||||
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED
|
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED
|
||||||
)
|
)
|
||||||
@ -675,6 +719,25 @@ 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
|
||||||
@ -685,15 +748,15 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getMaxSeekToPreviousPosition(): Long {
|
override fun getMaxSeekToPreviousPosition(): Long {
|
||||||
return Settings.seekInterval.toLong()
|
return SEEK_START_AFTER_SECONDS * 1000L
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSeekBackIncrement(): Long {
|
override fun getSeekBackIncrement(): Long {
|
||||||
return Settings.seekInterval.toLong()
|
return SEEK_INCREMENT_SECONDS * 1000L
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSeekForwardIncrement(): Long {
|
override fun getSeekForwardIncrement(): Long {
|
||||||
return Settings.seekInterval.toLong()
|
return SEEK_INCREMENT_SECONDS * 1000L
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isLoading(): Boolean {
|
override fun isLoading(): Boolean {
|
||||||
@ -716,8 +779,6 @@ 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)
|
||||||
}
|
}
|
||||||
@ -763,7 +824,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getDeviceInfo(): DeviceInfo {
|
override fun getDeviceInfo(): DeviceInfo {
|
||||||
return DEVICE_INFO
|
return DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPlayerError(): PlaybackException? {
|
override fun getPlayerError(): PlaybackException? {
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* 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,6 +8,7 @@
|
|||||||
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
|
||||||
@ -25,7 +26,7 @@ import androidx.media3.common.Tracks
|
|||||||
*/
|
*/
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
abstract class JukeboxUnimplementedFunctions : Player {
|
abstract class JukeboxUnimplementedFunctions : Service(), Player {
|
||||||
|
|
||||||
override fun setMediaItems(mediaItems: MutableList<MediaItem>) {
|
override fun setMediaItems(mediaItems: MutableList<MediaItem>) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
@ -139,6 +140,10 @@ abstract class JukeboxUnimplementedFunctions : 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
|
||||||
@ -223,4 +228,20 @@ abstract class JukeboxUnimplementedFunctions : 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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ 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
|
||||||
@ -48,13 +50,12 @@ private const val CONTROLLER_SWITCH_DELAY = 500L
|
|||||||
private const val VOLUME_DELTA = 0.05f
|
private const val VOLUME_DELTA = 0.05f
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Media Player Manager can forward commands to the Media3 controller as
|
* The implementation of the Media Player Controller.
|
||||||
* 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 MediaPlayerManager(
|
class MediaPlayerController(
|
||||||
private val playbackStateSerializer: PlaybackStateSerializer,
|
private val playbackStateSerializer: PlaybackStateSerializer,
|
||||||
private val externalStorageMonitor: ExternalStorageMonitor,
|
private val externalStorageMonitor: ExternalStorageMonitor,
|
||||||
val context: Context
|
val context: Context
|
||||||
@ -96,15 +97,15 @@ class MediaPlayerManager(
|
|||||||
* 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 = timeline.getFirstWindowIndex(isShufflePlayEnabled)
|
val start = controller?.currentTimeline?.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)
|
RxBus.playlistPublisher.onNext(playlist.map(MediaItem::toTrack))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
@ -178,8 +179,11 @@ class MediaPlayerManager(
|
|||||||
fun onCreate(onCreated: () -> Unit) {
|
fun onCreate(onCreated: () -> Unit) {
|
||||||
if (created) return
|
if (created) return
|
||||||
externalStorageMonitor.onCreate { reset() }
|
externalStorageMonitor.onCreate { reset() }
|
||||||
|
if (activeServerProvider.getActiveServer().jukeboxByDefault) {
|
||||||
createMediaController(onCreated)
|
switchToJukebox(onCreated)
|
||||||
|
} else {
|
||||||
|
switchToLocalPlayer(onCreated)
|
||||||
|
}
|
||||||
|
|
||||||
rxBusSubscription += RxBus.activeServerChangingObservable.subscribe { oldServer ->
|
rxBusSubscription += RxBus.activeServerChangingObservable.subscribe { oldServer ->
|
||||||
if (oldServer != OFFLINE_DB_ID) {
|
if (oldServer != OFFLINE_DB_ID) {
|
||||||
@ -191,7 +195,8 @@ class MediaPlayerManager(
|
|||||||
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.
|
||||||
switchToLocalPlayer()
|
releaseJukebox(controller)
|
||||||
|
controller = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,22 +246,6 @@ class MediaPlayerManager(
|
|||||||
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
|
||||||
|
|
||||||
@ -273,10 +262,6 @@ class MediaPlayerManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
@ -351,6 +336,7 @@ class MediaPlayerManager(
|
|||||||
@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()
|
||||||
}
|
}
|
||||||
@ -552,7 +538,7 @@ class MediaPlayerManager(
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun canSeekToPrevious(): Boolean {
|
fun canSeekToPrevious(): Boolean {
|
||||||
return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS) == true
|
return controller?.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS) == true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@ -562,7 +548,7 @@ class MediaPlayerManager(
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun canSeekToNext(): Boolean {
|
fun canSeekToNext(): Boolean {
|
||||||
return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT) == true
|
return controller?.isCommandAvailable(COMMAND_SEEK_TO_NEXT) == true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@ -594,49 +580,102 @@ class MediaPlayerManager(
|
|||||||
|
|
||||||
@set:Synchronized
|
@set:Synchronized
|
||||||
var isJukeboxEnabled: Boolean
|
var isJukeboxEnabled: Boolean
|
||||||
get() = PlaybackService.actualBackend == PlayerBackend.JUKEBOX
|
get() = controller is JukeboxMediaPlayer
|
||||||
set(shouldEnable) {
|
set(jukeboxEnabled) {
|
||||||
if (shouldEnable) {
|
if (jukeboxEnabled) {
|
||||||
switchToJukebox()
|
switchToJukebox {}
|
||||||
} else {
|
} else {
|
||||||
switchToLocalPlayer()
|
switchToLocalPlayer {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToJukebox() {
|
private fun switchToJukebox(onCreated: () -> Unit) {
|
||||||
if (isJukeboxEnabled) return
|
if (controller is JukeboxMediaPlayer) return
|
||||||
scheduleSwitchTo(PlayerBackend.JUKEBOX)
|
val currentPlaylist = playlist
|
||||||
|
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() {
|
private fun switchToLocalPlayer(onCreated: () -> Unit) {
|
||||||
if (!isJukeboxEnabled) return
|
if (controller is MediaController) return
|
||||||
scheduleSwitchTo(PlayerBackend.LOCAL)
|
val currentPlaylist = playlist
|
||||||
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({
|
||||||
// Change the backend
|
if (oldController != null) releaseJukebox(oldController)
|
||||||
PlaybackService.setBackend(newBackend)
|
setupLocalPlayer {
|
||||||
// Restore the media items
|
controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition)
|
||||||
controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition)
|
onCreated()
|
||||||
|
}
|
||||||
}, CONTROLLER_SWITCH_DELAY)
|
}, CONTROLLER_SWITCH_DELAY)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseController() {
|
private fun releaseController() {
|
||||||
controller?.removeListener(listeners)
|
when (controller) {
|
||||||
controller?.release()
|
null -> return
|
||||||
|
is JukeboxMediaPlayer -> releaseJukebox(controller)
|
||||||
|
is MediaController -> releaseLocalPlayer(controller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupLocalPlayer(onCreated: () -> Unit) {
|
||||||
|
mediaControllerFuture = MediaController.Builder(
|
||||||
|
context,
|
||||||
|
sessionToken
|
||||||
|
).buildAsync()
|
||||||
|
|
||||||
|
mediaControllerFuture?.addListener({
|
||||||
|
controller = mediaControllerFuture?.get()
|
||||||
|
|
||||||
|
Timber.i("MediaController Instance received")
|
||||||
|
controller?.addListener(listeners)
|
||||||
|
onCreated()
|
||||||
|
Timber.i("MediaPlayerController creation complete")
|
||||||
|
}, MoreExecutors.directExecutor())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun releaseLocalPlayer(player: Player?) {
|
||||||
|
player?.removeListener(listeners)
|
||||||
|
player?.release()
|
||||||
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
|
||||||
@ -661,6 +700,10 @@ class MediaPlayerManager(
|
|||||||
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
|
||||||
*/
|
*/
|
||||||
@ -798,6 +841,4 @@ class MediaPlayerManager(
|
|||||||
enum class InsertionMode {
|
enum class InsertionMode {
|
||||||
CLEAR, APPEND, AFTER_CURRENT
|
CLEAR, APPEND, AFTER_CURRENT
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class PlayerBackend { JUKEBOX, LOCAL }
|
|
||||||
}
|
}
|
@ -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 mediaPlayerManager by inject<MediaPlayerManager>()
|
private val mediaPlayerController by inject<MediaPlayerController>()
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaPlayerManager.onCreate {
|
mediaPlayerController.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)
|
||||||
|
|
||||||
mediaPlayerManager.restore(
|
mediaPlayerController.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.isNullOrEmpty()) return
|
if (intentAction == null || intentAction.isEmpty()) 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 (!mediaPlayerManager.isJukeboxEnabled) {
|
if (!mediaPlayerController.isJukeboxEnabled) {
|
||||||
mediaPlayerManager.pause()
|
mediaPlayerController.pause()
|
||||||
}
|
}
|
||||||
} else if (state == 1) {
|
} else if (state == 1) {
|
||||||
if (!mediaPlayerManager.isJukeboxEnabled &&
|
if (!mediaPlayerController.isJukeboxEnabled &&
|
||||||
Settings.resumePlayOnHeadphonePlug && !mediaPlayerManager.isPlaying
|
Settings.resumePlayOnHeadphonePlug && !mediaPlayerController.isPlaying
|
||||||
) {
|
) {
|
||||||
mediaPlayerManager.prepare()
|
mediaPlayerController.prepare()
|
||||||
mediaPlayerManager.play()
|
mediaPlayerController.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 -> mediaPlayerManager.togglePlayPause()
|
KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause()
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerManager.seekToPrevious()
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.seekToPrevious()
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerManager.seekToNext()
|
KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.seekToNext()
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerManager.stop()
|
KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop()
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerManager.play()
|
KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play()
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerManager.pause()
|
KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause()
|
||||||
KeyEvent.KEYCODE_1 -> mediaPlayerManager.legacySetRating(1)
|
KeyEvent.KEYCODE_1 -> mediaPlayerController.legacySetRating(1)
|
||||||
KeyEvent.KEYCODE_2 -> mediaPlayerManager.legacySetRating(2)
|
KeyEvent.KEYCODE_2 -> mediaPlayerController.legacySetRating(2)
|
||||||
KeyEvent.KEYCODE_3 -> mediaPlayerManager.legacySetRating(3)
|
KeyEvent.KEYCODE_3 -> mediaPlayerController.legacySetRating(3)
|
||||||
KeyEvent.KEYCODE_4 -> mediaPlayerManager.legacySetRating(4)
|
KeyEvent.KEYCODE_4 -> mediaPlayerController.legacySetRating(4)
|
||||||
KeyEvent.KEYCODE_5 -> mediaPlayerManager.legacySetRating(5)
|
KeyEvent.KEYCODE_5 -> mediaPlayerController.legacySetRating(5)
|
||||||
KeyEvent.KEYCODE_STAR -> mediaPlayerManager.legacyToggleStar()
|
KeyEvent.KEYCODE_STAR -> mediaPlayerController.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 -> mediaPlayerManager.play()
|
Constants.CMD_PLAY -> mediaPlayerController.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) mediaPlayerManager.resumeOrPlay()
|
if (isRunning) mediaPlayerController.resumeOrPlay()
|
||||||
|
|
||||||
Constants.CMD_NEXT -> mediaPlayerManager.seekToNext()
|
Constants.CMD_NEXT -> mediaPlayerController.seekToNext()
|
||||||
Constants.CMD_PREVIOUS -> mediaPlayerManager.seekToPrevious()
|
Constants.CMD_PREVIOUS -> mediaPlayerController.seekToPrevious()
|
||||||
Constants.CMD_TOGGLEPAUSE -> mediaPlayerManager.togglePlayPause()
|
Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause()
|
||||||
Constants.CMD_STOP -> mediaPlayerManager.stop()
|
Constants.CMD_STOP -> mediaPlayerController.stop()
|
||||||
Constants.CMD_PAUSE -> mediaPlayerManager.pause()
|
Constants.CMD_PAUSE -> mediaPlayerController.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
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 mediaPlayerManager: MediaPlayerManager,
|
val mediaPlayerController: MediaPlayerController,
|
||||||
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 -> MediaPlayerManager.InsertionMode.APPEND
|
append -> MediaPlayerController.InsertionMode.APPEND
|
||||||
playNext -> MediaPlayerManager.InsertionMode.AFTER_CURRENT
|
playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT
|
||||||
else -> MediaPlayerManager.InsertionMode.CLEAR
|
else -> MediaPlayerController.InsertionMode.CLEAR
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playlistName != null) {
|
if (playlistName != null) {
|
||||||
mediaPlayerManager.suggestedPlaylistName = playlistName
|
mediaPlayerController.suggestedPlaylistName = playlistName
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaPlayerManager.addToPlaylist(
|
mediaPlayerController.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.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
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<MediaPlayerManager>(
|
val mediaController = inject<MediaPlayerController>(
|
||||||
MediaPlayerManager::class.java
|
MediaPlayerController::class.java
|
||||||
)
|
)
|
||||||
|
|
||||||
val playlist = mainScope.future { mediaController.value.playlist }.get()
|
val playlist = mainScope.future { mediaController.value.playlist }.get()
|
||||||
|
@ -128,9 +128,6 @@ 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)
|
||||||
|
28
ultrasonic/src/main/res/layout/jukebox_volume.xml
Normal file
28
ultrasonic/src/main/res/layout/jukebox_volume.xml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?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,6 +48,7 @@
|
|||||||
<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,6 +61,7 @@
|
|||||||
<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,6 +62,7 @@
|
|||||||
<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,6 +61,7 @@
|
|||||||
<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,6 +54,7 @@
|
|||||||
<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,6 +45,7 @@
|
|||||||
<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,6 +42,7 @@
|
|||||||
<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,6 +339,7 @@
|
|||||||
<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,6 +63,7 @@
|
|||||||
<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,6 +47,7 @@
|
|||||||
<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,6 +62,7 @@
|
|||||||
<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,6 +47,7 @@
|
|||||||
<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,6 +59,7 @@
|
|||||||
<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,6 +60,7 @@
|
|||||||
<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,6 +141,7 @@
|
|||||||
<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,6 +63,7 @@
|
|||||||
<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