Compare commits

..

15 Commits

Author SHA1 Message Date
tzugen
b7889b6324
Clean Timber 2023-05-19 23:30:39 +02:00
tzugen
7c325f3ffb
Cleanup 2023-05-19 22:30:22 +02:00
tzugen
b48e82171a
Working :) 2023-05-19 22:21:43 +02:00
tzugen
e64b4ab486
Small 2023-05-19 21:57:41 +02:00
tzugen
16ea8e6e24
Kind of ok 2023-05-19 21:06:47 +02:00
tzugen
33933c788b
Try volume again 2023-05-19 20:47:41 +02:00
tzugen
2993c63a16
Clean 2023-05-19 20:42:59 +02:00
tzugen
7a39ad3a6d
Clean 2023-05-19 19:18:59 +02:00
tzugen
5e09364d9f
Format 2023-05-19 19:16:10 +02:00
tzugen
a3d9e35199
Nicer volume setter 2023-05-19 18:38:12 +02:00
tzugen
ce3ad45364
Finish 2023-05-19 18:01:31 +02:00
tzugen
67ad135590
Finish 2023-05-19 15:59:44 +02:00
tzugen
8650023013
Finish 2023-05-19 15:58:56 +02:00
tzugen
554172a3f3
Use native EventList 2023-05-19 15:06:52 +02:00
tzugen
16e40518ef
Formatting 2023-05-19 13:09:22 +02:00
31 changed files with 191 additions and 424 deletions

View File

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

View File

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

View File

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

View File

@ -17,7 +17,6 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.SearchRecentSuggestions import android.provider.SearchRecentSuggestions
import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -55,8 +54,8 @@ import org.moire.ultrasonic.data.ServerSettingDao
import org.moire.ultrasonic.fragment.OnBackPressedHandler import org.moire.ultrasonic.fragment.OnBackPressedHandler
import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.provider.SearchSuggestionProvider import org.moire.ultrasonic.provider.SearchSuggestionProvider
import org.moire.ultrasonic.service.MediaPlayerManager
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
@ -274,18 +273,6 @@ class NavigationActivity : AppCompatActivity() {
} }
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
val isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP
val isVolumeAdjust = isVolumeDown || isVolumeUp
val isJukebox = mediaPlayerManager.isJukeboxEnabled
if (isVolumeAdjust && isJukebox) {
mediaPlayerManager.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)

View File

