From e4a599f234d15432f541356254f899a0da6dc6b4 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Tue, 14 Feb 2023 16:59:19 +0100 Subject: [PATCH] Show custom actions in music widget --- .../ui/launcher/widgets/music/MusicWidget.kt | 125 +++++++++++++++++- .../launcher/widgets/music/MusicWidgetVM.kt | 4 + .../de/mm20/launcher2/music/MusicService.kt | 9 ++ .../mm20/launcher2/music/SupportedActions.kt | 13 +- 4 files changed, 149 insertions(+), 2 deletions(-) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt index ba3e1715..65f91b53 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt @@ -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( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidgetVM.kt index ee333f2a..f13b6c0d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidgetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidgetVM.kt @@ -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) } diff --git a/services/music/src/main/java/de/mm20/launcher2/music/MusicService.kt b/services/music/src/main/java/de/mm20/launcher2/music/MusicService.kt index d575184a..bcdbda71 100644 --- a/services/music/src/main/java/de/mm20/launcher2/music/MusicService.kt +++ b/services/music/src/main/java/de/mm20/launcher2/music/MusicService.kt @@ -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 { // List of known music apps that don't have the correct intent filter val apps = mutableSetOf( diff --git a/services/music/src/main/java/de/mm20/launcher2/music/SupportedActions.kt b/services/music/src/main/java/de/mm20/launcher2/music/SupportedActions.kt index e32f785a..b69ca3b5 100644 --- a/services/music/src/main/java/de/mm20/launcher2/music/SupportedActions.kt +++ b/services/music/src/main/java/de/mm20/launcher2/music/SupportedActions.kt @@ -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)}") + } + } + } } \ No newline at end of file