feat(android/diagnostics): track-identity enrichment + zero stale Sonos state
android / Build + lint + test (push) Successful in 3m27s
android / Build + lint + test (push) Successful in 3m27s
Numeric indices wobble across re-casts (offset +1↔0 seen during output toggling), making "same track?" ambiguous. Enrich both the track_change event and the heartbeat with local_track_id (TrackRef.id) and sonos_uri (RemotePlayerState.currentTrackUri — the URL the speaker is actually streaming), so a desync is unambiguous. Also fixes the cast→phone stale-state pollution (#1211): sonos_* is now zeroed unless a remote route is active, via a shared putSonos() helper — so a just-ended cast's RemotePlayerState can't masquerade as live Sonos data in the diagnostics. Refs Scribe M9 (#119), tasks #1210 #1211. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K55iTxn95BtshocgdE1shW
This commit is contained in:
+20
-6
@@ -174,12 +174,16 @@ class DiagnosticsReporter @Inject constructor(
|
|||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect { (index, _) ->
|
.collect { (index, _) ->
|
||||||
val ui = playerController.uiState.value
|
val ui = playerController.uiState.value
|
||||||
|
val casting = outputPicker.routesState.value.current.protocol !=
|
||||||
|
OutputRoute.Protocol.SYSTEM
|
||||||
record("playback", buildJsonObject {
|
record("playback", buildJsonObject {
|
||||||
put("event", "track_change")
|
put("event", "track_change")
|
||||||
put("local_index", index)
|
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("local_pos_ms", ui.positionMs)
|
||||||
put("sonos_track", remoteState.trackNumber)
|
putSonos(this, casting)
|
||||||
put("sonos_pos_ms", remoteState.positionMs)
|
|
||||||
put("upnp_loading", ui.isUpnpLoading)
|
put("upnp_loading", ui.isUpnpLoading)
|
||||||
put("server_health", networkStatus.state.value.name)
|
put("server_health", networkStatus.state.value.name)
|
||||||
put("route", outputPicker.routesState.value.current.name)
|
put("route", outputPicker.routesState.value.current.name)
|
||||||
@@ -214,13 +218,11 @@ class DiagnosticsReporter @Inject constructor(
|
|||||||
put("is_playing", ui.isPlaying)
|
put("is_playing", ui.isPlaying)
|
||||||
put("source", ui.currentSource ?: "")
|
put("source", ui.currentSource ?: "")
|
||||||
put("local_index", ui.queueIndex)
|
put("local_index", ui.queueIndex)
|
||||||
|
put("local_track_id", ui.currentTrack?.id ?: "")
|
||||||
put("local_pos_ms", ui.positionMs)
|
put("local_pos_ms", ui.positionMs)
|
||||||
put("route", route.name)
|
put("route", route.name)
|
||||||
put("route_protocol", route.protocol.name)
|
put("route_protocol", route.protocol.name)
|
||||||
// UPnP/Sonos remote-vs-local — the desync signal.
|
putSonos(this, routeActive)
|
||||||
put("sonos_track", remoteState.trackNumber)
|
|
||||||
put("sonos_pos_ms", remoteState.positionMs)
|
|
||||||
put("sonos_playing", remoteState.isPlaying)
|
|
||||||
addPowerFields(this)
|
addPowerFields(this)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -285,6 +287,18 @@ class DiagnosticsReporter @Inject constructor(
|
|||||||
addPowerFields(this)
|
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) {
|
private fun addPowerFields(builder: kotlinx.serialization.json.JsonObjectBuilder) {
|
||||||
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
builder.put("doze", pm.isDeviceIdleMode)
|
builder.put("doze", pm.isDeviceIdleMode)
|
||||||
|
|||||||
Reference in New Issue
Block a user