Show custom actions in music widget

This commit is contained in:
MM20 2023-02-14 16:59:19 +01:00
parent c8d6da41dd
commit e4a599f234
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
4 changed files with 149 additions and 2 deletions

View File

@ -1,5 +1,7 @@
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.compose.animation.AnimatedVisibility
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.material.icons.Icons
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.SkipNext
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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltipBox
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.SupportedActions
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.HomeTransitionParams
import de.mm20.launcher2.ui.locals.LocalWindowSize
import kotlin.math.min
@Composable
fun MusicWidget() {
@ -251,7 +261,8 @@ fun MusicWidget() {
Image(
bitmap = art.asImageBitmap(),
modifier = Modifier
.size(96.dp),
.size(96.dp)
.clip(shape),
contentDescription = null,
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
fun NoData() {
Row(

View File

@ -66,6 +66,10 @@ class MusicWidgetVM: ViewModel(), KoinComponent {
musicService.openPlayerChooser(context)
}
fun performCustomAction(action: CustomAction) {
musicService.performCustomAction(action)
}
fun requestPermission(context: AppCompatActivity) {
permissionsManager.requestPermission(context, PermissionGroup.Notifications)
}

View File

@ -8,6 +8,7 @@ import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.media.AudioManager
import android.media.MediaMetadata
import android.media.Rating
import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.PlaybackState.CustomAction
@ -67,6 +68,7 @@ interface MusicService {
fun play()
fun togglePause()
fun seekTo(position: Long)
fun performCustomAction(action: CustomAction)
fun openPlayer(): PendingIntent?
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> {
// List of known music apps that don't have the correct intent filter
val apps = mutableSetOf(

View File

@ -2,6 +2,8 @@ package de.mm20.launcher2.music
import android.media.session.PlaybackState
import android.media.session.PlaybackState.CustomAction
import android.os.Bundle
import android.util.Log
data class SupportedActions(
val stop: Boolean = false,
@ -24,5 +26,14 @@ data class SupportedActions(
setPlaybackSpeed = actions?.and(PlaybackState.ACTION_SET_PLAYBACK_SPEED) == PlaybackState.ACTION_SET_PLAYBACK_SPEED,
setRating = actions?.and(PlaybackState.ACTION_SET_RATING) == PlaybackState.ACTION_SET_RATING,
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)}")
}
}
}
}