Add sleep timer

This commit is contained in:
vfsfitvnm 2022-06-15 14:35:51 +02:00
parent 92963642a2
commit 3aeeb6c601
7 changed files with 567 additions and 38 deletions

View file

@ -1,6 +1,5 @@
package it.vfsfitvnm.vimusic
import android.content.ComponentName
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@ -28,14 +27,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.google.common.util.concurrent.ListenableFuture
import com.valentinilk.shimmer.LocalShimmerTheme
import com.valentinilk.shimmer.defaultShimmerTheme
import it.vfsfitvnm.vimusic.enums.ColorPaletteMode
import it.vfsfitvnm.vimusic.services.PlayerService
import it.vfsfitvnm.vimusic.ui.components.BottomSheetMenu
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState
@ -50,16 +45,11 @@ import it.vfsfitvnm.vimusic.utils.*
@ExperimentalAnimationApi
@ExperimentalFoundationApi
class MainActivity : ComponentActivity() {
private lateinit var mediaControllerFuture: ListenableFuture<MediaController>
private var uri by mutableStateOf<Uri?>(null, neverEqualPolicy())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val sessionToken = SessionToken(this, ComponentName(this, PlayerService::class.java))
mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
uri = intent?.data
setContent {
@ -122,7 +112,7 @@ class MainActivity : ComponentActivity() {
LocalColorPalette provides colorPalette,
LocalShimmerTheme provides shimmerTheme,
LocalTypography provides rememberTypography(colorPalette.text),
LocalYoutubePlayer provides rememberYoutubePlayer(mediaControllerFuture),
LocalYoutubePlayer provides rememberYoutubePlayer((application as MainApplication).mediaControllerFuture),
LocalMenuState provides rememberMenuState(),
LocalHapticFeedback provides rememberHapticFeedback()
) {
@ -160,9 +150,4 @@ class MainActivity : ComponentActivity() {
super.onNewIntent(intent)
uri = intent?.data
}
override fun onDestroy() {
MediaController.releaseFuture(mediaControllerFuture)
super.onDestroy()
}
}

View file

@ -1,16 +1,31 @@
package it.vfsfitvnm.vimusic
import android.app.Application
import android.content.ComponentName
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.disk.DiskCache
import com.google.common.util.concurrent.ListenableFuture
import it.vfsfitvnm.vimusic.services.PlayerService
import it.vfsfitvnm.vimusic.utils.preferences
@ExperimentalAnimationApi
@ExperimentalFoundationApi
class MainApplication : Application(), ImageLoaderFactory {
lateinit var mediaControllerFuture: ListenableFuture<MediaController>
override fun onCreate() {
super.onCreate()
DatabaseInitializer()
val sessionToken = SessionToken(this, ComponentName(this, PlayerService::class.java))
mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
}
override fun newImageLoader(): ImageLoader {

View file

@ -10,6 +10,7 @@ import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.ExperimentalAnimationApi
@ -46,6 +47,7 @@ import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.Outcome
import kotlinx.coroutines.*
import kotlin.math.roundToInt
import kotlin.system.exitProcess
val StartRadioCommand = SessionCommand("StartRadioCommand", Bundle.EMPTY)
@ -60,6 +62,11 @@ val SetSkipSilenceCommand = SessionCommand("SetSkipSilenceCommand", Bundle.EMPTY
val GetAudioSessionIdCommand = SessionCommand("GetAudioSessionIdCommand", Bundle.EMPTY)
val SetSleepTimerCommand = SessionCommand("SetSleepTimerCommand", Bundle.EMPTY)
val GetSleepTimerMillisLeftCommand = SessionCommand("GetSleepTimerMillisLeftCommand", Bundle.EMPTY)
val CancelSleepTimerCommand = SessionCommand("CancelSleepTimerCommand", Bundle.EMPTY)
@ExperimentalAnimationApi
@ExperimentalFoundationApi
class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
@ -70,6 +77,9 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
companion object {
private const val NotificationId = 1001
private const val NotificationChannelId = "default_channel_id"
private const val SleepTimerNotificationId = 1002
private const val SleepTimerNotificationChannelId = "sleep_timer_channel_id"
}
private lateinit var cache: SimpleCache
@ -86,6 +96,9 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
private var radio: YoutubePlayer.Radio? = null
private var sleepTimerJob: Job? = null
private var sleepTimerRealtime: Long? = null
private val coroutineScope = CoroutineScope(Dispatchers.IO) + Job()
override fun onCreate() {
@ -112,8 +125,6 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
)
.build()
player.repeatMode = preferences.repeatMode
player.skipSilenceEnabled = preferences.skipSilence
player.playWhenReady = true
@ -151,6 +162,9 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
.add(DeleteSongCacheCommand)
.add(SetSkipSilenceCommand)
.add(GetAudioSessionIdCommand)
.add(SetSleepTimerCommand)
.add(GetSleepTimerMillisLeftCommand)
.add(CancelSleepTimerCommand)
.build()
val playerCommands = Player.Commands.Builder().addAllCommands().build()
return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands)
@ -205,6 +219,39 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
)
)
}
SetSleepTimerCommand -> {
val delayMillis = args.getLong("delayMillis", 2000)
sleepTimerJob = coroutineScope.launch {
sleepTimerRealtime = SystemClock.elapsedRealtime() + delayMillis
delay(delayMillis)
withContext(Dispatchers.Main) {
val notification = NotificationCompat.Builder(this@PlayerService, SleepTimerNotificationChannelId)
.setContentTitle("Sleep timer ended")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setShowWhen(true)
.setSmallIcon(R.drawable.app_icon)
.build()
notificationManager.notify(SleepTimerNotificationId, notification)
}
exitProcess(0)
}
}
GetSleepTimerMillisLeftCommand -> {
return Futures.immediateFuture(sleepTimerRealtime?.let {
(SessionResult(SessionResult.RESULT_SUCCESS, bundleOf("millisLeft" to it - SystemClock.elapsedRealtime())))
} ?: SessionResult(SessionResult.RESULT_ERROR_INVALID_STATE))
}
CancelSleepTimerCommand -> {
sleepTimerJob?.cancel()
sleepTimerJob = null
sleepTimerRealtime = null
}
}
return super.onCustomCommand(session, controller, customCommand, args)
@ -345,14 +392,17 @@ class PlayerService : MediaSessionService(), MediaSession.MediaItemFiller,
private fun createNotificationChannel() {
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Util.SDK_INT >= 26 && notificationManager.getNotificationChannel(NotificationChannelId) == null) {
notificationManager.createNotificationChannel(
NotificationChannel(
NotificationChannelId,
getString(R.string.default_notification_channel_name),
NotificationManager.IMPORTANCE_LOW
)
)
if (Util.SDK_INT < 26) return
with(notificationManager) {
if (getNotificationChannel(NotificationChannelId) == null) {
createNotificationChannel(NotificationChannel(NotificationChannelId, getString(R.string.default_notification_channel_name), NotificationManager.IMPORTANCE_LOW))
}
if (getNotificationChannel(SleepTimerNotificationChannelId) == null) {
createNotificationChannel(NotificationChannel(SleepTimerNotificationChannelId, "Sleep timer", NotificationManager.IMPORTANCE_DEFAULT))
}
}
}

View file

@ -35,6 +35,7 @@ fun ChunkyButton(
@DrawableRes icon: Int? = null,
shape: Shape = RoundedCornerShape(16.dp),
colorFilter: ColorFilter = ColorFilter.tint(rippleColor),
isEnabled: Boolean = true,
onMore: (() -> Unit)? = null
) {
Row(
@ -46,6 +47,7 @@ fun ChunkyButton(
.clickable(
indication = rememberRipple(bounded = true, color = rippleColor),
interactionSource = remember { MutableInteractionSource() },
enabled = isEnabled,
onClick = onClick
)
.padding(horizontal = 24.dp, vertical = 16.dp)

View file

@ -0,0 +1,306 @@
package it.vfsfitvnm.vimusic.ui.components
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.center
import androidx.compose.ui.util.lerp
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
@Composable
fun Pager(
selectedIndex: Int,
onSelectedIndex: (Int) -> Unit,
modifier: Modifier = Modifier,
orientation: Orientation = Orientation.Horizontal,
alignment: Alignment = Alignment.Center,
transformer: PagerTransformer = PagerTransformer.Default,
content: @Composable () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val velocityTracker = remember {
VelocityTracker()
}
val state = remember {
Animatable(0f)
}
var steps by remember {
mutableStateOf(emptyList<Int>())
}
Layout(
modifier = modifier
.clipToBounds()
.pointerInput(Unit) {
val function = when (orientation) {
Orientation.Vertical -> ::detectVerticalDragGestures
Orientation.Horizontal -> ::detectHorizontalDragGestures
}
function(
{},
{
val velocity = -velocityTracker.calculateVelocity().x
val initialTargetValue = splineBasedDecay<Float>(this).calculateTargetValue(state.value, velocity)
velocityTracker.resetTracking()
val (targetValue, newSelectedIndex) = run {
for (i in 1..steps.lastIndex) {
val current = steps[i]
val previous = steps[i - 1]
val currentDelta = current - initialTargetValue
val previousDelta = initialTargetValue - previous
return@run when {
currentDelta >= 0 && previousDelta > 0 -> if (currentDelta < previousDelta) {
current to i
} else {
previous to i - 1
}
previousDelta <= 0 -> previous to i - 1
else -> continue
}
}
steps.last() to steps.lastIndex
}
coroutineScope.launch {
state.animateTo(
targetValue = targetValue.toFloat(),
initialVelocity = velocity,
)
}
onSelectedIndex(newSelectedIndex)
},
{},
{ change, dragAmount ->
coroutineScope.launch {
state.snapTo(state.value - dragAmount)
}
velocityTracker.addPosition(change.uptimeMillis, change.position)
change.consume()
},
)
},
content = content
) { measurables, constraints ->
val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val placeables = measurables.map {
it.measure(childConstraints)
}
var acc = 0
steps = placeables.map {
val dim = when (orientation) {
Orientation.Horizontal -> it.width
Orientation.Vertical -> it.height
}
val step = acc + dim / 2
acc += dim
step
}.also {
if (steps.isEmpty()) {
coroutineScope.launch {
state.animateTo(it[selectedIndex].toFloat())
}
}
}
state.updateBounds(lowerBound = steps.first().toFloat(), upperBound = steps.last().toFloat())
val layoutDimension = IntSize(
width = if (constraints.minWidth > 0 || placeables.isEmpty()) {
constraints.minWidth
} else {
placeables.maxOf {
it.width
}
},
height = if (constraints.minHeight > 0 || placeables.isEmpty()) {
constraints.minHeight
} else {
placeables.maxOf {
it.height
}
}
)
val center = when (orientation) {
Orientation.Horizontal -> layoutDimension.center.x
Orientation.Vertical -> layoutDimension.center.y
}
layout(
width = layoutDimension.width,
height = layoutDimension.height
) {
var position = center - state.value.toInt()
for (placeable in placeables) {
val otherPosition = alignment.align(
size = IntSize(
width = placeable.width,
height = placeable.height
),
space = layoutDimension,
layoutDirection = layoutDirection
).let {
when (orientation) {
Orientation.Horizontal -> it.y
Orientation.Vertical -> it.x
}
}
val placeablePosition = when (orientation) {
Orientation.Horizontal -> IntOffset(position, otherPosition)
Orientation.Vertical -> IntOffset(otherPosition, position)
}
placeable.placeWithLayer(position = placeablePosition) {
with(transformer) {
val size = when (orientation) {
Orientation.Horizontal -> placeable.width
Orientation.Vertical -> placeable.height
}.toFloat()
val offset = (center - (position + size / 2)).absoluteValue / size
apply(distance = offset)
}
}
position += when (orientation) {
Orientation.Horizontal -> placeable.width
Orientation.Vertical -> placeable.height
}
}
}
}
}
// Cannot inline: https://issuetracker.google.com/issues/204897513
@Composable
fun <T> ItemPager(
items: List<T>,
selectedIndex: Int,
onSelectedIndex: (Int) -> Unit,
modifier: Modifier = Modifier,
orientation: Orientation = Orientation.Horizontal,
alignment: Alignment = Alignment.Center,
transformer: PagerTransformer = PagerTransformer.Default,
content: @Composable (item: T) -> Unit
) {
Pager(
modifier = modifier,
selectedIndex = selectedIndex,
onSelectedIndex = onSelectedIndex,
orientation = orientation,
alignment = alignment,
transformer = transformer,
) {
for (item in items) {
content(item)
}
}
}
// Cannot inline: https://issuetracker.google.com/issues/204897513
@Composable
fun <T> ItemPager(
items: List<T>,
selectedValue: T,
onSelectedValue: (T) -> Unit,
modifier: Modifier = Modifier,
orientation: Orientation = Orientation.Horizontal,
alignment: Alignment = Alignment.Center,
transformer: PagerTransformer = PagerTransformer.Default,
content: @Composable (item: T) -> Unit
) {
Pager(
modifier = modifier,
selectedIndex = items.indexOf(selectedValue).coerceAtLeast(0),
onSelectedIndex = {
onSelectedValue(items[it])
},
orientation = orientation,
alignment = alignment,
transformer = transformer,
) {
for (item in items) {
content(item)
}
}
}
// Cannot inline: https://issuetracker.google.com/issues/204897513
@Composable
fun <T : Enum<T>> EnumPager(
value: T,
onSelectedValue: (T) -> Unit,
modifier: Modifier = Modifier,
orientation: Orientation = Orientation.Horizontal,
alignment: Alignment = Alignment.Center,
transformer: PagerTransformer = PagerTransformer.Default,
content: @Composable (item: T) -> Unit
) {
val items = remember {
value.declaringClass.enumConstants!!
}
Pager(
modifier = modifier,
selectedIndex = value.ordinal,
onSelectedIndex = {
onSelectedValue(items[it])
},
orientation = orientation,
alignment = alignment,
transformer = transformer,
) {
for (item in items) {
content(item)
}
}
}
@Immutable
fun interface PagerTransformer {
fun GraphicsLayerScope.apply(distance: Float)
companion object {
@Stable
val Empty = PagerTransformer {}
@Stable
val Default = PagerTransformer {
val value = 1f - it.coerceIn(0f, 1f)
lerp(start = 0.85f, stop = 1f, fraction = value).also { scale ->
scaleX = scale
scaleY = scale
}
alpha = lerp(start = 0.5f, stop = 1f, fraction = value)
}
}
}

View file

@ -3,15 +3,16 @@ package it.vfsfitvnm.vimusic.ui.screens.settings
import android.content.Intent
import android.media.audiofx.AudioEffect
import android.os.Bundle
import android.text.format.DateUtils
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
@ -19,18 +20,22 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.media3.common.C
import androidx.media3.session.SessionResult
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.services.GetAudioSessionIdCommand
import it.vfsfitvnm.vimusic.services.SetSkipSilenceCommand
import it.vfsfitvnm.vimusic.services.*
import it.vfsfitvnm.vimusic.ui.components.ChunkyButton
import it.vfsfitvnm.vimusic.ui.components.Pager
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
import it.vfsfitvnm.vimusic.ui.components.themed.DefaultDialog
import it.vfsfitvnm.vimusic.ui.screens.*
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
import it.vfsfitvnm.vimusic.utils.LocalPreferences
import it.vfsfitvnm.vimusic.utils.LocalYoutubePlayer
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.isActive
@ExperimentalAnimationApi
@ -65,14 +70,12 @@ fun PlayerSettingsScreen() {
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
}
val audioSessionId by produceState(initialValue = C.AUDIO_SESSION_ID_UNSET) {
val audioSessionId by produceState(initialValue = C.AUDIO_SESSION_ID_UNSET, mediaController) {
val hasEqualizer = context.packageManager.resolveActivity(
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL),
0
) != null
println("hasEqualizer? $hasEqualizer")
if (hasEqualizer) {
value =
mediaController?.sendCustomCommand(GetAudioSessionIdCommand, Bundle.EMPTY)
@ -81,6 +84,138 @@ fun PlayerSettingsScreen() {
}
}
var sleepTimerMillisLeft by remember {
mutableStateOf<Long?>(null)
}
LaunchedEffect(mediaController) {
while (isActive) {
println("mediaController: $mediaController")
sleepTimerMillisLeft =
mediaController?.syncCommand(GetSleepTimerMillisLeftCommand)
?.takeIf { it.resultCode == SessionResult.RESULT_SUCCESS }
?.extras?.getLong("millisLeft")
delay(1000)
}
}
var isShowingSleepTimerDialog by remember {
mutableStateOf(false)
}
if (isShowingSleepTimerDialog) {
if (sleepTimerMillisLeft != null) {
ConfirmationDialog(
text = "Do you want to stop the sleep timer?",
cancelText = "No",
confirmText = "Stop",
onDismiss = {
isShowingSleepTimerDialog = false
},
onConfirm = {
mediaController?.syncCommand(CancelSleepTimerCommand)
sleepTimerMillisLeft = null
}
)
} else {
DefaultDialog(
onDismiss = {
isShowingSleepTimerDialog = false
},
modifier = Modifier
) {
var hours by remember {
mutableStateOf(0)
}
var minutes by remember {
mutableStateOf(0)
}
BasicText(
text = "Set sleep timer",
style = typography.s.semiBold,
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 24.dp)
)
Row(
modifier = Modifier
.padding(vertical = 16.dp)
) {
Pager(
selectedIndex = hours,
onSelectedIndex = {
hours = it
},
orientation = Orientation.Vertical,
modifier = Modifier
.padding(horizontal = 8.dp)
.height(72.dp)
) {
repeat(12) {
BasicText(
text = "$it h",
style = typography.xs.semiBold
)
}
}
Pager(
selectedIndex = minutes,
onSelectedIndex = {
minutes = it
},
orientation = Orientation.Vertical,
modifier = Modifier
.padding(horizontal = 8.dp)
.height(72.dp)
) {
repeat(4) {
BasicText(
text = "${it * 15} m",
style = typography.xs.semiBold
)
}
}
}
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxWidth()
) {
ChunkyButton(
backgroundColor = colorPalette.lightBackground,
text = "Cancel",
textStyle = typography.xs.semiBold,
shape = RoundedCornerShape(36.dp),
onClick = { isShowingSleepTimerDialog = false }
)
ChunkyButton(
backgroundColor = colorPalette.primaryContainer,
text = "Set",
textStyle = typography.xs.semiBold.color(colorPalette.onPrimaryContainer),
shape = RoundedCornerShape(36.dp),
isEnabled = hours > 0 || minutes > 0,
onClick = {
mediaController?.syncCommand(
SetSleepTimerCommand,
bundleOf("delayMillis" to (hours * 60 + minutes * 15) * 60 * 1000L)
)
sleepTimerMillisLeft =
mediaController?.syncCommand(GetSleepTimerMillisLeftCommand)?.extras?.getLong(
"millisLeft"
)
isShowingSleepTimerDialog = false
}
)
}
}
}
}
Column(
modifier = Modifier
.background(colorPalette.background)
@ -144,12 +279,24 @@ fun PlayerSettingsScreen() {
activityResultLauncher.launch(
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
putExtra(
AudioEffect.EXTRA_CONTENT_TYPE,
AudioEffect.CONTENT_TYPE_MUSIC
)
}
)
},
isEnabled = audioSessionId != C.AUDIO_SESSION_ID_UNSET && audioSessionId != AudioEffect.ERROR_BAD_VALUE
)
SettingsEntry(
title = "Sleep timer",
text = sleepTimerMillisLeft?.let { "${DateUtils.formatElapsedTime(it / 1000)} left" }
?: "Stop the music after a period of time",
onClick = {
isShowingSleepTimerDialog = true
}
)
}
}
}

View file

@ -0,0 +1,24 @@
package it.vfsfitvnm.vimusic.utils
import android.os.Bundle
import androidx.media3.session.MediaController
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.google.common.util.concurrent.MoreExecutors
import kotlinx.coroutines.guava.await
suspend fun MediaController.send(command: SessionCommand, args: Bundle = Bundle.EMPTY): SessionResult {
return sendCustomCommand(command, args).await()
}
fun MediaController.command(command: SessionCommand, args: Bundle = Bundle.EMPTY, listener: ((SessionResult) -> Unit)? = null) {
val future = sendCustomCommand(command, args)
listener?.let {
future.addListener({ it(future.get()) }, MoreExecutors.directExecutor())
}
}
fun MediaController.syncCommand(command: SessionCommand, args: Bundle = Bundle.EMPTY): SessionResult {
return sendCustomCommand(command, args).get()
}