M9 diagnostics follow-ups: playback relabel, sort, connected fix, per-skip + track-identity #105

Merged
bvandeusen merged 4 commits from dev into main 2026-06-30 19:19:15 -04:00
Showing only changes of commit 392454b249 - Show all commits
@@ -174,12 +174,16 @@ class DiagnosticsReporter @Inject constructor(
.distinctUntilChanged()
.collect { (index, _) ->
val ui = playerController.uiState.value
val casting = outputPicker.routesState.value.current.protocol !=
OutputRoute.Protocol.SYSTEM
record("playback", buildJsonObject {
put("event", "track_change")
put("local_index", index)
// Track IDENTITY, not just index — indices wobble across
// re-casts, so the id + Sonos URI make a desync unambiguous.
put("local_track_id", ui.currentTrack?.id ?: "")
put("local_pos_ms", ui.positionMs)
put("sonos_track", remoteState.trackNumber)
put("sonos_pos_ms", remoteState.positionMs)
putSonos(this, casting)
put("upnp_loading", ui.isUpnpLoading)
put("server_health", networkStatus.state.value.name)
put("route", outputPicker.routesState.value.current.name)
@@ -214,13 +218,11 @@ class DiagnosticsReporter @Inject constructor(
put("is_playing", ui.isPlaying)
put("source", ui.currentSource ?: "")
put("local_index", ui.queueIndex)
put("local_track_id", ui.currentTrack?.id ?: "")
put("local_pos_ms", ui.positionMs)
put("route", route.name)
put("route_protocol", route.protocol.name)
// UPnP/Sonos remote-vs-local — the desync signal.
put("sonos_track", remoteState.trackNumber)
put("sonos_pos_ms", remoteState.positionMs)
put("sonos_playing", remoteState.isPlaying)
putSonos(this, routeActive)
addPowerFields(this)
})
}
@@ -285,6 +287,18 @@ class DiagnosticsReporter @Inject constructor(
addPowerFields(this)
}
// Sonos/UPnP remote-vs-local desync fields. Only meaningful while a
// remote route is active; zeroed otherwise so a stale RemotePlayerState
// from a just-ended cast can't masquerade as live Sonos data (the
// cast→phone handoff artifact). currentTrackUri is the desync ground
// truth — it carries the track the speaker is actually streaming.
private fun putSonos(builder: kotlinx.serialization.json.JsonObjectBuilder, casting: Boolean) {
builder.put("sonos_track", if (casting) remoteState.trackNumber else 0)
builder.put("sonos_pos_ms", if (casting) remoteState.positionMs else 0)
builder.put("sonos_playing", casting && remoteState.isPlaying)
if (casting) builder.put("sonos_uri", remoteState.currentTrackUri)
}
private fun addPowerFields(builder: kotlinx.serialization.json.JsonObjectBuilder) {
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
builder.put("doze", pm.isDeviceIdleMode)