Skip to content

Wear playlist transfer — PR 5: UI del teléfono (acción, confirmación, progreso) - #11

Merged
PonceGL merged 9 commits into
feature/wear-batch-transfer-coordinatorfrom
feature/wear-send-playlist-ui
Aug 9, 2026
Merged

Wear playlist transfer — PR 5: UI del teléfono (acción, confirmación, progreso)#11
PonceGL merged 9 commits into
feature/wear-batch-transfer-coordinatorfrom
feature/wear-send-playlist-ui

Conversation

@PonceGL

@PonceGL PonceGL commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Apilado sobre #10 (coordinador de lote). Quinto PR del plan: cierra el flujo descrito en la tarea original — desde "busco la opción para enviar esa playlist" hasta "recibo una notificación... puedo salir de la app".

Contenido

  • Acción nueva en el sheet de opciones de playlist (PlaylistDetailScreen): "Send to Watch" o "Update on Watch" según si alguna canción ya está en el reloj.
  • Diálogo de confirmación con la estimación ya construida en PR3 (WatchPlaylistTransferEstimator): canciones pendientes, tamaño aproximado, tiempo aproximado. Botón de confirmar deshabilitado si el reloj no está disponible o no hay nada pendiente.
  • Banner de progreso no bloqueante en la pantalla de la playlist: el usuario puede navegar fuera o salir de la app mientras continúa — la notificación (ya construida en PR4) es la que sigue el progreso desde ahí.
  • Indicador también en LibraryScreen: un lote de playlist tiene prioridad visual sobre una transferencia suelta de una sola canción en el badge del TopAppBar (misma regla de prioridad que ya usa la notificación). Diálogo WatchPlaylistBatchProgressDialog que replica el look del diálogo existente de canción individual.
  • PlaylistViewModel gana estimateWatchTransfer, isPlaylistFullyOnWatch, sendPlaylistToWatch, cancelPlaylistTransfer, activePlaylistBatchTransfer — mismo patrón que SongInfoBottomSheetViewModel ya usa para el caso de una sola canción.

Una corrección sobre mi propio trabajo, hecha antes de publicar

Extraje inicialmente el banner de progreso a un componente compartido pensando que LibraryScreen lo reutilizaría. Al construir la integración en LibraryScreen terminé usando un diseño distinto (badge + diálogo, no banner) que ya es la convención establecida en esa pantalla. Revertí la extracción — el componente solo se usa en un sitio, así que queda private dentro de PlaylistDetailScreen.kt como el resto de composables locales del archivo. Como aún no había publicado la rama, corregí el commit en vez de dejar un mensaje inexacto en el historial.

Verificado

  • :app:testDebugUnitTest, suite completa → 462 tests, solo los mismos 5 fallos preexistentes de siempre. Los 7 tests nuevos de PlaylistViewModelTest (que no existía antes de este PR) están en verde.
  • :app:assembleDebug → compila y empaqueta limpio.

Sin verificar (necesita dispositivo)

Todo lo visual — composables, animaciones, layout — se verifica en la sesión de hardware dedicada. La lógica que sí es testeable en JVM (estimación, delegación al coordinador, reactividad del StateFlow de lote) tiene test.

Deuda técnica que dejo anotada, no corregida

PlaylistViewModel ya tenía ~1.230 líneas antes de este PR; suma 4 dependencias de constructor más. Es un God Object preexistente (GEN-AP-01) que no toca partir en esta tarea.

Ejecutado en local (JBR de Android Studio como JDK 21).

PonceGL added 9 commits August 8, 2026 13:13
estimateWatchTransfer, isPlaylistFullyOnWatch, sendPlaylistToWatch,
cancelPlaylistTransfer, and activePlaylistBatchTransfer — thin
delegation to PlaylistWatchTransferCoordinator/PhoneWatchTransferStateStore/
WearPhoneTransferSender, mirroring the exact pattern
SongInfoBottomSheetViewModel already uses for the single-song case.

Adds 4 constructor dependencies to an already-1200-line ViewModel.
Flagged, not fixed here — splitting it is a separate, unrelated
refactor.
…aylist screen

New action in the playlist options sheet — labeled "Send to Watch" or
"Update on Watch" depending on whether any of its songs are already
there. Tapping it refreshes watch availability and opens a
confirmation dialog showing pending-song count and the size/time
estimate (WatchPlaylistTransferEstimator, already built) before
anything is sent.

A non-blocking progress banner appears at the top of the songs list
once a batch is running for this playlist, with a cancel action — the
user can navigate away or leave the app while it continues; the
foreground notification (already built) is what tracks it from there.
A playlist batch takes priority over a concurrent lone single-song
transfer in the top bar badge and compact-navigation pill — same
priority rule as the transfer notification (WatchTransferForegroundService)
and the playlist screen's own banner: it's the longer-running, more
significant operation, and showing both at once would be unreadable.

