fix(android): satisfy detekt — ReturnCount + LongMethod refactors
android / Build + lint + test (push) Failing after 3m20s
android / Build + lint + test (push) Failing after 3m20s
Three rule trips from the playback-errors + scrubber commits: PlayerController.startRadio (4 returns → 2): extract the mid-queue append branch into appendRadioToQueue(). startRadio just does the guard checks and dispatches; the helper handles the cursor trim + addMediaItems. Behavior identical. PlayerController.onPlaybackStateChanged (4 returns → 2): extract the duration check + zero-duration error emission + skip logic into handleZeroDurationIfNeeded(). The listener stays compact (one return for non-READY, one for repeat-evaluation guard); the helper owns the failure path. NowPlayingScreen.ScrubberRow (63 lines → ~50): extract the custom track Box block into a ScrubTrack(fraction, accent) composable. The Slider's `track` lambda becomes a one-line call. Pixel output is identical.
This commit is contained in:
@@ -200,21 +200,29 @@ class PlayerController @Inject constructor(
|
||||
val tracks = radio.seed(trackId)
|
||||
if (tracks.isEmpty()) return
|
||||
val source = "radio:$trackId"
|
||||
|
||||
if (controller.mediaItemCount == 0) {
|
||||
setQueue(tracks, initialIndex = 0, source = source)
|
||||
return
|
||||
} else {
|
||||
appendRadioToQueue(controller, tracks, trackId, source)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mid-queue radio insert. The current track keeps playing; every
|
||||
* upcoming item is removed; the radio results are appended after.
|
||||
* Drops the seed from the appended list when it matches the
|
||||
* currently-playing track so it doesn't immediately repeat.
|
||||
*/
|
||||
private fun appendRadioToQueue(
|
||||
controller: MediaController,
|
||||
tracks: List<TrackRef>,
|
||||
seedTrackId: String,
|
||||
source: String,
|
||||
) {
|
||||
val currentIdx = controller.currentMediaItemIndex
|
||||
val currentTrack = queueRefs.getOrNull(currentIdx)
|
||||
// Server returns seed at index 0. Drop the duplicate when the
|
||||
// seed is the currently playing track — otherwise append the
|
||||
// full list so the current track is followed by the seed +
|
||||
// recommendations.
|
||||
val toAppend = if (currentTrack?.id == trackId) tracks.drop(1) else tracks
|
||||
val toAppend = if (currentTrack?.id == seedTrackId) tracks.drop(1) else tracks
|
||||
if (toAppend.isEmpty()) return
|
||||
|
||||
val nextIdx = currentIdx + 1
|
||||
if (nextIdx < controller.mediaItemCount) {
|
||||
controller.removeMediaItems(nextIdx, controller.mediaItemCount)
|
||||
@@ -223,6 +231,31 @@ class PlayerController @Inject constructor(
|
||||
controller.addMediaItems(toAppend.map { it.toMediaItem(source) })
|
||||
}
|
||||
|
||||
/**
|
||||
* When a STATE_READY transition for a freshly-loaded item reports
|
||||
* a zero / TIME_UNSET duration, fire a `zero_duration` error event
|
||||
* and advance past the dead track. Otherwise no-op.
|
||||
*/
|
||||
private fun handleZeroDurationIfNeeded(controller: MediaController, idx: Int) {
|
||||
val current = queueRefs.getOrNull(idx) ?: return
|
||||
val duration = controller.duration
|
||||
val isZeroDuration = duration <= 0L || duration == androidx.media3.common.C.TIME_UNSET
|
||||
if (!isZeroDuration) return
|
||||
playbackErrorEventsChannel.trySend(
|
||||
PlaybackErrorEvent(
|
||||
trackId = current.id,
|
||||
kind = "zero_duration",
|
||||
title = current.title.ifEmpty { "Track" },
|
||||
detail = "duration=$duration",
|
||||
),
|
||||
)
|
||||
if (controller.hasNextMediaItem()) {
|
||||
controller.seekToNextMediaItem()
|
||||
} else {
|
||||
controller.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Internal: async connect + Listener-driven UI state sync ──────────
|
||||
|
||||
private suspend fun connectAndObserve() {
|
||||
@@ -271,26 +304,7 @@ class PlayerController @Inject constructor(
|
||||
val idx = controller.currentMediaItemIndex
|
||||
if (idx == lastEvaluatedItemIndex) return
|
||||
lastEvaluatedItemIndex = idx
|
||||
val current = queueRefs.getOrNull(idx) ?: return
|
||||
val duration = controller.duration
|
||||
if (duration > 0L && duration != androidx.media3.common.C.TIME_UNSET) return
|
||||
// Zero-duration / unset-duration track: file decoded
|
||||
// to nothing playable. Fail fast — fire an error
|
||||
// event for the reporter (snackbar + server log) and
|
||||
// advance so the user doesn't sit on the dead track.
|
||||
playbackErrorEventsChannel.trySend(
|
||||
PlaybackErrorEvent(
|
||||
trackId = current.id,
|
||||
kind = "zero_duration",
|
||||
title = current.title.ifEmpty { "Track" },
|
||||
detail = "duration=$duration",
|
||||
),
|
||||
)
|
||||
if (controller.hasNextMediaItem()) {
|
||||
controller.seekToNextMediaItem()
|
||||
} else {
|
||||
controller.stop()
|
||||
}
|
||||
handleZeroDurationIfNeeded(controller, idx)
|
||||
}
|
||||
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
|
||||
@@ -491,29 +491,8 @@ private fun ScrubberRow(positionMs: Long, durationMs: Long, onSeek: (Long) -> Un
|
||||
.background(accent),
|
||||
)
|
||||
},
|
||||
// Custom 4dp rounded track. M3's default Track is 16dp tall
|
||||
// and reads as a heavy pill rather than a measurement line;
|
||||
// making the thumb visibly taller than the track (14dp thumb
|
||||
// on a 4dp bar) restores the "handle on a string" cue your
|
||||
// eye reads as "tool, draggable." Also drops M3's stop
|
||||
// indicator dot, which the web scrubber doesn't have.
|
||||
track = { _ ->
|
||||
Box(modifier = Modifier.fillMaxWidth().height(SCRUB_TRACK_HEIGHT_DP)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.clip(RoundedCornerShape(SCRUB_TRACK_CORNER_DP))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fraction)
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(SCRUB_TRACK_CORNER_DP))
|
||||
.background(accent),
|
||||
)
|
||||
}
|
||||
},
|
||||
// Custom 4dp rounded track — see ScrubTrack for rationale.
|
||||
track = { _ -> ScrubTrack(fraction = fraction, accent = accent) },
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -532,6 +511,34 @@ private fun ScrubberRow(positionMs: Long, durationMs: Long, onSeek: (Long) -> Un
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom 4dp rounded scrubber track. M3's default Track is 16dp tall
|
||||
* and reads as a heavy pill rather than a measurement line; making
|
||||
* the thumb visibly taller than the track (14dp thumb on a 4dp bar)
|
||||
* restores the "handle on a string" cue your eye reads as "tool,
|
||||
* draggable." Also drops M3's stop indicator dot, which the web
|
||||
* scrubber doesn't have. Extracted so [ScrubberRow] stays under
|
||||
* detekt's LongMethod ceiling.
|
||||
*/
|
||||
@Composable
|
||||
private fun ScrubTrack(fraction: Float, accent: Color) {
|
||||
Box(modifier = Modifier.fillMaxWidth().height(SCRUB_TRACK_HEIGHT_DP)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.clip(RoundedCornerShape(SCRUB_TRACK_CORNER_DP))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fraction)
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(SCRUB_TRACK_CORNER_DP))
|
||||
.background(accent),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransportRow(
|
||||
isPlaying: Boolean,
|
||||
|
||||
Reference in New Issue
Block a user