@ -2,8 +2,8 @@ package org.moire.ultrasonic.di
import org.koin.dsl.module import org.koin.dsl.module
import org.moire.ultrasonic.service.ExternalStorageMonitor import org.moire.ultrasonic.service.ExternalStorageMonitor
import org.moire.ultrasonic.service.MediaPlayerManager
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
/** /**

View File

@ -401,6 +401,13 @@ class PlayerFragment :
} }
} }
// 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) }
} }
@ -794,8 +801,7 @@ class PlayerFragment :
private fun update(cancel: CancellationToken? = null) { private fun update(cancel: CancellationToken? = null) {
if (cancel?.isCancellationRequested == true) return if (cancel?.isCancellationRequested == true) return
val mediaPlayerController = mediaPlayerManager if (currentSong?.id != mediaPlayerManager.currentMediaItem?.mediaId) {
if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) {
onTrackChanged() onTrackChanged()
} }
updateSeekBar() updateSeekBar()
@ -1110,6 +1116,10 @@ class PlayerFragment :
updateSongRating() updateSongRating()
updateMediaButtonActivationState()
}
private fun updateMediaButtonActivationState() {
nextButton.isEnabled = mediaPlayerManager.canSeekToNext() nextButton.isEnabled = mediaPlayerManager.canSeekToNext()
previousButton.isEnabled = mediaPlayerManager.canSeekToPrevious() previousButton.isEnabled = mediaPlayerManager.canSeekToPrevious()
} }

View File

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

View File

@ -253,7 +253,7 @@ 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
// FIXME: This is a looping reference // TODO: This is a looping reference
libraryService.onUpdateNotification(session) libraryService.onUpdateNotification(session)
} }

View File

@ -82,11 +82,6 @@ class PlaybackService :
instance = this instance = this
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.i("onStartCommand called: $intent")
return super.onStartCommand(intent, flags, startId)
}
private fun getWakeModeFlag(): Int { private fun getWakeModeFlag(): Int {
return if (ActiveServerProvider.isOffline()) C.WAKE_MODE_LOCAL else C.WAKE_MODE_NETWORK return if (ActiveServerProvider.isOffline()) C.WAKE_MODE_LOCAL else C.WAKE_MODE_NETWORK
} }
@ -153,7 +148,6 @@ class PlaybackService :
.setBitmapLoader(ArtworkBitmapLoader()) .setBitmapLoader(ArtworkBitmapLoader())
.build() .build()
// Set a listener to update the API client when the active server has changed // Set a listener to update the API client when the active server has changed
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
// Set the player wake mode // Set the player wake mode
@ -164,7 +158,7 @@ class PlaybackService :
rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle -> rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle ->
// This only applies for local playback // This only applies for local playback
val exo = if (player is ExoPlayer) { val exo = if (player is ExoPlayer) {
player as ExoPlayer player as ExoPlayer
} else { } else {
return@subscribe return@subscribe
} }
@ -199,14 +193,21 @@ class PlaybackService :
isStarted = true isStarted = true
} }
private fun updateBackend(newBackend: MediaPlayerManager.PlayerBackend) { private fun updateBackend(newBackend: MediaPlayerManager.PlayerBackend) {
Timber.i("Switching player backends") Timber.i("Switching player backends")
// Remove old listeners
player.removeListener(listener)
player.release()
player = if (newBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) { player = if (newBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) {
getJukeboxPlayer() getJukeboxPlayer()
} else { } else {
getLocalPlayer() getLocalPlayer()
} }
// Add fresh listeners
player.addListener(listener)
mediaLibrarySession.player = player mediaLibrarySession.player = player
actualBackend = newBackend actualBackend = newBackend
} }
@ -291,6 +292,7 @@ class PlaybackService :
} }
private fun cacheNextSongs() { private fun cacheNextSongs() {
if (actualBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) return
Timber.d("PlaybackService caching the next songs") Timber.d("PlaybackService caching the next songs")
val nextSongs = Util.getPlayListFromTimeline( val nextSongs = Util.getPlayListFromTimeline(
player.currentTimeline, player.currentTimeline,
@ -382,7 +384,6 @@ class PlaybackService :
companion object { companion object {
var actualBackend: MediaPlayerManager.PlayerBackend? = null var actualBackend: MediaPlayerManager.PlayerBackend? = null
// FIXME with by default stuff
private var desiredBackend: MediaPlayerManager.PlayerBackend? = null private var desiredBackend: MediaPlayerManager.PlayerBackend? = null
fun setBackend(playerBackend: MediaPlayerManager.PlayerBackend) { fun setBackend(playerBackend: MediaPlayerManager.PlayerBackend) {
desiredBackend = playerBackend desiredBackend = playerBackend
@ -395,7 +396,4 @@ class PlaybackService :
private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic error messages" private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic error messages"
private const val NOTIFICATION_ID = 3009 private const val NOTIFICATION_ID = 3009
} }
} }

View File

@ -7,17 +7,12 @@
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.widget.ProgressBar
import android.widget.Toast
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
@ -27,7 +22,15 @@ 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 java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import 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
@ -35,19 +38,11 @@ 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.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util.sleepQuietly import org.moire.ultrasonic.util.Util.sleepQuietly
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.roundToInt
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
/** /**
@ -67,22 +62,45 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
private val timeOfLastUpdate = AtomicLong() private val timeOfLastUpdate = AtomicLong()
private var jukeboxStatus: JukeboxStatus? = null private var jukeboxStatus: JukeboxStatus? = null
private var previousJukeboxStatus: JukeboxStatus? = null private var previousJukeboxStatus: JukeboxStatus? = null
private var gain = 0.5f private var gain = (MAX_GAIN / 3)
private var volumeToast: VolumeToast? = null private val floatGain: Float
get() = gain.toFloat() / MAX_GAIN
private var serviceThread: Thread? = null private var serviceThread: Thread? = null
private var listeners: MutableList<Player.Listener> = mutableListOf() private var listeners: ListenerSet<Player.Listener>
private val playlist: MutableList<MediaItem> = mutableListOf() private val playlist: MutableList<MediaItem> = mutableListOf()
// This must never be smaller 0 private var _currentIndex: Int = 0
private var currentIndex: Int = 0 private var currentIndex: Int
get() = _currentIndex
set(value) {
// This must never be smaller 0
_currentIndex = if (value >= 0) value else 0
}
companion object {
// This is quite important, by setting the DeviceInfo the player is recognized by
// Android as being a remote playback surface
val DEVICE_INFO = DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 10)
val running = AtomicBoolean()
const val MAX_GAIN = 10
}
init { init {
// FIXME: Adapt
//if (running.get()) return
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()
@ -90,23 +108,18 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")
fun onDestroy() { override fun release() {
tasks.clear() tasks.clear()
stop() stop()
if (!running.get()) return if (!running.get()) return
running.set(false) running.set(false)
serviceThread!!.join() serviceThread?.join()
Timber.d("Stopped Jukebox Service") Timber.d("Stopped Jukebox Service")
} }
companion object {
val running = AtomicBoolean()
}
override fun addListener(listener: Player.Listener) { override fun addListener(listener: Player.Listener) {
listeners.add(listener) listeners.add(listener)
} }
@ -155,14 +168,20 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
tasks.add(Skip(mediaItemIndex, positionSeconds)) tasks.add(Skip(mediaItemIndex, positionSeconds))
currentIndex = mediaItemIndex currentIndex = mediaItemIndex
updateAvailableCommands()
} }
override fun seekBack() { override fun seekBack() {
seekTo(0L.coerceAtMost((jukeboxStatus?.positionSeconds ?: 0) - SEEK_INCREMENT_SECONDS)) seekTo(
0L.coerceAtMost(
(jukeboxStatus?.positionSeconds ?: 0) -
Settings.seekIntervalMillis
)
)
} }
override fun seekForward() { override fun seekForward() {
seekTo((jukeboxStatus?.positionSeconds ?: 0) + SEEK_INCREMENT_SECONDS) seekTo((jukeboxStatus?.positionSeconds ?: 0) + Settings.seekIntervalMillis)
} }
override fun isCurrentMediaItemSeekable() = true override fun isCurrentMediaItemSeekable() = true
@ -183,18 +202,15 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
override fun getAvailableCommands(): Player.Commands { override fun getAvailableCommands(): Player.Commands {
// FIXME Stale ness problem!!
Timber.i("adding new commands")
val commandsBuilder = Player.Commands.Builder().addAll( val commandsBuilder = Player.Commands.Builder().addAll(
Player.COMMAND_SET_VOLUME,
Player.COMMAND_GET_VOLUME,
Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_CHANGE_MEDIA_ITEMS,
Player.COMMAND_GET_TIMELINE, Player.COMMAND_GET_TIMELINE,
// TEST Player.COMMAND_GET_DEVICE_VOLUME,
Player.COMMAND_SEEK_TO_NEXT 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 (true || playlist.isNotEmpty()) { if (playlist.isNotEmpty()) {
commandsBuilder.addAll( commandsBuilder.addAll(
Player.COMMAND_GET_CURRENT_MEDIA_ITEM, Player.COMMAND_GET_CURRENT_MEDIA_ITEM,
Player.COMMAND_GET_MEDIA_ITEMS_METADATA, Player.COMMAND_GET_MEDIA_ITEMS_METADATA,
@ -204,19 +220,14 @@ 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
) else { )
Timber.i("Not adding prev command")
}
if (currentIndex < playlist.size - 1) commandsBuilder.addAll( if (currentIndex < playlist.size - 1) commandsBuilder.addAll(
Player.COMMAND_SEEK_TO_NEXT, Player.COMMAND_SEEK_TO_NEXT,
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
) else { )
Timber.i("Not adding seek command")
}
} }
return commandsBuilder.build() return commandsBuilder.build()
} }
@ -225,6 +236,18 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
return availableCommands.contains(command) return availableCommands.contains(command)
} }
private fun updateAvailableCommands() {
Handler(Looper.getMainLooper()).post {
listeners.sendEvent(
Player.EVENT_AVAILABLE_COMMANDS_CHANGED
) { listener: Player.Listener ->
listener.onAvailableCommandsChanged(
availableCommands
)
}
}
}
override fun getPlayWhenReady(): Boolean { override fun getPlayWhenReady(): Boolean {
return isPlaying return isPlaying
} }
@ -241,7 +264,6 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
override fun getCurrentTimeline(): Timeline { override fun getCurrentTimeline(): Timeline {
Timber.i("getCurrentTimeline was requested")
return PlaylistTimeline(playlist) return PlaylistTimeline(playlist)
} }
@ -261,21 +283,43 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {} override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
override fun setVolume(volume: Float) { override fun setDeviceVolume(volume: Int) {
gain = volume gain = volume
tasks.remove(SetGain::class.java) tasks.remove(SetGain::class.java)
tasks.add(SetGain(volume)) tasks.add(SetGain(floatGain))
val context = applicationContext()
if (volumeToast == null) volumeToast = VolumeToast(context) // We must trigger an event so that the Controller knows the new volume
volumeToast!!.setVolume(volume) Handler(Looper.getMainLooper()).post {
listeners.queueEvent(Player.EVENT_DEVICE_VOLUME_CHANGED) {
it.onDeviceVolumeChanged(
gain,
false
)
}
}
}
override fun increaseDeviceVolume() {
gain = (gain + 1).coerceAtMost(MAX_GAIN)
deviceVolume = gain
}
override fun decreaseDeviceVolume() {
gain = (gain - 1).coerceAtLeast(0)
deviceVolume = gain
}
override fun setDeviceMuted(muted: Boolean) {
gain = 0
deviceVolume = gain
} }
override fun getVolume(): Float { override fun getVolume(): Float {
return gain return floatGain
} }
override fun getDeviceVolume(): Int { override fun getDeviceVolume(): Int {
return (gain * 100).toInt() return gain
} }
override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) { override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) {
@ -315,9 +359,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
return Player.REPEAT_MODE_OFF return Player.REPEAT_MODE_OFF
} }
override fun setRepeatMode(repeatMode: Int) { override fun setRepeatMode(repeatMode: Int) {}
// TODO
}
override fun getCurrentPosition(): Long { override fun getCurrentPosition(): Long {
return positionSeconds * 1000L return positionSeconds * 1000L
@ -349,7 +391,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
override fun seekToPrevious() { override fun seekToPrevious() {
if ((jukeboxStatus?.positionSeconds ?: 0) > SEEK_START_AFTER_SECONDS) { if ((jukeboxStatus?.positionSeconds ?: 0) > (Settings.seekIntervalMillis)) {
seekTo(currentIndex, 0) seekTo(currentIndex, 0)
return return
} }
@ -382,8 +424,6 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
private fun startStatusUpdate() { private fun startStatusUpdate() {
stopStatusUpdate() stopStatusUpdate()
val updateTask = Runnable { val updateTask = Runnable {
Timber.i("UpdateTask")
Timber.i("playlist: ${playlist.size}")
tasks.remove(GetStatus::class.java) tasks.remove(GetStatus::class.java)
tasks.add(GetStatus()) tasks.add(GetStatus())
} }
@ -406,25 +446,19 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
@Suppress("LoopWithTooManyJumpStatements") @Suppress("LoopWithTooManyJumpStatements")
private fun processTasks() { private fun processTasks() {
Timber.d("JukeboxMediaPlayer processTasks starting") Timber.d("JukeboxMediaPlayer processTasks starting")
while (true) { while (running.get()) {
// Sleep a bit to spare processor time if we loop a lot // Sleep a bit to spare processor time if we loop a lot
sleepQuietly(10) sleepQuietly(10)
// This is only necessary if Ultrasonic goes offline sooner than the thread stops // This is only necessary if Ultrasonic goes offline sooner than the thread stops
if (isOffline()) continue if (isOffline()) continue
var task: JukeboxTask? = null var task: JukeboxTask? = null
try { try {
task = tasks.poll() task = tasks.poll() ?: continue
// If running is false, exit when the queue is empty
if (task == null && !running.get()) {
Timber.d("JukeboxMediaPlayer processTasks exiting because not running")
break
}
if (task == null) continue
Timber.v("JukeBoxMediaPlayer processTasks processes Task %s", task::class) Timber.v("JukeBoxMediaPlayer processTasks processes Task %s", task::class)
val status = task.execute() val status = task.execute()
onStatusUpdate(status) onStatusUpdate(status)
} catch (x: Throwable) { } catch (all: Throwable) {
onError(task, x) onError(task, all)
} }
} }
Timber.d("JukeboxMediaPlayer processTasks stopped") Timber.d("JukeboxMediaPlayer processTasks stopped")
@ -435,6 +469,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
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 // Ensure that the index is never smaller than 0
// If -1 assume that this means we are not playing // If -1 assume that this means we are not playing
@ -445,23 +480,29 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
currentIndex = jukeboxStatus.currentPlayingIndex ?: currentIndex currentIndex = jukeboxStatus.currentPlayingIndex ?: currentIndex
if (jukeboxStatus.isPlaying != previousJukeboxStatus?.isPlaying) { if (jukeboxStatus.isPlaying != previousJukeboxStatus?.isPlaying) {
shouldUpdateCommands = true
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
listeners.forEach { listeners.queueEvent(Player.EVENT_PLAYBACK_STATE_CHANGED) {
it.onPlaybackStateChanged( it.onPlaybackStateChanged(
if (jukeboxStatus.isPlaying) Player.STATE_READY else Player.STATE_IDLE if (jukeboxStatus.isPlaying) Player.STATE_READY else Player.STATE_IDLE
) )
}
listeners.queueEvent(Player.EVENT_IS_PLAYING_CHANGED) {
it.onIsPlayingChanged(jukeboxStatus.isPlaying) it.onIsPlayingChanged(jukeboxStatus.isPlaying)
} }
} }
} }
if (jukeboxStatus.currentPlayingIndex != previousJukeboxStatus?.currentPlayingIndex) { if (jukeboxStatus.currentPlayingIndex != previousJukeboxStatus?.currentPlayingIndex) {
shouldUpdateCommands = true
currentIndex = jukeboxStatus.currentPlayingIndex ?: 0 currentIndex = jukeboxStatus.currentPlayingIndex ?: 0
val currentMedia = val currentMedia =
if (currentIndex > 0 && currentIndex < playlist.size) playlist[currentIndex] if (currentIndex > 0 && currentIndex < playlist.size) playlist[currentIndex]
else MediaItem.EMPTY else MediaItem.EMPTY
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
listeners.forEach { listeners.queueEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) {
it.onMediaItemTransition( it.onMediaItemTransition(
currentMedia, currentMedia,
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
@ -469,43 +510,40 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
} }
} }
if (shouldUpdateCommands) updateAvailableCommands()
Handler(Looper.getMainLooper()).post {
listeners.flushEvents()
}
} }
private fun onError(task: JukeboxTask?, x: Throwable) { private fun onError(task: JukeboxTask?, x: Throwable) {
var exception: PlaybackException? = null
if (x is ApiNotSupportedException && task !is Stop) { if (x is ApiNotSupportedException && task !is Stop) {
Handler(Looper.getMainLooper()).post { exception = PlaybackException(
listeners.forEach { "Jukebox server too old",
it.onPlayerError( null,
PlaybackException( R.string.download_jukebox_server_too_old
"Jukebox server too old", )
null,
R.string.download_jukebox_server_too_old
)
)
}
}
} else if (x is OfflineException && task !is Stop) { } else if (x is OfflineException && task !is Stop) {
Handler(Looper.getMainLooper()).post { exception = PlaybackException(
listeners.forEach { "Jukebox offline",
it.onPlayerError( null,
PlaybackException( R.string.download_jukebox_offline
"Jukebox offline", )
null,
R.string.download_jukebox_offline
)
)
}
}
} else if (x is SubsonicRESTException && x.code == 50 && task !is Stop) { } else if (x is SubsonicRESTException && x.code == 50 && task !is Stop) {
exception = PlaybackException(
"Jukebox not authorized",
null,
R.string.download_jukebox_not_authorized
)
}
if (exception != null) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
listeners.forEach { listeners.sendEvent(Player.EVENT_PLAYER_ERROR) {
it.onPlayerError( it.onPlayerError(exception)
PlaybackException(
"Jukebox not authorized",
null,
R.string.download_jukebox_not_authorized
)
)
} }
} }
} else { } else {
@ -524,8 +562,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
tasks.add(SetPlaylist(ids)) tasks.add(SetPlaylist(ids))
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
listeners.forEach { listeners.sendEvent(
it.onTimelineChanged( Player.EVENT_TIMELINE_CHANGED
) { listener: Player.Listener ->
listener.onTimelineChanged(
PlaylistTimeline(playlist), PlaylistTimeline(playlist),
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED
) )
@ -635,25 +675,6 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
} }
@SuppressLint("InflateParams")
private class VolumeToast(context: Context) : Toast(context) {
private val progressBar: ProgressBar
fun setVolume(volume: Float) {
progressBar.progress = (100 * volume).roundToInt()
show()
}
init {
duration = LENGTH_SHORT
val inflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val view = inflater.inflate(R.layout.jukebox_volume, null)
progressBar = view.findViewById<View>(R.id.jukebox_volume_progress_bar) as ProgressBar
setView(view)
setGravity(Gravity.TOP, 0, 0)
}
}
// The constants below are necessary so a MediaSession can be built from the Jukebox Service // The constants below are necessary so a MediaSession can be built from the Jukebox Service
override fun isCurrentMediaItemDynamic(): Boolean { override fun isCurrentMediaItemDynamic(): Boolean {
return false return false
@ -664,15 +685,15 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
override fun getMaxSeekToPreviousPosition(): Long { override fun getMaxSeekToPreviousPosition(): Long {
return SEEK_START_AFTER_SECONDS * 1000L return Settings.seekInterval.toLong()
} }
override fun getSeekBackIncrement(): Long { override fun getSeekBackIncrement(): Long {
return SEEK_INCREMENT_SECONDS * 1000L return Settings.seekInterval.toLong()
} }
override fun getSeekForwardIncrement(): Long { override fun getSeekForwardIncrement(): Long {
return SEEK_INCREMENT_SECONDS * 1000L return Settings.seekInterval.toLong()
} }
override fun isLoading(): Boolean { override fun isLoading(): Boolean {
@ -695,6 +716,8 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
return AudioAttributes.DEFAULT return AudioAttributes.DEFAULT
} }
override fun setVolume(volume: Float) {}
override fun getVideoSize(): VideoSize { override fun getVideoSize(): VideoSize {
return VideoSize(0, 0) return VideoSize(0, 0)
} }
@ -740,7 +763,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
override fun getDeviceInfo(): DeviceInfo { override fun getDeviceInfo(): DeviceInfo {
return DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 1) return DEVICE_INFO
} }
override fun getPlayerError(): PlaybackException? { override fun getPlayerError(): PlaybackException? {

View File

@ -1,97 +0,0 @@
/*
* JukeboxNotificationActionFactory.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.service
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.media3.common.Player
import androidx.media3.common.util.Util
import androidx.media3.session.CommandButton
import androidx.media3.session.MediaNotification
import androidx.media3.session.MediaSession
import org.moire.ultrasonic.app.UApp
/**
* This class creates Intents and Actions to be used with the Media Notification
* of the Jukebox Service
*/
@SuppressLint("UnsafeOptInUsageError")
class JukeboxNotificationActionFactory : MediaNotification.ActionFactory {
override fun createMediaAction(
mediaSession: MediaSession,
icon: IconCompat,
title: CharSequence,
command: Int
): NotificationCompat.Action {
return NotificationCompat.Action(
icon, title, createMediaActionPendingIntent(mediaSession, command.toLong())
)
}
override fun createCustomAction(
mediaSession: MediaSession,
icon: IconCompat,
title: CharSequence,
customAction: String,
extras: Bundle
): NotificationCompat.Action {
return NotificationCompat.Action(
icon, title, null
)
}
override fun createCustomActionFromCustomCommandButton(
mediaSession: MediaSession,
customCommandButton: CommandButton
): NotificationCompat.Action {
return NotificationCompat.Action(null, null, null)
}
@Suppress("MagicNumber")
override fun createMediaActionPendingIntent(
mediaSession: MediaSession,
command: Long
): PendingIntent {
val keyCode: Int = toKeyCode(command)
val intent = Intent(Intent.ACTION_MEDIA_BUTTON)
intent.component = ComponentName(UApp.applicationContext(), JukeboxMediaPlayer::class.java)
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
return if (Util.SDK_INT >= 26 && command == Player.COMMAND_PLAY_PAUSE.toLong()) {
return PendingIntent.getForegroundService(
UApp.applicationContext(), keyCode, intent, PendingIntent.FLAG_IMMUTABLE
)
} else {
PendingIntent.getService(
UApp.applicationContext(),
keyCode,
intent,
if (Util.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0
)
}
}
private fun toKeyCode(action: @Player.Command Long): Int {
return when (action.toInt()) {
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
Player.COMMAND_SEEK_TO_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
Player.COMMAND_SEEK_TO_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS
Player.COMMAND_STOP -> KeyEvent.KEYCODE_MEDIA_STOP
Player.COMMAND_SEEK_FORWARD -> KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
Player.COMMAND_SEEK_BACK -> KeyEvent.KEYCODE_MEDIA_REWIND
Player.COMMAND_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
else -> KeyEvent.KEYCODE_UNKNOWN
}
}
}

View File

@ -8,7 +8,6 @@
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Service
import android.view.Surface import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
@ -140,10 +139,6 @@ 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
@ -228,20 +223,4 @@ 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")
}
} }

