diff --git a/app/src/main/java/com/mediacontrol/floatingwidget/MainActivity.kt b/app/src/main/java/com/mediacontrol/floatingwidget/MainActivity.kt index 110461a..a4b7725 100644 --- a/app/src/main/java/com/mediacontrol/floatingwidget/MainActivity.kt +++ b/app/src/main/java/com/mediacontrol/floatingwidget/MainActivity.kt @@ -107,6 +107,7 @@ class MainActivity : AppCompatActivity() { onSetHorizontalOffsetPreset = ::setHorizontalOffsetPreset, onSetPersistentOverlayEnabled = ::setPersistentOverlayEnabled, onSetLowQualityThumbnailFallbackEnabled = ::setThumbnailEnabled, + onSetTripleTapToToggle = ::setTripleTapToToggle, onStartOverlay = ::startOverlay, onStopOverlay = ::stopOverlay, onDispatchPrevious = ::dispatchPrevious, @@ -195,6 +196,10 @@ class MainActivity : AppCompatActivity() { widgetConfigStateHolder.setLowQualityThumbnailFallbackEnabled(enabled) } + private fun setTripleTapToToggle(enabled: Boolean) { + widgetConfigStateHolder.setTripleTapToToggle(enabled) + } + private fun setHorizontalOffsetPreset(xOffsetDp: Int) { widgetConfigStateHolder.setHorizontalOffsetDp(xOffsetDp) } diff --git a/app/src/main/java/com/mediacontrol/floatingwidget/media/AndroidMediaSessionRepository.kt b/app/src/main/java/com/mediacontrol/floatingwidget/media/AndroidMediaSessionRepository.kt index e09a969..abb7ecb 100644 --- a/app/src/main/java/com/mediacontrol/floatingwidget/media/AndroidMediaSessionRepository.kt +++ b/app/src/main/java/com/mediacontrol/floatingwidget/media/AndroidMediaSessionRepository.kt @@ -255,12 +255,14 @@ class AndroidMediaSessionRepository( displayTitle = controller.metadata?.getText(MediaMetadata.METADATA_KEY_DISPLAY_TITLE), title = controller.metadata?.getText(MediaMetadata.METADATA_KEY_TITLE) ) + val artist = controller.metadata?.getText(MediaMetadata.METADATA_KEY_ARTIST)?.toString()?.trim()?.takeIf { it.isNotEmpty() } val artworkCandidates = resolveArtworkCandidates(controller) val sessionId = "${controller.packageName}:${controller.sessionToken}" val playbackState = controller.playbackState ?: return buildMediaSessionState( sessionId = sessionId, title = title, + artist = artist, artworkCandidates = artworkCandidates, playbackStatus = null, supportedActions = null @@ -269,6 +271,7 @@ class AndroidMediaSessionRepository( return buildMediaSessionState( sessionId = sessionId, title = title, + artist = artist, artworkCandidates = artworkCandidates, playbackStatus = playbackState.state.toPlaybackStatus(), supportedActions = playbackState.actions.toSupportedCommands() @@ -318,6 +321,7 @@ class AndroidMediaSessionRepository( fun buildMediaSessionState( sessionId: String, title: String?, + artist: String?, artworkCandidates: List = emptyList(), playbackStatus: PlaybackStatus?, supportedActions: Set? @@ -326,6 +330,7 @@ class AndroidMediaSessionRepository( ?: return MediaSessionState.Limited( reason = MediaSessionLimitReason.PlaybackStateUnknown, title = title, + artist = artist, artworkCandidates = artworkCandidates, supportedActions = emptySet() ) @@ -336,6 +341,7 @@ class AndroidMediaSessionRepository( MediaSessionState.Limited( reason = MediaSessionLimitReason.MissingTransportControls, title = title, + artist = artist, artworkCandidates = artworkCandidates, supportedActions = emptySet() ) @@ -343,6 +349,7 @@ class AndroidMediaSessionRepository( MediaSessionState.Active( sessionId = sessionId, title = title, + artist = artist, artworkCandidates = artworkCandidates, supportedActions = resolvedSupportedActions, playbackStatus = resolvedPlaybackStatus diff --git a/app/src/main/java/com/mediacontrol/floatingwidget/model/MediaModels.kt b/app/src/main/java/com/mediacontrol/floatingwidget/model/MediaModels.kt index 7ee2883..ee6ee92 100644 --- a/app/src/main/java/com/mediacontrol/floatingwidget/model/MediaModels.kt +++ b/app/src/main/java/com/mediacontrol/floatingwidget/model/MediaModels.kt @@ -79,6 +79,7 @@ sealed interface MediaSessionState { data class Active( val sessionId: String, val title: String?, + val artist: String?, val artworkCandidates: List = emptyList(), val supportedActions: Set, val playbackStatus: PlaybackStatus @@ -87,6 +88,7 @@ sealed interface MediaSessionState { data class Limited( val reason: MediaSessionLimitReason, val title: String?, + val artist: String?, val artworkCandidates: List = emptyList(), val supportedActions: Set ) : MediaSessionState @@ -116,6 +118,20 @@ fun MediaSessionState.currentTitle(): String? { } } +fun MediaSessionState.currentDisplayText(): String { + val t = when (this) { + is MediaSessionState.Active -> title.orEmpty() + is MediaSessionState.Limited -> title.orEmpty() + else -> "" + } + val a = when (this) { + is MediaSessionState.Active -> artist + is MediaSessionState.Limited -> artist + else -> null + } + return if (!a.isNullOrBlank() && t.isNotBlank()) "$a - $t" else t +} + fun MediaSessionState.currentArtworkCandidates(): List { return when (this) { is MediaSessionState.Active -> artworkCandidates diff --git a/app/src/main/java/com/mediacontrol/floatingwidget/model/WidgetModels.kt b/app/src/main/java/com/mediacontrol/floatingwidget/model/WidgetModels.kt index ca558b8..7b389ed 100644 --- a/app/src/main/java/com/mediacontrol/floatingwidget/model/WidgetModels.kt +++ b/app/src/main/java/com/mediacontrol/floatingwidget/model/WidgetModels.kt @@ -82,7 +82,8 @@ data class WidgetConfig( val themePreset: WidgetThemePreset = WidgetThemePreset.Dark, val opacity: Float = 1f, val persistentOverlayEnabled: Boolean = true, - val allowLowQualityThumbnailFallback: Boolean = false + val allowLowQualityThumbnailFallback: Boolean = false, + val tripleTapToToggle: Boolean = false ) data class WidgetOverlaySizing( diff --git a/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayHost.kt b/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayHost.kt index 63258a3..57fc760 100644 --- a/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayHost.kt +++ b/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayHost.kt @@ -20,4 +20,6 @@ interface OverlayHost { fun update(viewState: OverlayViewState) fun detach() + + fun setOnToggleWidget(onToggle: () -> Unit) } diff --git a/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayPresentationSpec.kt b/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayPresentationSpec.kt index 702fa59..12e97e9 100644 --- a/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayPresentationSpec.kt +++ b/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayPresentationSpec.kt @@ -4,7 +4,7 @@ import android.view.Gravity import android.widget.LinearLayout import sw2.io.mediafloat.model.DragHandlePlacement import sw2.io.mediafloat.model.MediaSessionState -import sw2.io.mediafloat.model.currentTitle +import sw2.io.mediafloat.model.currentDisplayText import sw2.io.mediafloat.model.WidgetOverlayAppearance internal data class OverlayPresentationSpec( @@ -34,7 +34,7 @@ internal object OverlayPresentationSpecFactory { dragHandlePlacement: DragHandlePlacement ): OverlayPresentationSpec { val sizing = appearance.sizing - val titleText = mediaState.currentTitle().orEmpty() + val titleText = mediaState.currentDisplayText() val titleVisible = true val metrics = OverlayLayoutCalculator.calculate( appearance = appearance, diff --git a/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayService.kt b/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayService.kt index 31971d8..6e16414 100644 --- a/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayService.kt +++ b/app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayService.kt @@ -184,6 +184,13 @@ class OverlayService : Service() { } currentMediaState = mediaRepository.refresh(reason = "overlay_attach") + overlayHost.setOnToggleWidget { + if (runtimeCoordinator.readinessRuntimeState() is sw2.io.mediafloat.model.OverlayRuntimeState.Showing) { + runtimeCoordinator.stopOverlay() + } else { + runtimeCoordinator.startOverlay() + } + } overlayHost.attach( OverlayViewState( config = currentWidgetConfig, diff --git a/app/src/main/java/com/mediacontrol/floatingwidget/overlay/WindowManagerOverlayHost.kt b/app/src/main/java/com/mediacontrol/floatingwidget/overlay/WindowManagerOverlayHost.kt index 50b958b..a172628 100644 --- a/app/src/main/java/com/mediacontrol/floatingwidget/overlay/WindowManagerOverlayHost.kt +++ b/app/src/main/java/com/mediacontrol/floatingwidget/overlay/WindowManagerOverlayHost.kt @@ -71,6 +71,13 @@ class WindowManagerOverlayHost( private var isDragging = false private var appliedDragHandlePlacement: DragHandlePlacement = DragHandlePlacement.Right private var appliedThumbnailSignature: String? = null + private var onToggleWidget: (() -> Unit)? = null + private var lastTapTime: Long = 0 + private var tapCount: Int = 0 + + override fun setOnToggleWidget(onToggle: () -> Unit) { + onToggleWidget = onToggle + } override fun attach(viewState: OverlayViewState) { currentViewState = viewState @@ -213,6 +220,20 @@ class WindowManagerOverlayHost( contentDescription = appContext.getString(R.string.overlay_drag_handle) gravity = Gravity.CENTER setOnTouchListener(DragTouchListener()) + setOnClickListener { + val now = System.currentTimeMillis() + if (lastTapTime > 0 && now - lastTapTime < 500L) { + tapCount++ + if (tapCount >= 3) { + tapCount = 0 + lastTapTime = 0 + onToggleWidget?.invoke() + } + } else { + tapCount = 1 + lastTapTime = now + } + } } } diff --git a/app/src/main/java/com/mediacontrol/floatingwidget/state/WidgetConfigStateHolder.kt b/app/src/main/java/com/mediacontrol/floatingwidget/state/WidgetConfigStateHolder.kt index aed7607..e01a24a 100644 --- a/app/src/main/java/com/mediacontrol/floatingwidget/state/WidgetConfigStateHolder.kt +++ b/app/src/main/java/com/mediacontrol/floatingwidget/state/WidgetConfigStateHolder.kt @@ -69,6 +69,10 @@ class WidgetConfigStateHolder( repository.saveConfig(currentState().config.copy(allowLowQualityThumbnailFallback = enabled)) } + fun setTripleTapToToggle(enabled: Boolean) { + repository.saveConfig(currentState().config.copy(tripleTapToToggle = enabled)) + } + fun savePosition(position: WidgetPosition) { repository.savePosition(position) } diff --git a/app/src/main/java/com/mediacontrol/floatingwidget/ui/AppShell.kt b/app/src/main/java/com/mediacontrol/floatingwidget/ui/AppShell.kt index abe4404..d3311f2 100644 --- a/app/src/main/java/com/mediacontrol/floatingwidget/ui/AppShell.kt +++ b/app/src/main/java/com/mediacontrol/floatingwidget/ui/AppShell.kt @@ -178,6 +178,7 @@ fun AppShell( onSetHorizontalOffsetPreset: (Int) -> Unit = {}, onSetPersistentOverlayEnabled: (Boolean) -> Unit = {}, onSetLowQualityThumbnailFallbackEnabled: (Boolean) -> Unit = {}, + onSetTripleTapToToggle: (Boolean) -> Unit = {}, onStartOverlay: () -> Unit = {}, onStopOverlay: () -> Unit = {}, onDispatchPrevious: () -> Unit = {}, @@ -275,6 +276,7 @@ fun AppShell( onSetHorizontalOffsetPreset = onSetHorizontalOffsetPreset, onSetPersistentOverlayEnabled = onSetPersistentOverlayEnabled, onSetLowQualityThumbnailFallbackEnabled = onSetLowQualityThumbnailFallbackEnabled, + onSetTripleTapToToggle = onSetTripleTapToToggle, onStartOverlay = onStartOverlay, onStopOverlay = onStopOverlay, onDispatchPrevious = onDispatchPrevious, @@ -321,6 +323,7 @@ fun AppShell( onSetHorizontalOffsetPreset = onSetHorizontalOffsetPreset, onSetPersistentOverlayEnabled = onSetPersistentOverlayEnabled, onSetLowQualityThumbnailFallbackEnabled = onSetLowQualityThumbnailFallbackEnabled, + onSetTripleTapToToggle = onSetTripleTapToToggle, onStartOverlay = onStartOverlay, onStopOverlay = onStopOverlay, onDispatchPrevious = onDispatchPrevious, @@ -491,6 +494,7 @@ private fun SectionContent( onSetHorizontalOffsetPreset: (Int) -> Unit, onSetPersistentOverlayEnabled: (Boolean) -> Unit, onSetLowQualityThumbnailFallbackEnabled: (Boolean) -> Unit, + onSetTripleTapToToggle: (Boolean) -> Unit, onStartOverlay: () -> Unit, onStopOverlay: () -> Unit, onDispatchPrevious: () -> Unit, @@ -540,6 +544,7 @@ private fun SectionContent( onSetDragHandlePlacement = onSetDragHandlePlacement, onSetHorizontalOffsetPreset = onSetHorizontalOffsetPreset, onSetLowQualityThumbnailFallbackEnabled = onSetLowQualityThumbnailFallbackEnabled, + onSetTripleTapToToggle = onSetTripleTapToToggle, onStartOverlay = onStartOverlay, onStopOverlay = onStopOverlay, wideLayout = wideLayout @@ -639,6 +644,7 @@ private fun SettingsScreen( onSetDragHandlePlacement: (DragHandlePlacement) -> Unit, onSetHorizontalOffsetPreset: (Int) -> Unit, onSetLowQualityThumbnailFallbackEnabled: (Boolean) -> Unit, + onSetTripleTapToToggle: (Boolean) -> Unit, onStartOverlay: () -> Unit, onStopOverlay: () -> Unit, wideLayout: Boolean @@ -688,6 +694,10 @@ private fun SettingsScreen( config = widgetConfigState.config, onSetEnabled = onSetLowQualityThumbnailFallbackEnabled ) + TripleTapToggleCard( + enabled = widgetConfigState.config.tripleTapToToggle, + onSetEnabled = onSetTripleTapToToggle + ) } } } else { @@ -727,6 +737,10 @@ private fun SettingsScreen( config = widgetConfigState.config, onSetEnabled = onSetLowQualityThumbnailFallbackEnabled ) + TripleTapToggleCard( + enabled = widgetConfigState.config.tripleTapToToggle, + onSetEnabled = onSetTripleTapToToggle + ) } } @@ -2360,6 +2374,47 @@ private fun PermissionItem( } } +@Composable +private fun TripleTapToggleCard( + enabled: Boolean, + onSetEnabled: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = PanelShape, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = "Triple-tap to toggle", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Medium + ) + Text( + text = "Tap the drag handle three times quickly to show or hide the widget.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Switch( + checked = enabled, + onCheckedChange = onSetEnabled + ) + } + } +} + @Composable private fun MediaStatusCard( mediaSummaryState: MediaSummaryState, @@ -3254,6 +3309,7 @@ private fun previewRuntimeState(): OverlayRuntimeState { mediaState = MediaSessionState.Active( sessionId = "preview-session", title = "Velvet City Lights After Midnight Remix", + artist = "Synthwave Collective", artworkCandidates = listOf( MediaArtwork.UriSource( source = MediaArtworkSource.MetadataArtUri, @@ -3273,6 +3329,7 @@ private fun previewMediaSummaryState(): MediaSummaryState { mediaState = MediaSessionState.Active( sessionId = "preview-session", title = "Velvet City Lights After Midnight Remix", + artist = "Synthwave Collective", artworkCandidates = listOf( MediaArtwork.UriSource( source = MediaArtworkSource.MetadataArtUri, diff --git a/app/src/main/java/com/mediacontrol/floatingwidget/widget/WidgetConfigStore.kt b/app/src/main/java/com/mediacontrol/floatingwidget/widget/WidgetConfigStore.kt index 4093bbc..c4e9c82 100644 --- a/app/src/main/java/com/mediacontrol/floatingwidget/widget/WidgetConfigStore.kt +++ b/app/src/main/java/com/mediacontrol/floatingwidget/widget/WidgetConfigStore.kt @@ -59,6 +59,10 @@ class WidgetConfigStore( allowLowQualityThumbnailFallback = storage.getBoolean( KEY_ALLOW_LOW_QUALITY_THUMBNAIL_FALLBACK, WidgetConfig().allowLowQualityThumbnailFallback + ), + tripleTapToToggle = storage.getBoolean( + KEY_TRIPLE_TAP_TO_TOGGLE, + WidgetConfig().tripleTapToToggle ) ) } @@ -73,6 +77,7 @@ class WidgetConfigStore( putInt(KEY_OPACITY_PERCENT, (config.opacity.coerceIn(0.35f, 1f) * 100).roundToInt()) putBoolean(KEY_PERSISTENT_OVERLAY_ENABLED, config.persistentOverlayEnabled) putBoolean(KEY_ALLOW_LOW_QUALITY_THUMBNAIL_FALLBACK, config.allowLowQualityThumbnailFallback) + putBoolean(KEY_TRIPLE_TAP_TO_TOGGLE, config.tripleTapToToggle) } } @@ -90,6 +95,7 @@ class WidgetConfigStore( const val KEY_OPACITY_PERCENT = "opacity_percent" const val KEY_PERSISTENT_OVERLAY_ENABLED = "persistent_overlay_enabled" const val KEY_ALLOW_LOW_QUALITY_THUMBNAIL_FALLBACK = "allow_low_quality_thumbnail_fallback" + const val KEY_TRIPLE_TAP_TO_TOGGLE = "triple_tap_to_toggle" } } diff --git a/app/src/test/java/com/mediacontrol/floatingwidget/media/AndroidMediaSessionRepositoryTest.kt b/app/src/test/java/com/mediacontrol/floatingwidget/media/AndroidMediaSessionRepositoryTest.kt index d073400..b61fa5d 100644 --- a/app/src/test/java/com/mediacontrol/floatingwidget/media/AndroidMediaSessionRepositoryTest.kt +++ b/app/src/test/java/com/mediacontrol/floatingwidget/media/AndroidMediaSessionRepositoryTest.kt @@ -66,6 +66,7 @@ class AndroidMediaSessionRepositoryTest { val state = AndroidMediaSessionRepository.buildMediaSessionState( sessionId = "session-1", title = "Late Night Drive", + artist = null, artworkCandidates = artworkCandidates, playbackStatus = null, supportedActions = null @@ -75,6 +76,7 @@ class AndroidMediaSessionRepositoryTest { MediaSessionState.Limited( reason = MediaSessionLimitReason.PlaybackStateUnknown, title = "Late Night Drive", + artist = null, artworkCandidates = artworkCandidates, supportedActions = emptySet() ), @@ -95,6 +97,7 @@ class AndroidMediaSessionRepositoryTest { val state = AndroidMediaSessionRepository.buildMediaSessionState( sessionId = "session-2", title = "Neon Skyline Avenue", + artist = "Synthwave Collective", artworkCandidates = artworkCandidates, playbackStatus = PlaybackStatus.Playing, supportedActions = setOf(MediaCommand.Previous, MediaCommand.TogglePlayPause, MediaCommand.Next) @@ -104,6 +107,7 @@ class AndroidMediaSessionRepositoryTest { state as MediaSessionState.Active assertEquals("session-2", state.sessionId) assertEquals("Neon Skyline Avenue", state.title) + assertEquals("Synthwave Collective", state.artist) assertEquals(artworkCandidates, state.artworkCandidates) assertEquals(PlaybackStatus.Playing, state.playbackStatus) assertEquals( diff --git a/app/src/test/java/com/mediacontrol/floatingwidget/model/MediaModelsTest.kt b/app/src/test/java/com/mediacontrol/floatingwidget/model/MediaModelsTest.kt index c2d1a18..09a0154 100644 --- a/app/src/test/java/com/mediacontrol/floatingwidget/model/MediaModelsTest.kt +++ b/app/src/test/java/com/mediacontrol/floatingwidget/model/MediaModelsTest.kt @@ -13,6 +13,7 @@ class MediaModelsTest { val state = MediaSessionState.Active( sessionId = "session-1", title = null, + artist = null, supportedActions = setOf(MediaCommand.TogglePlayPause), playbackStatus = PlaybackStatus.Playing ) @@ -60,6 +61,7 @@ class MediaModelsTest { MediaSessionState.Active( sessionId = "session-art", title = "Aurora", + artist = null, artworkCandidates = artworkCandidates, supportedActions = setOf(MediaCommand.TogglePlayPause), playbackStatus = PlaybackStatus.Playing @@ -70,6 +72,17 @@ class MediaModelsTest { MediaSessionState.Limited( reason = MediaSessionLimitReason.MissingTransportControls, title = "Aurora", + artist = null, + artworkCandidates = artworkCandidates, + supportedActions = emptySet() + ).currentArtworkCandidates() + ) + assertEquals( + artworkCandidates, + MediaSessionState.Limited( + reason = MediaSessionLimitReason.MissingTransportControls, + title = "Aurora", + artist = null, artworkCandidates = artworkCandidates, supportedActions = emptySet() ).currentArtworkCandidates() diff --git a/app/src/test/java/com/mediacontrol/floatingwidget/overlay/OverlayThumbnailPolicyTest.kt b/app/src/test/java/com/mediacontrol/floatingwidget/overlay/OverlayThumbnailPolicyTest.kt index 5d08731..ac7f690 100644 --- a/app/src/test/java/com/mediacontrol/floatingwidget/overlay/OverlayThumbnailPolicyTest.kt +++ b/app/src/test/java/com/mediacontrol/floatingwidget/overlay/OverlayThumbnailPolicyTest.kt @@ -90,6 +90,7 @@ class OverlayThumbnailPolicyTest { mediaState = MediaSessionState.Active( sessionId = "session-preview", title = "Late Bloom", + artist = null, artworkCandidates = listOf( MediaArtwork.UriSource( source = MediaArtworkSource.MetadataArtUri,