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)
|
val tracks = radio.seed(trackId)
|
||||||
if (tracks.isEmpty()) return
|
if (tracks.isEmpty()) return
|
||||||
val source = "radio:$trackId"
|
val source = "radio:$trackId"
|
||||||
|
|
||||||
if (controller.mediaItemCount == 0) {
|
if (controller.mediaItemCount == 0) {
|
||||||
setQueue(tracks, initialIndex = 0, source = source)
|
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 currentIdx = controller.currentMediaItemIndex
|
||||||
val currentTrack = queueRefs.getOrNull(currentIdx)
|
val currentTrack = queueRefs.getOrNull(currentIdx)
|
||||||
// Server returns seed at index 0. Drop the duplicate when the
|
val toAppend = if (currentTrack?.id == seedTrackId) tracks.drop(1) else tracks
|
||||||
// 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
|
|
||||||
if (toAppend.isEmpty()) return
|
if (toAppend.isEmpty()) return
|
||||||
|
|
||||||
val nextIdx = currentIdx + 1
|
val nextIdx = currentIdx + 1
|
||||||
if (nextIdx < controller.mediaItemCount) {
|
if (nextIdx < controller.mediaItemCount) {
|
||||||
controller.removeMediaItems(nextIdx, controller.mediaItemCount)
|
controller.removeMediaItems(nextIdx, controller.mediaItemCount)
|
||||||
@@ -223,6 +231,31 @@ class PlayerController @Inject constructor(
|
|||||||
controller.addMediaItems(toAppend.map { it.toMediaItem(source) })
|
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 ──────────
|
// ── Internal: async connect + Listener-driven UI state sync ──────────
|
||||||
|
|
||||||
private suspend fun connectAndObserve() {
|
private suspend fun connectAndObserve() {
|
||||||
@@ -271,26 +304,7 @@ class PlayerController @Inject constructor(
|
|||||||
val idx = controller.currentMediaItemIndex
|
val idx = controller.currentMediaItemIndex
|
||||||
if (idx == lastEvaluatedItemIndex) return
|
if (idx == lastEvaluatedItemIndex) return
|
||||||
lastEvaluatedItemIndex = idx
|
lastEvaluatedItemIndex = idx
|
||||||
val current = queueRefs.getOrNull(idx) ?: return
|
handleZeroDurationIfNeeded(controller, idx)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
|
|||||||
@@ -491,29 +491,8 @@ private fun ScrubberRow(positionMs: Long, durationMs: Long, onSeek: (Long) -> Un
|
|||||||
.background(accent),
|
.background(accent),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
// Custom 4dp rounded track. M3's default Track is 16dp tall
|
// Custom 4dp rounded track — see ScrubTrack for rationale.
|
||||||
// and reads as a heavy pill rather than a measurement line;
|
track = { _ -> ScrubTrack(fraction = fraction, accent = accent) },
|
||||||
// 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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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
|
@Composable
|
||||||
private fun TransportRow(
|
private fun TransportRow(
|
||||||
isPlaying: Boolean,
|
isPlaying: Boolean,
|
||||||
|
|||||||
Reference in New Issue
Block a user