Skip to content

feat(wear): PR5 — persistir y recuperar la reproducción local tras muerte del proceso - #20

Merged
PonceGL merged 1 commit into
feature/wear-playlist-transferfrom
feature/wear-playback-recovery
Aug 11, 2026
Merged

feat(wear): PR5 — persistir y recuperar la reproducción local tras muerte del proceso#20
PonceGL merged 1 commit into
feature/wear-playlist-transferfrom
feature/wear-playback-recovery

Conversation

@PonceGL

@PonceGL PonceGL commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Quinto y último ítem de la propuesta de Grupo B, sobre PR19 (aún sin fusionar).

Causa que ataca

Confirmado en hardware esta sesión, no es teórico: el proceso de la app se recicló durante un episodio de presión de memoria del sistema mientras corría una app de tracking de actividad junto a la reproducción local (el PID cambió en ~90s en los que otros 23 procesos del sistema también murieron por memoria). Que WearPlaybackService sea un MediaSessionService en primer plano lo hace menos probable, no imposible — un reloj Wear OS tiene muy poca RAM. Antes de este PR, ese tipo de muerte del proceso perdía la cola y la posición en silencio, sin más camino de vuelta que navegar manualmente hasta la playlist y darle play de nuevo.

Qué hace

  • WearPlaybackStatePersistence, respaldado por DataStore<Preferences> — refleja el mismo patrón de PlaylistBatchTransferPersistence en :app (un solo slot, JSON vía kotlinx.serialization), en su propio archivo de DataStore ya que :wear y :app son procesos distintos sin almacenamiento compartido.
  • WearLocalPlayerRepository guarda (queueSongIds, currentIndex, positionMs) en los eventos que importan (play/pause, cambio de canción) más un tick cada 10s mientras reproduce — no en cada tick de 1s de la UI, que sería puro costo de escritura en disco sin beneficio — y lo limpia en una parada deliberada.
  • En un WearPlayerViewModel recién creado (proceso nuevo), restorePersistedPlaybackIfAvailable() resuelve los IDs persistidos contra la biblioteca local actual del reloj (pueden haberse borrado canciones desde la foto), restaura lo que sigue existiendo en pausa — no reproduce automáticamente, porque arrancar audio sin un gesto reciente del usuario al abrir la app sería sorpresivo — y, si efectivamente restauró algo, cambia el outputTarget de WearStateRepository a WATCH. Ese cambio importa: outputTarget no está persistido (siempre arranca en PHONE), así que sin él la cola restaurada quedaría correcta dentro de WearLocalPlayerRepository pero nunca aparecería en el playerState unificado que lee la pantalla del reproductor.

Decisión de diseño acotada, con trade-off explícito

isPersistedLocalPlaybackStateRestorable exige que la foto no tenga más de 6 horas y tenga cola/índice válidos — una ventana conservadora, sin datos de hardware para calibrarla con precisión, elegida para cubrir recuperaciones reales sin resucitar en silencio una cola de hace días la próxima vez que se abra la app.

Pruebas

  • WearPlaybackStatePersistenceTest (6, round-trip DataStore) + WearPlaybackStateRestorabilityTest (6, la decisión pura de frescura/validez).
  • El cableado real de restauración al arrancar y si la notificación/estado en primer plano se ve bien no es testeable por unidad — necesita confirmación en el reloj, idealmente reproduciendo una muerte real del proceso (no solo cerrar la app normalmente) durante reproducción local activa.
  • ./gradlew :wear:compileDebugKotlin :wear:testDebugUnitTest — build limpio, 41 tests en verde.

Con este PR se completan los 5 ítems de la propuesta de Grupo B.

🤖 Generated with Claude Code

Confirmed on-device this session, not theoretical: the app's process was
recycled during a system-wide low-memory episode while a fitness-tracking app
ran alongside local playback (PID changed across ~90s in which 23 other
system processes were also killed for memory). WearPlaybackService being a
foreground MediaSessionService makes that less likely, not impossible — Wear
OS watches have very little RAM. Before this commit, that kind of process
death silently dropped the queue and position with no way back except
manually re-navigating to the playlist and hitting play again.

Adds WearPlaybackStatePersistence (DataStore<Preferences>-backed, mirrors
:app's PlaylistBatchTransferPersistence — same single-slot,
JSON-via-kotlinx.serialization shape, separate DataStore file since :wear and
:app are different processes with no shared storage). WearLocalPlayerRepository
saves (queueSongIds, currentIndex, positionMs) on the events that matter
(play/pause, track change) plus a coarse 10s tick while playing — not on every
1s UI tick, which would be pure DataStore-write battery cost for no benefit —
and clears it on a deliberate stop.

On a fresh WearPlayerViewModel (i.e. a fresh process), restorePersistedPlaybackIfAvailable()
resolves the persisted song ids against the watch's current local library
(songs may have been deleted since the snapshot), restores what's still
there paused — not auto-playing, since starting audio without a fresh user
gesture on app open would be surprising — and, if it actually restored
something, flips WearStateRepository's outputTarget to WATCH. That target
flip matters: outputTarget isn't itself persisted (always starts at PHONE),
so without it the restored queue would sit in WearLocalPlayerRepository
correctly but never surface in the unified playerState the Player screen
reads. A successful restore is exactly the signal that the user was on
watch-local playback when the process died.

isPersistedLocalPlaybackStateRestorable additionally requires the snapshot
to be no older than 6 hours and to have a valid queue/index — a
conservative, undedicated-hardware-calibrated window (no on-device data
exists to tune it precisely), picked to cover realistic crash-recovery gaps
without silently resurrecting a queue from days ago the next time the app
happens to open.

12 new tests (WearPlaybackStatePersistenceTest, mirrors
PlaylistBatchTransferPersistenceTest's DataStore-round-trip style;
WearPlaybackStateRestorabilityTest for the pure freshness/validity check).
The actual restore-on-boot wiring and the "does the notification/foreground
state look right" behavior are not unit-testable — need on-device
confirmation, ideally by reproducing a real process kill (not just a normal
app close) during active local playback.

`./gradlew :wear:compileDebugKotlin :wear:testDebugUnitTest` — clean build,
41 tests in verde.
@PonceGL
PonceGL changed the base branch from feature/wear-recomposition-audit to feature/wear-playlist-transfer August 11, 2026 18:06
@PonceGL
PonceGL merged commit 4c0a084 into feature/wear-playlist-transfer Aug 11, 2026
@PonceGL
PonceGL deleted the feature/wear-playback-recovery branch August 11, 2026 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant