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 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(

View File

@ -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)
} }

View File

@ -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(

View File

@ -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)}")
}
}
}
} }