Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class MainActivity : AppCompatActivity() {
onSetHorizontalOffsetPreset = ::setHorizontalOffsetPreset,
onSetPersistentOverlayEnabled = ::setPersistentOverlayEnabled,
onSetLowQualityThumbnailFallbackEnabled = ::setThumbnailEnabled,
onSetTripleTapToToggle = ::setTripleTapToToggle,
onStartOverlay = ::startOverlay,
onStopOverlay = ::stopOverlay,
onDispatchPrevious = ::dispatchPrevious,
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -269,6 +271,7 @@ class AndroidMediaSessionRepository(
return buildMediaSessionState(
sessionId = sessionId,
title = title,
artist = artist,
artworkCandidates = artworkCandidates,
playbackStatus = playbackState.state.toPlaybackStatus(),
supportedActions = playbackState.actions.toSupportedCommands()
Expand Down Expand Up @@ -318,6 +321,7 @@ class AndroidMediaSessionRepository(
fun buildMediaSessionState(
sessionId: String,
title: String?,
artist: String?,
artworkCandidates: List<MediaArtwork> = emptyList(),
playbackStatus: PlaybackStatus?,
supportedActions: Set<MediaCommand>?
Expand All @@ -326,6 +330,7 @@ class AndroidMediaSessionRepository(
?: return MediaSessionState.Limited(
reason = MediaSessionLimitReason.PlaybackStateUnknown,
title = title,
artist = artist,
artworkCandidates = artworkCandidates,
supportedActions = emptySet()
)
Expand All @@ -336,13 +341,15 @@ class AndroidMediaSessionRepository(
MediaSessionState.Limited(
reason = MediaSessionLimitReason.MissingTransportControls,
title = title,
artist = artist,
artworkCandidates = artworkCandidates,
supportedActions = emptySet()
)
} else {
MediaSessionState.Active(
sessionId = sessionId,
title = title,
artist = artist,
artworkCandidates = artworkCandidates,
supportedActions = resolvedSupportedActions,
playbackStatus = resolvedPlaybackStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ sealed interface MediaSessionState {
data class Active(
val sessionId: String,
val title: String?,
val artist: String?,
val artworkCandidates: List<MediaArtwork> = emptyList(),
val supportedActions: Set<MediaCommand>,
val playbackStatus: PlaybackStatus
Expand All @@ -87,6 +88,7 @@ sealed interface MediaSessionState {
data class Limited(
val reason: MediaSessionLimitReason,
val title: String?,
val artist: String?,
val artworkCandidates: List<MediaArtwork> = emptyList(),
val supportedActions: Set<MediaCommand>
) : MediaSessionState
Expand Down Expand Up @@ -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<MediaArtwork> {
return when (this) {
is MediaSessionState.Active -> artworkCandidates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ interface OverlayHost {
fun update(viewState: OverlayViewState)

fun detach()

fun setOnToggleWidget(onToggle: () -> Unit)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
57 changes: 57 additions & 0 deletions app/src/main/java/com/mediacontrol/floatingwidget/ui/AppShell.kt
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ fun AppShell(
onSetHorizontalOffsetPreset: (Int) -> Unit = {},
onSetPersistentOverlayEnabled: (Boolean) -> Unit = {},
onSetLowQualityThumbnailFallbackEnabled: (Boolean) -> Unit = {},
onSetTripleTapToToggle: (Boolean) -> Unit = {},
onStartOverlay: () -> Unit = {},
onStopOverlay: () -> Unit = {},
onDispatchPrevious: () -> Unit = {},
Expand Down Expand Up @@ -275,6 +276,7 @@ fun AppShell(
onSetHorizontalOffsetPreset = onSetHorizontalOffsetPreset,
onSetPersistentOverlayEnabled = onSetPersistentOverlayEnabled,
onSetLowQualityThumbnailFallbackEnabled = onSetLowQualityThumbnailFallbackEnabled,
onSetTripleTapToToggle = onSetTripleTapToToggle,
onStartOverlay = onStartOverlay,
onStopOverlay = onStopOverlay,
onDispatchPrevious = onDispatchPrevious,
Expand Down Expand Up @@ -321,6 +323,7 @@ fun AppShell(
onSetHorizontalOffsetPreset = onSetHorizontalOffsetPreset,
onSetPersistentOverlayEnabled = onSetPersistentOverlayEnabled,
onSetLowQualityThumbnailFallbackEnabled = onSetLowQualityThumbnailFallbackEnabled,
onSetTripleTapToToggle = onSetTripleTapToToggle,
onStartOverlay = onStartOverlay,
onStopOverlay = onStopOverlay,
onDispatchPrevious = onDispatchPrevious,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -540,6 +544,7 @@ private fun SectionContent(
onSetDragHandlePlacement = onSetDragHandlePlacement,
onSetHorizontalOffsetPreset = onSetHorizontalOffsetPreset,
onSetLowQualityThumbnailFallbackEnabled = onSetLowQualityThumbnailFallbackEnabled,
onSetTripleTapToToggle = onSetTripleTapToToggle,
onStartOverlay = onStartOverlay,
onStopOverlay = onStopOverlay,
wideLayout = wideLayout
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -688,6 +694,10 @@ private fun SettingsScreen(
config = widgetConfigState.config,
onSetEnabled = onSetLowQualityThumbnailFallbackEnabled
)
TripleTapToggleCard(
enabled = widgetConfigState.config.tripleTapToToggle,
onSetEnabled = onSetTripleTapToToggle
)
}
}
} else {
Expand Down Expand Up @@ -727,6 +737,10 @@ private fun SettingsScreen(
config = widgetConfigState.config,
onSetEnabled = onSetLowQualityThumbnailFallbackEnabled
)
TripleTapToggleCard(
enabled = widgetConfigState.config.tripleTapToToggle,
onSetEnabled = onSetTripleTapToToggle
)
}
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)
}
Expand All @@ -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)
}
}

Expand All @@ -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"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -75,6 +76,7 @@ class AndroidMediaSessionRepositoryTest {
MediaSessionState.Limited(
reason = MediaSessionLimitReason.PlaybackStateUnknown,
title = "Late Night Drive",
artist = null,
artworkCandidates = artworkCandidates,
supportedActions = emptySet()
),
Expand All @@ -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)
Expand All @@ -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(
Expand Down
Loading
Loading