Skip to content

Commit c1c153d

Browse files
authored
feat: add artist-title display and triple-tap widget toggle
Add artist field to MediaSessionState and currentDisplayText() format. Add tripleTapToToggle setting and gesture. Fix unit tests.
1 parent 98aa10f commit c1c153d

14 files changed

Lines changed: 147 additions & 3 deletions

File tree

app/src/main/java/com/mediacontrol/floatingwidget/MainActivity.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class MainActivity : AppCompatActivity() {
107107
onSetHorizontalOffsetPreset = ::setHorizontalOffsetPreset,
108108
onSetPersistentOverlayEnabled = ::setPersistentOverlayEnabled,
109109
onSetLowQualityThumbnailFallbackEnabled = ::setThumbnailEnabled,
110+
onSetTripleTapToToggle = ::setTripleTapToToggle,
110111
onStartOverlay = ::startOverlay,
111112
onStopOverlay = ::stopOverlay,
112113
onDispatchPrevious = ::dispatchPrevious,
@@ -195,6 +196,10 @@ class MainActivity : AppCompatActivity() {
195196
widgetConfigStateHolder.setLowQualityThumbnailFallbackEnabled(enabled)
196197
}
197198

199+
private fun setTripleTapToToggle(enabled: Boolean) {
200+
widgetConfigStateHolder.setTripleTapToToggle(enabled)
201+
}
202+
198203
private fun setHorizontalOffsetPreset(xOffsetDp: Int) {
199204
widgetConfigStateHolder.setHorizontalOffsetDp(xOffsetDp)
200205
}

app/src/main/java/com/mediacontrol/floatingwidget/media/AndroidMediaSessionRepository.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,14 @@ class AndroidMediaSessionRepository(
255255
displayTitle = controller.metadata?.getText(MediaMetadata.METADATA_KEY_DISPLAY_TITLE),
256256
title = controller.metadata?.getText(MediaMetadata.METADATA_KEY_TITLE)
257257
)
258+
val artist = controller.metadata?.getText(MediaMetadata.METADATA_KEY_ARTIST)?.toString()?.trim()?.takeIf { it.isNotEmpty() }
258259
val artworkCandidates = resolveArtworkCandidates(controller)
259260
val sessionId = "${controller.packageName}:${controller.sessionToken}"
260261
val playbackState = controller.playbackState
261262
?: return buildMediaSessionState(
262263
sessionId = sessionId,
263264
title = title,
265+
artist = artist,
264266
artworkCandidates = artworkCandidates,
265267
playbackStatus = null,
266268
supportedActions = null
@@ -269,6 +271,7 @@ class AndroidMediaSessionRepository(
269271
return buildMediaSessionState(
270272
sessionId = sessionId,
271273
title = title,
274+
artist = artist,
272275
artworkCandidates = artworkCandidates,
273276
playbackStatus = playbackState.state.toPlaybackStatus(),
274277
supportedActions = playbackState.actions.toSupportedCommands()
@@ -318,6 +321,7 @@ class AndroidMediaSessionRepository(
318321
fun buildMediaSessionState(
319322
sessionId: String,
320323
title: String?,
324+
artist: String?,
321325
artworkCandidates: List<MediaArtwork> = emptyList(),
322326
playbackStatus: PlaybackStatus?,
323327
supportedActions: Set<MediaCommand>?
@@ -326,6 +330,7 @@ class AndroidMediaSessionRepository(
326330
?: return MediaSessionState.Limited(
327331
reason = MediaSessionLimitReason.PlaybackStateUnknown,
328332
title = title,
333+
artist = artist,
329334
artworkCandidates = artworkCandidates,
330335
supportedActions = emptySet()
331336
)
@@ -336,13 +341,15 @@ class AndroidMediaSessionRepository(
336341
MediaSessionState.Limited(
337342
reason = MediaSessionLimitReason.MissingTransportControls,
338343
title = title,
344+
artist = artist,
339345
artworkCandidates = artworkCandidates,
340346
supportedActions = emptySet()
341347
)
342348
} else {
343349
MediaSessionState.Active(
344350
sessionId = sessionId,
345351
title = title,
352+
artist = artist,
346353
artworkCandidates = artworkCandidates,
347354
supportedActions = resolvedSupportedActions,
348355
playbackStatus = resolvedPlaybackStatus

app/src/main/java/com/mediacontrol/floatingwidget/model/MediaModels.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ sealed interface MediaSessionState {
7979
data class Active(
8080
val sessionId: String,
8181
val title: String?,
82+
val artist: String?,
8283
val artworkCandidates: List<MediaArtwork> = emptyList(),
8384
val supportedActions: Set<MediaCommand>,
8485
val playbackStatus: PlaybackStatus
@@ -87,6 +88,7 @@ sealed interface MediaSessionState {
8788
data class Limited(
8889
val reason: MediaSessionLimitReason,
8990
val title: String?,
91+
val artist: String?,
9092
val artworkCandidates: List<MediaArtwork> = emptyList(),
9193
val supportedActions: Set<MediaCommand>
9294
) : MediaSessionState
@@ -116,6 +118,20 @@ fun MediaSessionState.currentTitle(): String? {
116118
}
117119
}
118120

121+
fun MediaSessionState.currentDisplayText(): String {
122+
val t = when (this) {
123+
is MediaSessionState.Active -> title.orEmpty()
124+
is MediaSessionState.Limited -> title.orEmpty()
125+
else -> ""
126+
}
127+
val a = when (this) {
128+
is MediaSessionState.Active -> artist
129+
is MediaSessionState.Limited -> artist
130+
else -> null
131+
}
132+
return if (!a.isNullOrBlank() && t.isNotBlank()) "$a - $t" else t
133+
}
134+
119135
fun MediaSessionState.currentArtworkCandidates(): List<MediaArtwork> {
120136
return when (this) {
121137
is MediaSessionState.Active -> artworkCandidates

app/src/main/java/com/mediacontrol/floatingwidget/model/WidgetModels.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ data class WidgetConfig(
8282
val themePreset: WidgetThemePreset = WidgetThemePreset.Dark,
8383
val opacity: Float = 1f,
8484
val persistentOverlayEnabled: Boolean = true,
85-
val allowLowQualityThumbnailFallback: Boolean = false
85+
val allowLowQualityThumbnailFallback: Boolean = false,
86+
val tripleTapToToggle: Boolean = false
8687
)
8788

8889
data class WidgetOverlaySizing(

app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayHost.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ interface OverlayHost {
2020
fun update(viewState: OverlayViewState)
2121

2222
fun detach()
23+
24+
fun setOnToggleWidget(onToggle: () -> Unit)
2325
}

app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayPresentationSpec.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import android.view.Gravity
44
import android.widget.LinearLayout
55
import sw2.io.mediafloat.model.DragHandlePlacement
66
import sw2.io.mediafloat.model.MediaSessionState
7-
import sw2.io.mediafloat.model.currentTitle
7+
import sw2.io.mediafloat.model.currentDisplayText
88
import sw2.io.mediafloat.model.WidgetOverlayAppearance
99

1010
internal data class OverlayPresentationSpec(
@@ -34,7 +34,7 @@ internal object OverlayPresentationSpecFactory {
3434
dragHandlePlacement: DragHandlePlacement
3535
): OverlayPresentationSpec {
3636
val sizing = appearance.sizing
37-
val titleText = mediaState.currentTitle().orEmpty()
37+
val titleText = mediaState.currentDisplayText()
3838
val titleVisible = true
3939
val metrics = OverlayLayoutCalculator.calculate(
4040
appearance = appearance,

app/src/main/java/com/mediacontrol/floatingwidget/overlay/OverlayService.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ class OverlayService : Service() {
184184
}
185185

186186
currentMediaState = mediaRepository.refresh(reason = "overlay_attach")
187+
overlayHost.setOnToggleWidget {
188+
if (runtimeCoordinator.readinessRuntimeState() is sw2.io.mediafloat.model.OverlayRuntimeState.Showing) {
189+
runtimeCoordinator.stopOverlay()
190+
} else {
191+
runtimeCoordinator.startOverlay()
192+
}
193+
}
187194
overlayHost.attach(
188195
OverlayViewState(
189196
config = currentWidgetConfig,

app/src/main/java/com/mediacontrol/floatingwidget/overlay/WindowManagerOverlayHost.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ class WindowManagerOverlayHost(
7171
private var isDragging = false
7272
private var appliedDragHandlePlacement: DragHandlePlacement = DragHandlePlacement.Right
7373
private var appliedThumbnailSignature: String? = null
74+
private var onToggleWidget: (() -> Unit)? = null
75+
private var lastTapTime: Long = 0
76+
private var tapCount: Int = 0
77+
78+
override fun setOnToggleWidget(onToggle: () -> Unit) {
79+
onToggleWidget = onToggle
80+
}
7481

7582
override fun attach(viewState: OverlayViewState) {
7683
currentViewState = viewState
@@ -213,6 +220,20 @@ class WindowManagerOverlayHost(
213220
contentDescription = appContext.getString(R.string.overlay_drag_handle)
214221
gravity = Gravity.CENTER
215222
setOnTouchListener(DragTouchListener())
223+
setOnClickListener {
224+
val now = System.currentTimeMillis()
225+
if (lastTapTime > 0 && now - lastTapTime < 500L) {
226+
tapCount++
227+
if (tapCount >= 3) {
228+
tapCount = 0
229+
lastTapTime = 0
230+
onToggleWidget?.invoke()
231+
}
232+
} else {
233+
tapCount = 1
234+
lastTapTime = now
235+
}
236+
}
216237
}
217238
}
218239

app/src/main/java/com/mediacontrol/floatingwidget/state/WidgetConfigStateHolder.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ class WidgetConfigStateHolder(
6969
repository.saveConfig(currentState().config.copy(allowLowQualityThumbnailFallback = enabled))
7070
}
7171

72+
fun setTripleTapToToggle(enabled: Boolean) {
73+
repository.saveConfig(currentState().config.copy(tripleTapToToggle = enabled))
74+
}
75+
7276
fun savePosition(position: WidgetPosition) {
7377
repository.savePosition(position)
7478
}

app/src/main/java/com/mediacontrol/floatingwidget/ui/AppShell.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ fun AppShell(
178178
onSetHorizontalOffsetPreset: (Int) -> Unit = {},
179179
onSetPersistentOverlayEnabled: (Boolean) -> Unit = {},
180180
onSetLowQualityThumbnailFallbackEnabled: (Boolean) -> Unit = {},
181+
onSetTripleTapToToggle: (Boolean) -> Unit = {},
181182
onStartOverlay: () -> Unit = {},
182183
onStopOverlay: () -> Unit = {},
183184
onDispatchPrevious: () -> Unit = {},
@@ -275,6 +276,7 @@ fun AppShell(
275276
onSetHorizontalOffsetPreset = onSetHorizontalOffsetPreset,
276277
onSetPersistentOverlayEnabled = onSetPersistentOverlayEnabled,
277278
onSetLowQualityThumbnailFallbackEnabled = onSetLowQualityThumbnailFallbackEnabled,
279+
onSetTripleTapToToggle = onSetTripleTapToToggle,
278280
onStartOverlay = onStartOverlay,
279281
onStopOverlay = onStopOverlay,
280282
onDispatchPrevious = onDispatchPrevious,
@@ -321,6 +323,7 @@ fun AppShell(
321323
onSetHorizontalOffsetPreset = onSetHorizontalOffsetPreset,
322324
onSetPersistentOverlayEnabled = onSetPersistentOverlayEnabled,
323325
onSetLowQualityThumbnailFallbackEnabled = onSetLowQualityThumbnailFallbackEnabled,
326+
onSetTripleTapToToggle = onSetTripleTapToToggle,
324327
onStartOverlay = onStartOverlay,
325328
onStopOverlay = onStopOverlay,
326329
onDispatchPrevious = onDispatchPrevious,
@@ -491,6 +494,7 @@ private fun SectionContent(
491494
onSetHorizontalOffsetPreset: (Int) -> Unit,
492495
onSetPersistentOverlayEnabled: (Boolean) -> Unit,
493496
onSetLowQualityThumbnailFallbackEnabled: (Boolean) -> Unit,
497+
onSetTripleTapToToggle: (Boolean) -> Unit,
494498
onStartOverlay: () -> Unit,
495499
onStopOverlay: () -> Unit,
496500
onDispatchPrevious: () -> Unit,
@@ -540,6 +544,7 @@ private fun SectionContent(
540544
onSetDragHandlePlacement = onSetDragHandlePlacement,
541545
onSetHorizontalOffsetPreset = onSetHorizontalOffsetPreset,
542546
onSetLowQualityThumbnailFallbackEnabled = onSetLowQualityThumbnailFallbackEnabled,
547+
onSetTripleTapToToggle = onSetTripleTapToToggle,
543548
onStartOverlay = onStartOverlay,
544549
onStopOverlay = onStopOverlay,
545550
wideLayout = wideLayout
@@ -639,6 +644,7 @@ private fun SettingsScreen(
639644
onSetDragHandlePlacement: (DragHandlePlacement) -> Unit,
640645
onSetHorizontalOffsetPreset: (Int) -> Unit,
641646
onSetLowQualityThumbnailFallbackEnabled: (Boolean) -> Unit,
647+
onSetTripleTapToToggle: (Boolean) -> Unit,
642648
onStartOverlay: () -> Unit,
643649
onStopOverlay: () -> Unit,
644650
wideLayout: Boolean
@@ -688,6 +694,10 @@ private fun SettingsScreen(
688694
config = widgetConfigState.config,
689695
onSetEnabled = onSetLowQualityThumbnailFallbackEnabled
690696
)
697+
TripleTapToggleCard(
698+
enabled = widgetConfigState.config.tripleTapToToggle,
699+
onSetEnabled = onSetTripleTapToToggle
700+
)
691701
}
692702
}
693703
} else {
@@ -727,6 +737,10 @@ private fun SettingsScreen(
727737
config = widgetConfigState.config,
728738
onSetEnabled = onSetLowQualityThumbnailFallbackEnabled
729739
)
740+
TripleTapToggleCard(
741+
enabled = widgetConfigState.config.tripleTapToToggle,
742+
onSetEnabled = onSetTripleTapToToggle
743+
)
730744
}
731745
}
732746

@@ -2360,6 +2374,47 @@ private fun PermissionItem(
23602374
}
23612375
}
23622376

2377+
@Composable
2378+
private fun TripleTapToggleCard(
2379+
enabled: Boolean,
2380+
onSetEnabled: (Boolean) -> Unit,
2381+
modifier: Modifier = Modifier
2382+
) {
2383+
Card(
2384+
modifier = modifier.fillMaxWidth(),
2385+
shape = PanelShape,
2386+
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
2387+
) {
2388+
Row(
2389+
modifier = Modifier
2390+
.fillMaxWidth()
2391+
.padding(20.dp),
2392+
horizontalArrangement = Arrangement.SpaceBetween,
2393+
verticalAlignment = Alignment.CenterVertically
2394+
) {
2395+
Column(
2396+
modifier = Modifier.weight(1f),
2397+
verticalArrangement = Arrangement.spacedBy(4.dp)
2398+
) {
2399+
Text(
2400+
text = "Triple-tap to toggle",
2401+
style = MaterialTheme.typography.titleSmall,
2402+
fontWeight = FontWeight.Medium
2403+
)
2404+
Text(
2405+
text = "Tap the drag handle three times quickly to show or hide the widget.",
2406+
style = MaterialTheme.typography.bodySmall,
2407+
color = MaterialTheme.colorScheme.onSurfaceVariant
2408+
)
2409+
}
2410+
Switch(
2411+
checked = enabled,
2412+
onCheckedChange = onSetEnabled
2413+
)
2414+
}
2415+
}
2416+
}
2417+
23632418
@Composable
23642419
private fun MediaStatusCard(
23652420
mediaSummaryState: MediaSummaryState,
@@ -3254,6 +3309,7 @@ private fun previewRuntimeState(): OverlayRuntimeState {
32543309
mediaState = MediaSessionState.Active(
32553310
sessionId = "preview-session",
32563311
title = "Velvet City Lights After Midnight Remix",
3312+
artist = "Synthwave Collective",
32573313
artworkCandidates = listOf(
32583314
MediaArtwork.UriSource(
32593315
source = MediaArtworkSource.MetadataArtUri,
@@ -3273,6 +3329,7 @@ private fun previewMediaSummaryState(): MediaSummaryState {
32733329
mediaState = MediaSessionState.Active(
32743330
sessionId = "preview-session",
32753331
title = "Velvet City Lights After Midnight Remix",
3332+
artist = "Synthwave Collective",
32763333
artworkCandidates = listOf(
32773334
MediaArtwork.UriSource(
32783335
source = MediaArtworkSource.MetadataArtUri,

0 commit comments

Comments
 (0)