View File

@ -16,8 +16,6 @@ import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS
import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO
import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Player.REPEAT_MODE_OFF
import androidx.media3.common.Rating import androidx.media3.common.Rating
@ -105,8 +103,8 @@ class MediaPlayerManager(
it() it()
deferredPlay = null deferredPlay = null
} }
val playlist = Util.getPlayListFromTimeline(timeline, false).map(MediaItem::toTrack)
RxBus.playlistPublisher.onNext(Util.getPlayListFromTimeline(timeline, false).map(MediaItem::toTrack)) RxBus.playlistPublisher.onNext(playlist)
} }
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
@ -275,6 +273,10 @@ 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.
@ -349,7 +351,6 @@ 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()
} }
@ -358,8 +359,6 @@ class MediaPlayerManager(
fun play() { fun play() {
controller?.prepare() controller?.prepare()
controller?.play() controller?.play()
val time = controller?.currentTimeline
Timber.i("timeline: ${time?.windowCount}")
} }
@Synchronized @Synchronized
@ -553,9 +552,7 @@ class MediaPlayerManager(
@Synchronized @Synchronized
fun canSeekToPrevious(): Boolean { fun canSeekToPrevious(): Boolean {
val can = controller?.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS) == true return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS) == true
Timber.i("can prev command: $can")
return can
} }
@Synchronized @Synchronized
@ -565,9 +562,7 @@ class MediaPlayerManager(
@Synchronized @Synchronized
fun canSeekToNext(): Boolean { fun canSeekToNext(): Boolean {
val can = controller?.isCommandAvailable(COMMAND_SEEK_TO_NEXT) == true return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT) == true
Timber.i("can seek command: $can")
return can
} }
@Synchronized @Synchronized
@ -666,10 +661,6 @@ 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
*/ */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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