WatchPlaylistBatchProgressDialog mirrors the existing single-song
WatchTransferProgressDialog's look (loading ring + percent, wavy
progress bar, cancel button) rather than reusing PlaylistDetailScreen's
banner — LibraryScreen already establishes badge-tap-opens-dialog as
its own convention for this, and a lone playlist name/song-count
doesn't need the full list context a banner implies.
Covers estimateWatchTransfer, isPlaylistFullyOnWatch (empty list,
partial, and fully-on-watch cases), sendPlaylistToWatch,
cancelPlaylistTransfer, activePlaylistBatchTransfer, and
refreshWatchAvailability. The rest of PlaylistViewModel's existing
surface (CRUD, sorting, AI generation, M3U import/export) is untouched
and out of scope — no PlaylistViewModelTest existed before this.

activePlaylistBatchTransfer is a stateIn(WhileSubscribed) flow —
reading .value directly never triggers the upstream collection, so
that test uses Turbine's test{} for a real subscriber instead.

Verified: :app:testDebugUnitTest, full suite, 462 tests. Only the same
5 pre-existing failures unrelated to this branch. The 7 new tests in
this PR are green.
WearDataListenerService now routes the PLAYLIST_SYNC message path to
WearTransferRepository.onPlaylistSyncReceived, which upserts the
playlist entity and its song cross-refs (order preserved via
position) into LocalPlaylistDao in one transaction.

Re-syncing an existing playlistId (e.g. after editing it on the
phone) replaces membership/order rather than merging with stale
cross-refs, and preserves the original createdAt while bumping
updatedAt — the DAO's upsertPlaylist already had this transactional
behavior from PR2, this just starts calling it.

The manifest's MESSAGE_RECEIVED intent filter gets a matching <data>
entry for /playlist_sync, mirroring the existing entries for the
other message paths.

Unlike the two pre-existing branches in the same when-block
(TRANSFER_METADATA, FAVORITES_SYNC_STATE), this new branch's catch
re-throws CancellationException instead of swallowing it — left the
other two alone since fixing them is out of scope here.
Backs the upcoming local-playlists screens. Resolves each playlist
song's availability reactively by joining its cross-ref order against
LocalSongDao.getAllSongs(), so a song that finishes transferring
while the detail screen is open flips from pending to playable
without the user backing out and re-entering.

playlistIdsReceiving surfaces which playlists currently have an
in-flight song transfer, for a receiving indicator on the list
screen. playAll/playFrom skip songs still pending transfer.
LocalPlaylistsScreen lists playlists synced from the phone with a
receiving indicator; LocalPlaylistDetailScreen shows a playlist's
songs in sync order, marking pending ones as disabled with a
'waiting to transfer' label instead of hiding them, so the list's
shape matches the phone immediately even before every song has
arrived. Both use androidx.wear.compose.foundation.lazy's items()
overload with an explicit key (playlist.playlistId / item.songId)
instead of the module's usual count-based items(n){} — needed here
because playlists reorder by updatedAt on every sync and songs flip
availability while the screen is open, both of which lose state and
break animations without a stable key.

DownloadsScreen gets a new 'Playlists' entry navigating into the new
screens. Reachable via Downloads → Playlists → a playlist → its
songs.
Persists at most one in-flight playlist batch transfer intent
(batchId, playlistId, playlistName, songIds, requestedAtMillis) to
the app's shared DataStore<Preferences>, matching the existing
*PreferencesRepository convention.

Deliberately doesn't persist the rest of PhoneWatchTransferStateStore
(per-song byte progress, reachable nodes, ...) — that's UI-only
state, cheap to rebuild, and churns too fast to persist sensibly.
Only the intent needs to survive a process restart; the coordinator
already re-derives everything else when it runs a batch.

clearInFlightBatch(batchId) only removes the stored intent if its
batchId still matches — if a newer batch already overwrote it (e.g.
the user sent another playlist before the first one's cleanup ran),
clearing unconditionally would drop that newer intent instead.
PlaylistWatchTransferCoordinator now persists its batch intent when
a transfer starts and clears it on every terminal outcome (completed,
failed with no reachable watch, cancelled) — so a batch surviving to
the next app start is exactly the ones that were cut off by the
process dying mid-transfer, not a theoretical case for a transfer
that can run tens of minutes over Bluetooth.

resumePersistedBatchIfNeeded() re-runs any such orphaned batch: it
refreshes the watch-library snapshot first (empty right after a cold
start) and waits briefly for it to resolve, so the existing dedup
against what's already on the watch is accurate on the first pass
instead of relying solely on the watch's own duplicate rejection.
Re-running from scratch is safe either way — the watch rejects a
transfer for a song it already has.

Wired into PixelPlayApplication.onCreate(), alongside the app's other
one-shot startup work. Best-effort: a cold start not directly
triggered by the user may be too restricted to start the foreground
service this resumes into, so failures here are logged and skipped
rather than crashing app startup — the persisted intent stays put
for the next launch that can.
@PonceGL
PonceGL merged commit 56ff143 into feature/wear-batch-transfer-coordinator Aug 9, 2026
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