mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-07-21 10:51:55 +03:00
Compare commits
15 Commits
b46e27694a
...
b7889b6324
Author | SHA1 | Date | |
---|---|---|---|
|
b7889b6324 | ||
|
7c325f3ffb | ||
|
b48e82171a | ||
|
e64b4ab486 | ||
|
16ea8e6e24 | ||
|
33933c788b | ||
|
2993c63a16 | ||
|
7a39ad3a6d | ||
|
5e09364d9f | ||
|
a3d9e35199 | ||
|
ce3ad45364 | ||
|
67ad135590 | ||
|
8650023013 | ||
|
554172a3f3 | ||
|
16e40518ef |
@ -1,27 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 7.4.0" type="baseline" client="gradle" dependencies="true" name="AGP (7.4.0)" variant="all" version="7.4.0">
|
||||
|
||||
<issue
|
||||
id="MissingPermission"
|
||||
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
|
||||
errorLine1=" manager.notify(NOTIFICATION_ID, notification)"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt"
|
||||
line="260"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="MissingPermission"
|
||||
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
|
||||
errorLine1=" notificationManagerCompat.notify(notification.notificationId, notification.notification)"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt"
|
||||
line="194"
|
||||
column="9"/>
|
||||
</issue>
|
||||
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="true" name="AGP (8.0.1)" variant="all" version="8.0.1">
|
||||
|
||||
<issue
|
||||
id="PluralsCandidate"
|
||||
@ -30,7 +8,7 @@
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="152"
|
||||
line="151"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
@ -48,50 +26,6 @@
|
||||
file="../core/subsonic-api/build/libs/subsonic-api.jar"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedContentProvider"
|
||||
message="Exported content providers can provide access to potentially sensitive data"
|
||||
errorLine1=" <provider"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="128"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedContentProvider"
|
||||
message="Exported content providers can provide access to potentially sensitive data"
|
||||
errorLine1=" <provider"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="133"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedReceiver"
|
||||
message="Exported receiver does not require permission"
|
||||
errorLine1=" <receiver android:name=".receiver.UltrasonicIntentReceiver""
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="88"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedService"
|
||||
message="Exported service does not require permission"
|
||||
errorLine1=" <service android:name=".playback.PlaybackService""
|
||||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="77"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.drawable.media3_notification_pause` appears to be unused"
|
||||
@ -136,17 +70,6 @@
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.drawable.media3_notification_small_icon` appears to be unused"
|
||||
errorLine1="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
||||
errorLine2="^">
|
||||
<location
|
||||
file="src/main/res/drawable/media3_notification_small_icon.xml"
|
||||
line="1"
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Autofill"
|
||||
message="Missing `autofillHints` attribute"
|
||||
|
@ -66,13 +66,6 @@
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.JukeboxMediaPlayer"
|
||||
android:label="Ultrasonic Jukebox Media Player Service"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
|
||||
<service android:name=".playback.PlaybackService"
|
||||
android:label="@string/common.appname"
|
||||
|
@ -122,6 +122,7 @@ private fun Intent.getBluetoothDevice(): BluetoothDevice? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.provider.SearchRecentSuggestions
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
@ -55,8 +54,8 @@ import org.moire.ultrasonic.data.ServerSettingDao
|
||||
import org.moire.ultrasonic.fragment.OnBackPressedHandler
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
@ -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) {
|
||||
navigationView?.setupWithNavController(navController)
|
||||
|
||||
|
@ -2,8 +2,8 @@ package org.moire.ultrasonic.di
|
||||
|
||||
import org.koin.dsl.module
|
||||
import org.moire.ultrasonic.service.ExternalStorageMonitor
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.PlaybackStateSerializer
|
||||
|
||||
/**
|
||||
|
@ -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) }
|
||||
}
|
||||
|
||||
@ -794,8 +801,7 @@ class PlayerFragment :
|
||||
|
||||
private fun update(cancel: CancellationToken? = null) {
|
||||
if (cancel?.isCancellationRequested == true) return
|
||||
val mediaPlayerController = mediaPlayerManager
|
||||
if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) {
|
||||
if (currentSong?.id != mediaPlayerManager.currentMediaItem?.mediaId) {
|
||||
onTrackChanged()
|
||||
}
|
||||
updateSeekBar()
|
||||
@ -1110,6 +1116,10 @@ class PlayerFragment :
|
||||
|
||||
updateSongRating()
|
||||
|
||||
updateMediaButtonActivationState()
|
||||
}
|
||||
|
||||
private fun updateMediaButtonActivationState() {
|
||||
nextButton.isEnabled = mediaPlayerManager.canSeekToNext()
|
||||
previousButton.isEnabled = mediaPlayerManager.canSeekToPrevious()
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ package org.moire.ultrasonic.model
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
@ -90,7 +90,7 @@ class EditServerModel(val app: Application) : AndroidViewModel(app), KoinCompone
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun queryFeatureSupport(currentServerSetting: ServerSetting): Flow<FeatureSupport> {
|
||||
val client = buildTestClient(currentServerSetting)
|
||||
// One line of magic:
|
||||
|
@ -253,7 +253,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
override fun onSuccess(result: SessionResult) {
|
||||
track.starred = !track.starred
|
||||
// This needs to be called on the main Thread
|
||||
// FIXME: This is a looping reference
|
||||
// TODO: This is a looping reference
|
||||
libraryService.onUpdateNotification(session)
|
||||
}
|
||||
|
||||
|
@ -82,11 +82,6 @@ class PlaybackService :
|
||||
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 {
|
||||
return if (ActiveServerProvider.isOffline()) C.WAKE_MODE_LOCAL else C.WAKE_MODE_NETWORK
|
||||
}
|
||||
@ -153,7 +148,6 @@ class PlaybackService :
|
||||
.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
|
||||
@ -164,7 +158,7 @@ class PlaybackService :
|
||||
rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle ->
|
||||
// This only applies for local playback
|
||||
val exo = if (player is ExoPlayer) {
|
||||
player as ExoPlayer
|
||||
player as ExoPlayer
|
||||
} else {
|
||||
return@subscribe
|
||||
}
|
||||
@ -199,14 +193,21 @@ class PlaybackService :
|
||||
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
|
||||
}
|
||||
@ -291,6 +292,7 @@ class PlaybackService :
|
||||
}
|
||||
|
||||
private fun cacheNextSongs() {
|
||||
if (actualBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) return
|
||||
Timber.d("PlaybackService caching the next songs")
|
||||
val nextSongs = Util.getPlayListFromTimeline(
|
||||
player.currentTimeline,
|
||||
@ -382,7 +384,6 @@ class PlaybackService :
|
||||
companion object {
|
||||
var actualBackend: MediaPlayerManager.PlayerBackend? = null
|
||||
|
||||
// FIXME with by default stuff
|
||||
private var desiredBackend: MediaPlayerManager.PlayerBackend? = null
|
||||
fun setBackend(playerBackend: MediaPlayerManager.PlayerBackend) {
|
||||
desiredBackend = playerBackend
|
||||
@ -395,7 +396,4 @@ class PlaybackService :
|
||||
private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic error messages"
|
||||
private const val NOTIFICATION_ID = 3009
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -7,17 +7,12 @@
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
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.C
|
||||
import androidx.media3.common.DeviceInfo
|
||||
import androidx.media3.common.FlagSet
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.PlaybackException
|
||||
@ -27,7 +22,15 @@ import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.Clock
|
||||
import androidx.media3.common.util.ListenerSet
|
||||
import androidx.media3.common.util.Size
|
||||
import 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.api.subsonic.ApiNotSupportedException
|
||||
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.domain.JukeboxStatus
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util.sleepQuietly
|
||||
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 SEEK_INCREMENT_SECONDS = 5L
|
||||
private const val SEEK_START_AFTER_SECONDS = 5
|
||||
private const val QUEUE_POLL_INTERVAL_SECONDS = 1L
|
||||
|
||||
/**
|
||||
@ -67,22 +62,45 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
private val timeOfLastUpdate = AtomicLong()
|
||||
private var jukeboxStatus: JukeboxStatus? = null
|
||||
private var previousJukeboxStatus: JukeboxStatus? = null
|
||||
private var gain = 0.5f
|
||||
private var volumeToast: VolumeToast? = null
|
||||
private var gain = (MAX_GAIN / 3)
|
||||
private val floatGain: Float
|
||||
get() = gain.toFloat() / MAX_GAIN
|
||||
|
||||
private var serviceThread: Thread? = null
|
||||
|
||||
private var listeners: MutableList<Player.Listener> = mutableListOf()
|
||||
private var listeners: ListenerSet<Player.Listener>
|
||||
private val playlist: MutableList<MediaItem> = mutableListOf()
|
||||
|
||||
// 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 {
|
||||
// FIXME: Adapt
|
||||
//if (running.get()) return
|
||||
running.set(true)
|
||||
|
||||
listeners = ListenerSet(
|
||||
applicationLooper,
|
||||
Clock.DEFAULT
|
||||
) { listener: Player.Listener, flags: FlagSet? ->
|
||||
listener.onEvents(
|
||||
this,
|
||||
Player.Events(
|
||||
flags!!
|
||||
)
|
||||
)
|
||||
}
|
||||
tasks.clear()
|
||||
updatePlaylist()
|
||||
stop()
|
||||
@ -90,23 +108,18 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
@Suppress("MagicNumber")
|
||||
|
||||
fun onDestroy() {
|
||||
override fun release() {
|
||||
tasks.clear()
|
||||
stop()
|
||||
|
||||
if (!running.get()) return
|
||||
running.set(false)
|
||||
|
||||
serviceThread!!.join()
|
||||
|
||||
serviceThread?.join()
|
||||
|
||||
Timber.d("Stopped Jukebox Service")
|
||||
}
|
||||
|
||||
companion object {
|
||||
val running = AtomicBoolean()
|
||||
}
|
||||
|
||||
override fun addListener(listener: Player.Listener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
@ -155,14 +168,20 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
tasks.add(Skip(mediaItemIndex, positionSeconds))
|
||||
currentIndex = mediaItemIndex
|
||||
updateAvailableCommands()
|
||||
}
|
||||
|
||||
override fun seekBack() {
|
||||
seekTo(0L.coerceAtMost((jukeboxStatus?.positionSeconds ?: 0) - SEEK_INCREMENT_SECONDS))
|
||||
seekTo(
|
||||
0L.coerceAtMost(
|
||||
(jukeboxStatus?.positionSeconds ?: 0) -
|
||||
Settings.seekIntervalMillis
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun seekForward() {
|
||||
seekTo((jukeboxStatus?.positionSeconds ?: 0) + SEEK_INCREMENT_SECONDS)
|
||||
seekTo((jukeboxStatus?.positionSeconds ?: 0) + Settings.seekIntervalMillis)
|
||||
}
|
||||
|
||||
override fun isCurrentMediaItemSeekable() = true
|
||||
@ -183,18 +202,15 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
|
||||
override fun getAvailableCommands(): Player.Commands {
|
||||
// FIXME Stale ness problem!!
|
||||
Timber.i("adding new commands")
|
||||
val commandsBuilder = Player.Commands.Builder().addAll(
|
||||
Player.COMMAND_SET_VOLUME,
|
||||
Player.COMMAND_GET_VOLUME,
|
||||
Player.COMMAND_CHANGE_MEDIA_ITEMS,
|
||||
Player.COMMAND_GET_TIMELINE,
|
||||
// TEST
|
||||
Player.COMMAND_SEEK_TO_NEXT
|
||||
Player.COMMAND_GET_DEVICE_VOLUME,
|
||||
Player.COMMAND_ADJUST_DEVICE_VOLUME,
|
||||
Player.COMMAND_SET_DEVICE_VOLUME
|
||||
)
|
||||
if (isPlaying) commandsBuilder.add(Player.COMMAND_STOP)
|
||||
if (true || playlist.isNotEmpty()) {
|
||||
if (playlist.isNotEmpty()) {
|
||||
commandsBuilder.addAll(
|
||||
Player.COMMAND_GET_CURRENT_MEDIA_ITEM,
|
||||
Player.COMMAND_GET_MEDIA_ITEMS_METADATA,
|
||||
@ -204,19 +220,14 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
Player.COMMAND_SEEK_FORWARD,
|
||||
Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
|
||||
Player.COMMAND_SEEK_TO_MEDIA_ITEM,
|
||||
)
|
||||
if (currentIndex > 0) commandsBuilder.addAll(
|
||||
// Seeking back is always available
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS,
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
|
||||
) else {
|
||||
Timber.i("Not adding prev command")
|
||||
}
|
||||
)
|
||||
if (currentIndex < playlist.size - 1) commandsBuilder.addAll(
|
||||
Player.COMMAND_SEEK_TO_NEXT,
|
||||
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
|
||||
) else {
|
||||
Timber.i("Not adding seek command")
|
||||
}
|
||||
)
|
||||
}
|
||||
return commandsBuilder.build()
|
||||
}
|
||||
@ -225,6 +236,18 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
return availableCommands.contains(command)
|
||||
}
|
||||
|
||||
private fun updateAvailableCommands() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.sendEvent(
|
||||
Player.EVENT_AVAILABLE_COMMANDS_CHANGED
|
||||
) { listener: Player.Listener ->
|
||||
listener.onAvailableCommandsChanged(
|
||||
availableCommands
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPlayWhenReady(): Boolean {
|
||||
return isPlaying
|
||||
}
|
||||
@ -241,7 +264,6 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
|
||||
override fun getCurrentTimeline(): Timeline {
|
||||
Timber.i("getCurrentTimeline was requested")
|
||||
return PlaylistTimeline(playlist)
|
||||
}
|
||||
|
||||
@ -261,21 +283,43 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
|
||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
|
||||
|
||||
override fun setVolume(volume: Float) {
|
||||
override fun setDeviceVolume(volume: Int) {
|
||||
gain = volume
|
||||
tasks.remove(SetGain::class.java)
|
||||
tasks.add(SetGain(volume))
|
||||
val context = applicationContext()
|
||||
if (volumeToast == null) volumeToast = VolumeToast(context)
|
||||
volumeToast!!.setVolume(volume)
|
||||
tasks.add(SetGain(floatGain))
|
||||
|
||||
// We must trigger an event so that the Controller knows the new volume
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.queueEvent(Player.EVENT_DEVICE_VOLUME_CHANGED) {
|
||||
it.onDeviceVolumeChanged(
|
||||
gain,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun increaseDeviceVolume() {
|
||||
gain = (gain + 1).coerceAtMost(MAX_GAIN)
|
||||
deviceVolume = gain
|
||||
}
|
||||
|
||||
override fun decreaseDeviceVolume() {
|
||||
gain = (gain - 1).coerceAtLeast(0)
|
||||
deviceVolume = gain
|
||||
}
|
||||
|
||||
override fun setDeviceMuted(muted: Boolean) {
|
||||
gain = 0
|
||||
deviceVolume = gain
|
||||
}
|
||||
|
||||
override fun getVolume(): Float {
|
||||
return gain
|
||||
return floatGain
|
||||
}
|
||||
|
||||
override fun getDeviceVolume(): Int {
|
||||
return (gain * 100).toInt()
|
||||
return gain
|
||||
}
|
||||
|
||||
override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) {
|
||||
@ -315,9 +359,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
return Player.REPEAT_MODE_OFF
|
||||
}
|
||||
|
||||
override fun setRepeatMode(repeatMode: Int) {
|
||||
// TODO
|
||||
}
|
||||
override fun setRepeatMode(repeatMode: Int) {}
|
||||
|
||||
override fun getCurrentPosition(): Long {
|
||||
return positionSeconds * 1000L
|
||||
@ -349,7 +391,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
|
||||
override fun seekToPrevious() {
|
||||
if ((jukeboxStatus?.positionSeconds ?: 0) > SEEK_START_AFTER_SECONDS) {
|
||||
if ((jukeboxStatus?.positionSeconds ?: 0) > (Settings.seekIntervalMillis)) {
|
||||
seekTo(currentIndex, 0)
|
||||
return
|
||||
}
|
||||
@ -382,8 +424,6 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
private fun startStatusUpdate() {
|
||||
stopStatusUpdate()
|
||||
val updateTask = Runnable {
|
||||
Timber.i("UpdateTask")
|
||||
Timber.i("playlist: ${playlist.size}")
|
||||
tasks.remove(GetStatus::class.java)
|
||||
tasks.add(GetStatus())
|
||||
}
|
||||
@ -406,25 +446,19 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
@Suppress("LoopWithTooManyJumpStatements")
|
||||
private fun processTasks() {
|
||||
Timber.d("JukeboxMediaPlayer processTasks starting")
|
||||
while (true) {
|
||||
while (running.get()) {
|
||||
// Sleep a bit to spare processor time if we loop a lot
|
||||
sleepQuietly(10)
|
||||
// This is only necessary if Ultrasonic goes offline sooner than the thread stops
|
||||
if (isOffline()) continue
|
||||
var task: JukeboxTask? = null
|
||||
try {
|
||||
task = tasks.poll()
|
||||
// If running is false, exit when the queue is empty
|
||||
if (task == null && !running.get()) {
|
||||
Timber.d("JukeboxMediaPlayer processTasks exiting because not running")
|
||||
break
|
||||
}
|
||||
if (task == null) continue
|
||||
task = tasks.poll() ?: continue
|
||||
Timber.v("JukeBoxMediaPlayer processTasks processes Task %s", task::class)
|
||||
val status = task.execute()
|
||||
onStatusUpdate(status)
|
||||
} catch (x: Throwable) {
|
||||
onError(task, x)
|
||||
} catch (all: Throwable) {
|
||||
onError(task, all)
|
||||
}
|
||||
}
|
||||
Timber.d("JukeboxMediaPlayer processTasks stopped")
|
||||
@ -435,6 +469,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
timeOfLastUpdate.set(System.currentTimeMillis())
|
||||
previousJukeboxStatus = this.jukeboxStatus
|
||||
this.jukeboxStatus = jukeboxStatus
|
||||
var shouldUpdateCommands = false
|
||||
|
||||
// Ensure that the index is never smaller than 0
|
||||
// If -1 assume that this means we are not playing
|
||||
@ -445,23 +480,29 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
currentIndex = jukeboxStatus.currentPlayingIndex ?: currentIndex
|
||||
|
||||
if (jukeboxStatus.isPlaying != previousJukeboxStatus?.isPlaying) {
|
||||
shouldUpdateCommands = true
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
listeners.queueEvent(Player.EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
it.onPlaybackStateChanged(
|
||||
if (jukeboxStatus.isPlaying) Player.STATE_READY else Player.STATE_IDLE
|
||||
)
|
||||
}
|
||||
|
||||
listeners.queueEvent(Player.EVENT_IS_PLAYING_CHANGED) {
|
||||
it.onIsPlayingChanged(jukeboxStatus.isPlaying)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jukeboxStatus.currentPlayingIndex != previousJukeboxStatus?.currentPlayingIndex) {
|
||||
shouldUpdateCommands = true
|
||||
currentIndex = jukeboxStatus.currentPlayingIndex ?: 0
|
||||
val currentMedia =
|
||||
if (currentIndex > 0 && currentIndex < playlist.size) playlist[currentIndex]
|
||||
else MediaItem.EMPTY
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
listeners.queueEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) {
|
||||
it.onMediaItemTransition(
|
||||
currentMedia,
|
||||
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
|
||||
@ -469,43 +510,40 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUpdateCommands) updateAvailableCommands()
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.flushEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError(task: JukeboxTask?, x: Throwable) {
|
||||
var exception: PlaybackException? = null
|
||||
if (x is ApiNotSupportedException && task !is Stop) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onPlayerError(
|
||||
PlaybackException(
|
||||
"Jukebox server too old",
|
||||
null,
|
||||
R.string.download_jukebox_server_too_old
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
exception = PlaybackException(
|
||||
"Jukebox server too old",
|
||||
null,
|
||||
R.string.download_jukebox_server_too_old
|
||||
)
|
||||
} else if (x is OfflineException && task !is Stop) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onPlayerError(
|
||||
PlaybackException(
|
||||
"Jukebox offline",
|
||||
null,
|
||||
R.string.download_jukebox_offline
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
exception = PlaybackException(
|
||||
"Jukebox offline",
|
||||
null,
|
||||
R.string.download_jukebox_offline
|
||||
)
|
||||
} else if (x is SubsonicRESTException && x.code == 50 && task !is Stop) {
|
||||
exception = PlaybackException(
|
||||
"Jukebox not authorized",
|
||||
null,
|
||||
R.string.download_jukebox_not_authorized
|
||||
)
|
||||
}
|
||||
|
||||
if (exception != null) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onPlayerError(
|
||||
PlaybackException(
|
||||
"Jukebox not authorized",
|
||||
null,
|
||||
R.string.download_jukebox_not_authorized
|
||||
)
|
||||
)
|
||||
listeners.sendEvent(Player.EVENT_PLAYER_ERROR) {
|
||||
it.onPlayerError(exception)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -524,8 +562,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
tasks.add(SetPlaylist(ids))
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onTimelineChanged(
|
||||
listeners.sendEvent(
|
||||
Player.EVENT_TIMELINE_CHANGED
|
||||
) { listener: Player.Listener ->
|
||||
listener.onTimelineChanged(
|
||||
PlaylistTimeline(playlist),
|
||||
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED
|
||||
)
|
||||
@ -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
|
||||
override fun isCurrentMediaItemDynamic(): Boolean {
|
||||
return false
|
||||
@ -664,15 +685,15 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
|
||||
override fun getMaxSeekToPreviousPosition(): Long {
|
||||
return SEEK_START_AFTER_SECONDS * 1000L
|
||||
return Settings.seekInterval.toLong()
|
||||
}
|
||||
|
||||
override fun getSeekBackIncrement(): Long {
|
||||
return SEEK_INCREMENT_SECONDS * 1000L
|
||||
return Settings.seekInterval.toLong()
|
||||
}
|
||||
|
||||
override fun getSeekForwardIncrement(): Long {
|
||||
return SEEK_INCREMENT_SECONDS * 1000L
|
||||
return Settings.seekInterval.toLong()
|
||||
}
|
||||
|
||||
override fun isLoading(): Boolean {
|
||||
@ -695,6 +716,8 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
return AudioAttributes.DEFAULT
|
||||
}
|
||||
|
||||
override fun setVolume(volume: Float) {}
|
||||
|
||||
override fun getVideoSize(): VideoSize {
|
||||
return VideoSize(0, 0)
|
||||
}
|
||||
@ -740,7 +763,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
|
||||
override fun getDeviceInfo(): DeviceInfo {
|
||||
return DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 1)
|
||||
return DEVICE_INFO
|
||||
}
|
||||
|
||||
override fun getPlayerError(): PlaybackException? {
|
||||
|
@ -1,97 +0,0 @@
|
||||
/*
|
||||
* JukeboxNotificationActionFactory.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.MediaNotification
|
||||
import androidx.media3.session.MediaSession
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
|
||||
/**
|
||||
* This class creates Intents and Actions to be used with the Media Notification
|
||||
* of the Jukebox Service
|
||||
*/
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class JukeboxNotificationActionFactory : MediaNotification.ActionFactory {
|
||||
override fun createMediaAction(
|
||||
mediaSession: MediaSession,
|
||||
icon: IconCompat,
|
||||
title: CharSequence,
|
||||
command: Int
|
||||
): NotificationCompat.Action {
|
||||
return NotificationCompat.Action(
|
||||
icon, title, createMediaActionPendingIntent(mediaSession, command.toLong())
|
||||
)
|
||||
}
|
||||
|
||||
override fun createCustomAction(
|
||||
mediaSession: MediaSession,
|
||||
icon: IconCompat,
|
||||
title: CharSequence,
|
||||
customAction: String,
|
||||
extras: Bundle
|
||||
): NotificationCompat.Action {
|
||||
return NotificationCompat.Action(
|
||||
icon, title, null
|
||||
)
|
||||
}
|
||||
|
||||
override fun createCustomActionFromCustomCommandButton(
|
||||
mediaSession: MediaSession,
|
||||
customCommandButton: CommandButton
|
||||
): NotificationCompat.Action {
|
||||
return NotificationCompat.Action(null, null, null)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun createMediaActionPendingIntent(
|
||||
mediaSession: MediaSession,
|
||||
command: Long
|
||||
): PendingIntent {
|
||||
val keyCode: Int = toKeyCode(command)
|
||||
val intent = Intent(Intent.ACTION_MEDIA_BUTTON)
|
||||
intent.component = ComponentName(UApp.applicationContext(), JukeboxMediaPlayer::class.java)
|
||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
|
||||
return if (Util.SDK_INT >= 26 && command == Player.COMMAND_PLAY_PAUSE.toLong()) {
|
||||
return PendingIntent.getForegroundService(
|
||||
UApp.applicationContext(), keyCode, intent, PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getService(
|
||||
UApp.applicationContext(),
|
||||
keyCode,
|
||||
intent,
|
||||
if (Util.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toKeyCode(action: @Player.Command Long): Int {
|
||||
return when (action.toInt()) {
|
||||
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
|
||||
Player.COMMAND_SEEK_TO_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS
|
||||
Player.COMMAND_STOP -> KeyEvent.KEYCODE_MEDIA_STOP
|
||||
Player.COMMAND_SEEK_FORWARD -> KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|
||||
Player.COMMAND_SEEK_BACK -> KeyEvent.KEYCODE_MEDIA_REWIND
|
||||
Player.COMMAND_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||
else -> KeyEvent.KEYCODE_UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
@ -140,10 +139,6 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getCurrentTracks(): Tracks {
|
||||
// TODO Dummy information is returned for now, this seems to work
|
||||
return Tracks.EMPTY
|
||||
@ -228,20 +223,4 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
||||
override fun clearVideoTextureView(textureView: TextureView?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setDeviceVolume(volume: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun increaseDeviceVolume() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun decreaseDeviceVolume() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setDeviceMuted(muted: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,6 @@ import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS
|
||||
import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO
|
||||
import androidx.media3.common.Player.REPEAT_MODE_OFF
|
||||
import androidx.media3.common.Rating
|
||||
@ -105,8 +103,8 @@ class MediaPlayerManager(
|
||||
it()
|
||||
deferredPlay = null
|
||||
}
|
||||
|
||||
RxBus.playlistPublisher.onNext(Util.getPlayListFromTimeline(timeline, false).map(MediaItem::toTrack))
|
||||
val playlist = Util.getPlayListFromTimeline(timeline, false).map(MediaItem::toTrack)
|
||||
RxBus.playlistPublisher.onNext(playlist)
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
@ -275,6 +273,10 @@ class MediaPlayerManager(
|
||||
}
|
||||
}
|
||||
|
||||
fun addListener(listener: Player.Listener) {
|
||||
controller?.addListener(listener)
|
||||
}
|
||||
|
||||
private fun clearBookmark() {
|
||||
// This method is called just before we update the cachedMediaItem,
|
||||
// so in fact cachedMediaItem will refer to the track that has just finished.
|
||||
@ -349,7 +351,6 @@ class MediaPlayerManager(
|
||||
@Synchronized
|
||||
fun play(index: Int) {
|
||||
controller?.seekTo(index, 0L)
|
||||
// FIXME CHECK ITS NOT MAKING PROBLEMS
|
||||
controller?.prepare()
|
||||
controller?.play()
|
||||
}
|
||||
@ -358,8 +359,6 @@ class MediaPlayerManager(
|
||||
fun play() {
|
||||
controller?.prepare()
|
||||
controller?.play()
|
||||
val time = controller?.currentTimeline
|
||||
Timber.i("timeline: ${time?.windowCount}")
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@ -553,9 +552,7 @@ class MediaPlayerManager(
|
||||
|
||||
@Synchronized
|
||||
fun canSeekToPrevious(): Boolean {
|
||||
val can = controller?.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS) == true
|
||||
Timber.i("can prev command: $can")
|
||||
return can
|
||||
return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS) == true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@ -565,9 +562,7 @@ class MediaPlayerManager(
|
||||
|
||||
@Synchronized
|
||||
fun canSeekToNext(): Boolean {
|
||||
val can = controller?.isCommandAvailable(COMMAND_SEEK_TO_NEXT) == true
|
||||
Timber.i("can seek command: $can")
|
||||
return can
|
||||
return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT) == true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@ -666,10 +661,6 @@ class MediaPlayerManager(
|
||||
controller?.volume = gain
|
||||
}
|
||||
|
||||
fun setVolume(volume: Float) {
|
||||
controller?.volume = volume
|
||||
}
|
||||
|
||||
/*
|
||||
* Sets the rating of the current track
|
||||
*/
|
||||
|
@ -128,6 +128,9 @@ object Settings {
|
||||
var seekInterval
|
||||
by StringIntSetting(getKey(R.string.setting_key_increment_time), 5000)
|
||||
|
||||
val seekIntervalMillis: Long
|
||||
get() = (seekInterval / 1000).toLong()
|
||||
|
||||
@JvmStatic
|
||||
var mediaButtonsEnabled
|
||||
by BooleanSetting(getKey(R.string.setting_key_media_buttons), true)
|
||||
|
@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||
a:id="@+id/toast_layout_root"
|
||||
a:orientation="vertical"
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="fill_parent"
|
||||
a:background="@android:drawable/toast_frame">
|
||||
|
||||
<TextView
|
||||
a:layout_width="wrap_content"
|
||||
a:layout_height="wrap_content"
|
||||
a:text="@string/download.jukebox_volume"
|
||||
a:textAppearance="?android:attr/textAppearanceMedium"
|
||||
a:textColor="#ffffffff"
|
||||
a:shadowColor="#bb000000"
|
||||
a:shadowRadius="2.75"
|
||||
a:paddingStart="32dp"
|
||||
a:paddingEnd="32dp"
|
||||
a:paddingBottom="12dp"
|
||||
/>
|
||||
|
||||
<ProgressBar a:id="@+id/jukebox_volume_progress_bar"
|
||||
style="@android:style/Widget.Holo.Light.ProgressBar.Horizontal"
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="wrap_content"
|
||||
a:paddingBottom="3dp" />
|
||||
|
||||
</LinearLayout>
|
@ -48,7 +48,6 @@
|
||||
<string name="download.jukebox_offline">Vzdálené ovládání není dostupné v offline módu.</string>
|
||||
<string name="download.jukebox_on">Vzdálené ovládání zapnuto. Hudba přehrávána na serveru.</string>
|
||||
<string name="download.jukebox_server_too_old">Vzdálené ovládání není podporováno. Aktualizujte svůj Subsonic server.</string>
|
||||
<string name="download.jukebox_volume">Hlasitost vzdáleného přístroje</string>
|
||||
<string name="download.menu_equalizer">Ekvalizér</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox vypnut</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox zapnut</string>
|
||||
|
@ -61,7 +61,6 @@
|
||||
<string name="download.jukebox_offline">Fernbedienungs-Modus is Offline nicht verfügbar.</string>
|
||||
<string name="download.jukebox_on">Fernbedienung ausgeschaltet. Musik wird auf dem Server wiedergegeben.</string>
|
||||
<string name="download.jukebox_server_too_old">Fernbedienungs Modus wird nicht unterstützt. Bitte den Subsonic Server aktualisieren.</string>
|
||||
<string name="download.jukebox_volume">Entfernte Lautstärke</string>
|
||||
<string name="download.menu_equalizer">Equalizer</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox Aus</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox An</string>
|
||||
|
@ -62,7 +62,6 @@
|
||||
<string name="download.jukebox_offline">Control remoto no disponible en modo fuera de línea.</string>
|
||||
<string name="download.jukebox_on">Control remoto encendido. La música se reproduce en el servidor.</string>
|
||||
<string name="download.jukebox_server_too_old">Control remoto no soportado. Por favor actualiza tu servidor de Subsonic.</string>
|
||||
<string name="download.jukebox_volume">Volumen remoto</string>
|
||||
<string name="download.menu_equalizer">Ecualizador</string>
|
||||
<string name="download.menu_jukebox_off">Apagar Jukebox</string>
|
||||
<string name="download.menu_jukebox_on">Encender Jukebox</string>
|
||||
|
@ -61,7 +61,6 @@
|
||||
<string name="download.jukebox_offline">Le mode jukebox n\'est pas disponible en mode déconnecté.</string>
|
||||
<string name="download.jukebox_on">Mode jukebox activé. La musique est jouée sur le serveur</string>
|
||||
<string name="download.jukebox_server_too_old">Le mode jukebox n\'est pas pris en charge. Mise à jour du serveur Subsonic requise.</string>
|
||||
<string name="download.jukebox_volume">Volume sur serveur distant</string>
|
||||
<string name="download.menu_equalizer">Égaliseur</string>
|
||||
<string name="download.menu_jukebox_off">Désactiver le mode jukebox</string>
|
||||
<string name="download.menu_jukebox_on">Activer le mode jukebox</string>
|
||||
|
@ -54,7 +54,6 @@
|
||||
<string name="download.jukebox_offline">A távvezérlés nem lehetséges kapcsolat nélküli módban!</string>
|
||||
<string name="download.jukebox_on">Távvezérlés bekapcsolása. A zenelejátszás a kiszolgálón történik.</string>
|
||||
<string name="download.jukebox_server_too_old">A távvezérlés nem támogatott. Kérjük, frissítse a Subsonic kiszolgálót!</string>
|
||||
<string name="download.jukebox_volume">Hangerő távvezérlése</string>
|
||||
<string name="download.menu_equalizer">Equalizer</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox ki</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox be</string>
|
||||
|
@ -45,7 +45,6 @@
|
||||
<string name="download.jukebox_offline">Il controllo remoto non è disponibile nella modalità offline. </string>
|
||||
<string name="download.jukebox_on">Controllo remoto abilitato. La musica verrà riprodotta sul server.</string>
|
||||
<string name="download.jukebox_server_too_old">Il controllo remoto non è supportato. Per favore aggiorna la versione del server Airsonic.</string>
|
||||
<string name="download.jukebox_volume">Volume remoto</string>
|
||||
<string name="download.menu_equalizer">Equalizzatore</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox spento</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox acceso</string>
|
||||
|
@ -42,7 +42,6 @@
|
||||
<string name="download.jukebox_on">リモートコントロールがオンになりました。サーバーで音楽が再生されます。</string>
|
||||
<string name="download.jukebox_off">リモートコントロールがオフになりました。モバイル端末で音楽が再生されます。</string>
|
||||
<string name="download.jukebox_server_too_old">リモートコントロールがサポートされていません。Subsonicサーバーをアップグレードしてください。</string>
|
||||
<string name="download.jukebox_volume">リモート音量</string>
|
||||
<string name="download.menu_jukebox_on">ジュークボックス ON</string>
|
||||
<string name="download.menu_lyrics">歌詞</string>
|
||||
<string name="download.menu_show_album">アルバムを表示</string>
|
||||
|
@ -339,7 +339,6 @@
|
||||
<string name="download.jukebox_not_authorized">Fjernkontroll er avskrudd. Skru på jukebox-modus i <b>Brukere > Innstillinger</b> på din Subsonic-tjener.</string>
|
||||
<string name="download.jukebox_off">Fjernkontroll avskrudd. Musikk spilles på enheten.</string>
|
||||
<string name="download.jukebox_server_too_old">Fjernkontroll støttes ikke. Oppgrader din Subsonic-tjener.</string>
|
||||
<string name="download.jukebox_volume">Fjernkontroll</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox avslått</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox påslått</string>
|
||||
<string name="download.menu_shuffle">Omstokking</string>
|
||||
|
@ -63,7 +63,6 @@
|
||||
<string name="download.jukebox_offline">Afstandsbediening is niet beschikbaar in offline-modus.</string>
|
||||
<string name="download.jukebox_on">Afstandsbediening ingeschakeld; muziek wordt afgespeeld op de server.</string>
|
||||
<string name="download.jukebox_server_too_old">Afstandsbediening wordt niet ondersteund. Werk je Subsonic-server bij.</string>
|
||||
<string name="download.jukebox_volume">Afstandsbedieningvolume</string>
|
||||
<string name="download.menu_equalizer">Equalizer</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox uitgeschakeld</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox ingeschakeld</string>
|
||||
|
@ -47,7 +47,6 @@
|
||||
<string name="download.jukebox_offline">Pilot jest niedostępny w trybie offline.</string>
|
||||
<string name="download.jukebox_on">Tryb pilota jest włączony. Muzyka jest odtwarzana na serwerze.</string>
|
||||
<string name="download.jukebox_server_too_old">Tryb pilota jest niedostępny. Proszę uaktualnić serwer Subsonic.</string>
|
||||
<string name="download.jukebox_volume">Zdalna głośność</string>
|
||||
<string name="download.menu_equalizer">Korektor dźwięku</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox wyłączony</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox włączony</string>
|
||||
|
@ -62,7 +62,6 @@
|
||||
<string name="download.jukebox_offline">Controle remoto não está disponível no modo offline.</string>
|
||||
<string name="download.jukebox_on">Controle remoto ligado. Música tocada no servidor.</string>
|
||||
<string name="download.jukebox_server_too_old">Controle remoto não suportado. Atualize seu servidor Subsonic.</string>
|
||||
<string name="download.jukebox_volume">Volume Remoto</string>
|
||||
<string name="download.menu_equalizer">Equalizador</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox Desligado</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox Ligado</string>
|
||||
|
@ -47,7 +47,6 @@
|
||||
<string name="download.jukebox_offline">Controle remoto não está disponível no modo offline.</string>
|
||||
<string name="download.jukebox_on">Controle remoto ligado. Música tocada no servidor.</string>
|
||||
<string name="download.jukebox_server_too_old">Controle remoto não suportado. Atualize seu servidor Subsonic.</string>
|
||||
<string name="download.jukebox_volume">Volume Remoto</string>
|
||||
<string name="download.menu_equalizer">Equalizador</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox Desligado</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox Ligado</string>
|
||||
|
@ -59,7 +59,6 @@
|
||||
<string name="download.jukebox_offline">Пульт дистанционного управления недоступен в автономном режиме.</string>
|
||||
<string name="download.jukebox_on">Включен пульт управления. Музыка играет на сервере.</string>
|
||||
<string name="download.jukebox_server_too_old">Пульт дистанционного управления не поддерживается. Пожалуйста, обновите ваш дозвуковой сервер.</string>
|
||||
<string name="download.jukebox_volume">Удаленная громкость</string>
|
||||
<string name="download.menu_equalizer">Эквалайзер</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox выключен</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox включен</string>
|
||||
|
@ -60,7 +60,6 @@
|
||||
<string name="download.jukebox_offline">离线模式不支持远程控制。</string>
|
||||
<string name="download.jukebox_on">已打开远程控制,音乐将在服务端播放。</string>
|
||||
<string name="download.jukebox_server_too_old">远程控制不支持,请升级您的 Subsonic 服务器。</string>
|
||||
<string name="download.jukebox_volume">远程音量</string>
|
||||
<string name="download.menu_equalizer">均衡器</string>
|
||||
<string name="download.menu_jukebox_off">关闭点唱机</string>
|
||||
<string name="download.menu_jukebox_on">开启点唱机</string>
|
||||
|
@ -141,7 +141,6 @@
|
||||
<string name="common.pin">固定</string>
|
||||
<string name="chat.send_button">傳送</string>
|
||||
<string name="button_bar.chat">聊天</string>
|
||||
<string name="download.jukebox_volume">遠端音量</string>
|
||||
<string name="chat.user_avatar">頭像</string>
|
||||
<string name="common.delete_selection_confirmation">您真的要刪除目前選取的項目嗎?</string>
|
||||
<string name="background_task.parse_error">無法理解答覆,請檢查伺服器位址。</string>
|
||||
|
@ -63,7 +63,6 @@
|
||||
<string name="download.jukebox_offline">Remote control is not available in offline mode.</string>
|
||||
<string name="download.jukebox_on">Turned on remote control. Music is played on server.</string>
|
||||
<string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string>
|
||||
<string name="download.jukebox_volume">Remote Volume</string>
|
||||
<string name="download.menu_equalizer">Equalizer</string>
|
||||
<string name="download.menu_jukebox_off">Jukebox Off</string>
|
||||
<string name="download.menu_jukebox_on">Jukebox On</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user