address review: pre-P shadow fallback and safer notification star

- draw the play halo as an offset copy below API 28, where path
  shadow layers are ignored, and skip the inner shadow there too
- add a repository toggleStar that reads and writes under the star lock,
  so fast double taps on the notification cannot desync
- reset the star button when a track starts, so a failed source lookup
  cannot leave the previous song's state showing
This commit is contained in:
austinried
2026-10-04 00:30:02 +00:00
parent 08eace9e90
commit 51ed8fdd6b
3 changed files with 60 additions and 25 deletions
@@ -209,20 +209,37 @@ class LibraryRepository(
starScope.launch { setStar(type, id, starred) } starScope.launch { setStar(type, id, starred) }
} }
fun toggleStar(
type: StarType,
id: String,
) {
starScope.launch {
starLock.withLock {
val sourceId = sourceRepository.activeSourceIdOnce() ?: return@withLock
setStarLocked(type, id, starredValue(sourceId, type, id) == null)
}
}
}
suspend fun setStar( suspend fun setStar(
type: StarType, type: StarType,
id: String, id: String,
starred: Boolean, starred: Boolean,
): Result<Unit> = ): Result<Unit> = starLock.withLock { setStarLocked(type, id, starred) }
starLock.withLock {
private suspend fun setStarLocked(
type: StarType,
id: String,
starred: Boolean,
): Result<Unit> {
val sourceId = sourceRepository.activeSourceIdOnce() val sourceId = sourceRepository.activeSourceIdOnce()
val source = sourceRepository.activeMusicSource() val source = sourceRepository.activeMusicSource()
if (sourceId == null || source == null) { if (sourceId == null || source == null) {
return@withLock Result.failure(IllegalStateException("No active server")) return Result.failure(IllegalStateException("No active server"))
} }
val previous = starredValue(sourceId, type, id) val previous = starredValue(sourceId, type, id)
updateStarred(sourceId, type, id, if (starred) System.currentTimeMillis() else null) updateStarred(sourceId, type, id, if (starred) System.currentTimeMillis() else null)
try { return try {
source.setStar(type, id, starred) source.setStar(type, id, starred)
Result.success(Unit) Result.success(Unit)
} catch (cancellation: CancellationException) { } catch (cancellation: CancellationException) {
@@ -147,12 +147,13 @@ class PlaybackService : MediaSessionService() {
private fun observeStar(songId: String?) { private fun observeStar(songId: String?) {
starJob?.cancel() starJob?.cancel()
currentSongId = songId currentSongId = songId
starred = false
val library = library ?: return val library = library ?: return
if (songId == null) { if (songId == null) {
starred = false
session?.setMediaButtonPreferences(emptyList()) session?.setMediaButtonPreferences(emptyList())
return return
} }
session?.setMediaButtonPreferences(listOf(starButton(false)))
starJob = starJob =
scope.launch { scope.launch {
val sourceId = sourceRepository?.activeSourceIdOnce() ?: return@launch val sourceId = sourceRepository?.activeSourceIdOnce() ?: return@launch
@@ -167,7 +168,7 @@ class PlaybackService : MediaSessionService() {
private fun toggleStar() { private fun toggleStar() {
val songId = currentSongId ?: return val songId = currentSongId ?: return
val library = library ?: return val library = library ?: return
library.star(StarType.Song, songId, !starred) library.toggleStar(StarType.Song, songId)
} }
private fun starButton(isStarred: Boolean): CommandButton = private fun starButton(isStarred: Boolean): CommandButton =
@@ -1,5 +1,6 @@
package com.subtracks.ui.library package com.subtracks.ui.library
import android.os.Build
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -174,7 +175,9 @@ private fun CoverArtCell(
colorFilter = if (isPlaying) DIM else null, colorFilter = if (isPlaying) DIM else null,
) )
if (isPlaying) { if (isPlaying) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Box(Modifier.matchParentSize().drawBehind { drawInnerShadow() }) Box(Modifier.matchParentSize().drawBehind { drawInnerShadow() })
}
Box( Box(
Modifier Modifier
.matchParentSize() .matchParentSize()
@@ -227,13 +230,27 @@ private fun PlayIndicator(
val scale = minOf(size.width / PLAY_ARROW.viewportWidth, size.height / PLAY_ARROW.viewportHeight) val scale = minOf(size.width / PLAY_ARROW.viewportWidth, size.height / PLAY_ARROW.viewportHeight)
withTransform({ scale(scale, scale, pivot = Offset.Zero) }) { withTransform({ scale(scale, scale, pivot = Offset.Zero) }) {
drawIntoCanvas { canvas -> drawIntoCanvas { canvas ->
val paint = val path = PLAY_ARROW_PATH.asAndroidPath()
val glyph =
android.graphics.Paint().apply { android.graphics.Paint().apply {
isAntiAlias = true isAntiAlias = true
color = tint.toArgb() color = tint.toArgb()
setShadowLayer(radius, 0f, 0f, PLAY_SHADOW.toArgb())
} }
canvas.nativeCanvas.drawPath(PLAY_ARROW_PATH.asAndroidPath(), paint) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
glyph.setShadowLayer(radius, 0f, 0f, PLAY_SHADOW.toArgb())
canvas.nativeCanvas.drawPath(path, glyph)
} else {
val halo =
android.graphics.Paint().apply {
isAntiAlias = true
color = PLAY_SHADOW.toArgb()
}
canvas.nativeCanvas.save()
canvas.nativeCanvas.translate(1f, 1.5f)
canvas.nativeCanvas.drawPath(path, halo)
canvas.nativeCanvas.restore()
canvas.nativeCanvas.drawPath(path, glyph)
}
} }
} }
} }