feat(wear): PR5 — persistir y recuperar la reproducción local tras muerte del proceso - #20
Merged
PonceGL merged 1 commit intoAug 11, 2026
Conversation
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
changed the base branch from
feature/wear-recomposition-audit
to
feature/wear-playlist-transfer
August 11, 2026 18:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
WearPlaybackServicesea unMediaSessionServiceen 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 porDataStore<Preferences>— refleja el mismo patrón dePlaylistBatchTransferPersistenceen:app(un solo slot, JSON vía kotlinx.serialization), en su propio archivo de DataStore ya que:weary:appson procesos distintos sin almacenamiento compartido.WearLocalPlayerRepositoryguarda(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.WearPlayerViewModelrecié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 eloutputTargetdeWearStateRepositoryaWATCH. Ese cambio importa:outputTargetno está persistido (siempre arranca enPHONE), así que sin él la cola restaurada quedaría correcta dentro deWearLocalPlayerRepositorypero nunca aparecería en elplayerStateunificado que lee la pantalla del reproductor.Decisión de diseño acotada, con trade-off explícito
isPersistedLocalPlaybackStateRestorableexige 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)../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