Show custom actions in music widget
This commit is contained in:
parent
c8d6da41dd
commit
e4a599f234
@ -1,5 +1,7 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.widgets.music
|
package de.mm20.launcher2.ui.launcher.widgets.music
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.media.session.PlaybackState.CustomAction
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||||
@ -27,14 +29,18 @@ import androidx.compose.foundation.layout.wrapContentHeight
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Audiotrack
|
import androidx.compose.material.icons.rounded.Audiotrack
|
||||||
|
import androidx.compose.material.icons.rounded.MoreVert
|
||||||
import androidx.compose.material.icons.rounded.MusicNote
|
import androidx.compose.material.icons.rounded.MusicNote
|
||||||
import androidx.compose.material.icons.rounded.SkipNext
|
import androidx.compose.material.icons.rounded.SkipNext
|
||||||
import androidx.compose.material.icons.rounded.SkipPrevious
|
import androidx.compose.material.icons.rounded.SkipPrevious
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.FilledTonalIconButton
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.PlainTooltipBox
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -55,8 +61,11 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import coil.request.ImageRequest
|
||||||
import de.mm20.launcher2.music.PlaybackState
|
import de.mm20.launcher2.music.PlaybackState
|
||||||
import de.mm20.launcher2.music.SupportedActions
|
import de.mm20.launcher2.music.SupportedActions
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
@ -65,6 +74,7 @@ import de.mm20.launcher2.ui.ktx.conditional
|
|||||||
import de.mm20.launcher2.ui.launcher.transitions.HandleHomeTransition
|
import de.mm20.launcher2.ui.launcher.transitions.HandleHomeTransition
|
||||||
import de.mm20.launcher2.ui.launcher.transitions.HomeTransitionParams
|
import de.mm20.launcher2.ui.launcher.transitions.HomeTransitionParams
|
||||||
import de.mm20.launcher2.ui.locals.LocalWindowSize
|
import de.mm20.launcher2.ui.locals.LocalWindowSize
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MusicWidget() {
|
fun MusicWidget() {
|
||||||
@ -251,7 +261,8 @@ fun MusicWidget() {
|
|||||||
Image(
|
Image(
|
||||||
bitmap = art.asImageBitmap(),
|
bitmap = art.asImageBitmap(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(96.dp),
|
.size(96.dp)
|
||||||
|
.clip(shape),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
@ -316,10 +327,122 @@ fun MusicWidget() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CustomActions(
|
||||||
|
actions = supportedActions,
|
||||||
|
onActionSelected = {
|
||||||
|
viewModel.performCustomAction(it)
|
||||||
|
},
|
||||||
|
playerPackage = viewModel.currentPlayerPackage,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CustomActions(
|
||||||
|
actions: SupportedActions,
|
||||||
|
onActionSelected: (CustomAction) -> Unit,
|
||||||
|
playerPackage: String?
|
||||||
|
) {
|
||||||
|
val usedSlots = 1 + (if (actions.skipToPrevious) 1 else 0) + (if (actions.skipToNext) 1 else 0)
|
||||||
|
val slots = 5 - usedSlots
|
||||||
|
|
||||||
|
for (i in 0 until min(actions.customActions.size, slots - 1)) {
|
||||||
|
val action = actions.customActions[i]
|
||||||
|
PlainTooltipBox(tooltip = { Text(action.name.toString()) }) {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.tooltipAnchor(),
|
||||||
|
onClick = {
|
||||||
|
onActionSelected(action)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
CustomActionIcon(action, playerPackage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slots < actions.customActions.size) {
|
||||||
|
var showOverflowMenu by remember { mutableStateOf(false) }
|
||||||
|
Box {
|
||||||
|
IconButton(onClick = { showOverflowMenu = true }) {
|
||||||
|
Icon(imageVector = Icons.Rounded.MoreVert, contentDescription = null)
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showOverflowMenu,
|
||||||
|
onDismissRequest = { showOverflowMenu = false },
|
||||||
|
) {
|
||||||
|
for (i in slots - 1 until actions.customActions.size) {
|
||||||
|
val action = actions.customActions[i]
|
||||||
|
DropdownMenuItem(
|
||||||
|
leadingIcon = {
|
||||||
|
CustomActionIcon(action, playerPackage)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = action.name.toString(),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
showOverflowMenu = false
|
||||||
|
onActionSelected(action)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (slots == actions.customActions.size) {
|
||||||
|
val action = actions.customActions.last()
|
||||||
|
PlainTooltipBox(tooltip = { Text(action.name.toString()) }) {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.tooltipAnchor(),
|
||||||
|
onClick = {
|
||||||
|
onActionSelected(action)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
CustomActionIcon(action, playerPackage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CustomActionIcon(action: CustomAction, playerPackage: String?) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val resources = remember(playerPackage) {
|
||||||
|
playerPackage?.let {
|
||||||
|
context.packageManager.getResourcesForApplication(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val drawable = remember(action, resources) {
|
||||||
|
if (resources != null) {
|
||||||
|
try {
|
||||||
|
ResourcesCompat.getDrawable(
|
||||||
|
resources, action.icon, null
|
||||||
|
)
|
||||||
|
} catch (e: Resources.NotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val painter = rememberAsyncImagePainter(
|
||||||
|
ImageRequest.Builder(context)
|
||||||
|
.data(drawable)
|
||||||
|
.crossfade(false)
|
||||||
|
.placeholder(drawable)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
painter = painter,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NoData() {
|
fun NoData() {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@ -66,6 +66,10 @@ class MusicWidgetVM: ViewModel(), KoinComponent {
|
|||||||
musicService.openPlayerChooser(context)
|
musicService.openPlayerChooser(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun performCustomAction(action: CustomAction) {
|
||||||
|
musicService.performCustomAction(action)
|
||||||
|
}
|
||||||
|
|
||||||
fun requestPermission(context: AppCompatActivity) {
|
fun requestPermission(context: AppCompatActivity) {
|
||||||
permissionsManager.requestPermission(context, PermissionGroup.Notifications)
|
permissionsManager.requestPermission(context, PermissionGroup.Notifications)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import android.content.pm.PackageManager
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.MediaMetadata
|
import android.media.MediaMetadata
|
||||||
|
import android.media.Rating
|
||||||
import android.media.session.MediaController
|
import android.media.session.MediaController
|
||||||
import android.media.session.MediaSession
|
import android.media.session.MediaSession
|
||||||
import android.media.session.PlaybackState.CustomAction
|
import android.media.session.PlaybackState.CustomAction
|
||||||
@ -67,6 +68,7 @@ interface MusicService {
|
|||||||
fun play()
|
fun play()
|
||||||
fun togglePause()
|
fun togglePause()
|
||||||
fun seekTo(position: Long)
|
fun seekTo(position: Long)
|
||||||
|
fun performCustomAction(action: CustomAction)
|
||||||
fun openPlayer(): PendingIntent?
|
fun openPlayer(): PendingIntent?
|
||||||
|
|
||||||
fun openPlayerChooser(context: Context)
|
fun openPlayerChooser(context: Context)
|
||||||
@ -552,6 +554,13 @@ internal class MusicServiceImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun performCustomAction(action: CustomAction) {
|
||||||
|
scope.launch {
|
||||||
|
val controller = currentMediaController.firstOrNull()
|
||||||
|
controller?.transportControls?.sendCustomAction(action.action, action.extras)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getMusicApps(): Set<String> {
|
private fun getMusicApps(): Set<String> {
|
||||||
// List of known music apps that don't have the correct intent filter
|
// List of known music apps that don't have the correct intent filter
|
||||||
val apps = mutableSetOf(
|
val apps = mutableSetOf(
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package de.mm20.launcher2.music
|
|||||||
|
|
||||||
import android.media.session.PlaybackState
|
import android.media.session.PlaybackState
|
||||||
import android.media.session.PlaybackState.CustomAction
|
import android.media.session.PlaybackState.CustomAction
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
data class SupportedActions(
|
data class SupportedActions(
|
||||||
val stop: Boolean = false,
|
val stop: Boolean = false,
|
||||||
@ -24,5 +26,14 @@ data class SupportedActions(
|
|||||||
setPlaybackSpeed = actions?.and(PlaybackState.ACTION_SET_PLAYBACK_SPEED) == PlaybackState.ACTION_SET_PLAYBACK_SPEED,
|
setPlaybackSpeed = actions?.and(PlaybackState.ACTION_SET_PLAYBACK_SPEED) == PlaybackState.ACTION_SET_PLAYBACK_SPEED,
|
||||||
setRating = actions?.and(PlaybackState.ACTION_SET_RATING) == PlaybackState.ACTION_SET_RATING,
|
setRating = actions?.and(PlaybackState.ACTION_SET_RATING) == PlaybackState.ACTION_SET_RATING,
|
||||||
customActions = customActions ?: emptyList(),
|
customActions = customActions ?: emptyList(),
|
||||||
)
|
) {
|
||||||
|
for (action in customActions ?: emptyList()) {
|
||||||
|
Log.d("MM20", action.action.toString())
|
||||||
|
val extras = action.extras ?: Bundle.EMPTY
|
||||||
|
val keySet = extras.keySet()
|
||||||
|
for (key in keySet) {
|
||||||
|
Log.d("MM20", "$key: ${extras.get(key)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user