Files
pluriwave/openspec/changes/archive/2026-07-19-android-auto-local-music-paging/tasks.md
T
FreeTLab e030a0975d docs(openspec): archive android-auto-local-music-paging
Merges its delta requirements into the android-auto-media base spec.
This closes out Phase-1 polish for local music; Phase 2 (metadata,
sort/filter, real art) and Phase 3 (subfolder scoping, shuffle)
remain the only planned future work for this feature.
2026-07-19 22:21:45 +02:00

8.6 KiB

Tasks: Android Auto Local Music — On-Demand Paging + Friendly Folder Name

Review Workload Forecast

Field Value
Estimated changed lines ~480-600 (prod ~170: navegacion_auto.dart ~120, musica_local_auto.dart ~45, pantalla_ajustes.dart ~8; l10n ~26 across 13 arb; tests ~300-350: paging/id/Más/call-count/hijosMusicaLocal + friendly-name + 8 call-site updates)
400-line budget risk Medium
Chained PRs recommended Yes
Suggested split PR 1 → PR 2 → PR 3 (each independently <400 lines)
Delivery strategy single PR with size:exception (user explicitly overrode the chained-PR recommendation)
Chain strategy n/a — single PR

Decision needed before apply: Resolved — single PR, size:exception. Chained PRs recommended: Yes (not taken; user chose single-PR exception) 400-line budget risk: Medium (accepted)

Suggested Work Units

Unit Goal Likely PR Notes
1 paginaDe<T>/hayPaginaSiguiente + carpeta_local_pag: id encode/decode, fully unit-tested PR 1 Foundation; no behavior change yet; ~200 lines
2 itemsLocales paged rewrite + "Más…" item + hijosMusicaLocal paged branch, call-count/boundary tests PR 2 Depends on PR 1; user-visible paging behavior; ~250 lines
3 nombreCarpetaDesdeUri + 13-locale localMusicFolderGenericName + settings UI wiring PR 3 Independent of PR 1/2; ~150 lines

Implemented as a single PR per delivery strategy — all 3 units applied in one pass, work-unit commit boundaries preserved conceptually in the diff structure (paging primitives → paged id scheme → itemsLocales/hijosMusicaLocal → friendly name → l10n → settings UI).

Phase 1: Paging Primitives (lib/servicios/navegacion_auto.dart)

  • 1.1 RED: test paginaDe<T> — mid-list slice, empty list, page beyond range, partial last page (spec: no permanent unreachability)
  • 1.2 GREEN: implement top-level paginaDe<T> (skip/take, generic per ADR-6)
  • 1.3 RED: test hayPaginaSiguiente exact boundary (total == (pagina+1)*tamano → false; > → true)
  • 1.4 GREEN: implement top-level hayPaginaSiguiente

Phase 2: Paged Media-ID Scheme

  • 2.1 RED: test esCarpetaLocalPaginadaMediaId/paginaCarpetaLocalDesde — collision-free vs emisora:/grupo:/eq_preset:/carpeta_local:/pista: + bare root ids; docId containing ://; empty-tail root page (carpeta_local_pag:1:)
  • 2.2 GREEN: implement _prefijoCarpetaLocalPaginada, esCarpetaLocalPaginadaMediaId, paginaCarpetaLocalDesde (strip-by-length, split-on-first-:, per ADR-1)

Phase 3: Paged itemsLocales + "Más…" Item

  • 3.1 RED: test call-count invariant via @visibleForTesting construirItem spy — 200 NodoLocals, page 0/1/3 each call spy exactly min(50, remaining) times (ADR-3 regression guard)
  • 3.2 RED: test "Más…" boundary — exactly 50 (no "Más…"), 51-100 (one "Más…", then last page none), 100+ chains 3+ pages, all items reached
  • 3.3 RED: test "Más…" item shape (id carpeta_local_pag:<n+1>:<docId>, title='Más…', playable:false, no artUri, round-trips via paginaCarpetaLocalDesde); root paging (documentIdPadre:''); stale page beyond range → [], no "Más…", no throw
  • 3.4 GREEN: extend itemsLocales(nodos, {required documentIdPadre, pagina=0, tamano=_maxItemsCarpetaLocal, construirItem}) — sort full list, paginaDe the cheap list, map only the slice, append _itemMasLocal via hayPaginaSiguiente; add _tituloMasLocal='Más…', _itemMasLocal (hardcoded Spanish per ADR-5 — no arb key)
  • 3.5 Update existing itemsLocales(nodos) call sites in test/servicios/navegacion_auto_test.dart (~lines 384-484) to pass documentIdPadre — mechanical signature adaptation. One pre-existing 60-item test asserted the OLD cap-truncate behavior (exactly 50 items, no more) — this is superseded by the spec's paging requirement (Modified Requirement "Local Music Folder Item Cap and Paging"), so that specific assertion was deliberately rewritten (not just signature-patched) into two new boundary tests (exactamente 50 items, 51 a 100 items) that assert the NEW paging behavior; every other call site kept identical assertions with only documentIdPadre added.

Phase 4: Wire Paged ID into hijosMusicaLocal

  • 4.1 RED: test hijosMusicaLocal routes carpeta_local_pag:<n>:<docId> to (docId, n) and returns that page; non-local id still null; null fuente → []; thrown error → []
  • 4.2 GREEN: add paged branch inside hijosMusicaLocal (ADR-7 — servicio_audio.dart unchanged; getChildren/playFromMediaId need no edit)

Phase 5: Friendly Folder Name (lib/servicios/musica_local_auto.dart)

  • 5.1 RED: test nombreCarpetaDesdeUri — internal storage (primary:Music/MyFolderMyFolder), SD card (1A2B-3C4D:MusicMusic), nested /-segment, storage-root empty tail (primary:)→generic fallback, unparseable→generic fallback, never raw URI/empty (ADR-4)
  • 5.2 GREEN: implement pure nombreCarpetaDesdeUri(String, {required String nombreGenerico}) -> String. Signature refinement vs. the terse task description (mirrors the same abbreviation gap task 3.4 had for itemsLocales): the generic-fallback text must be the LOCALIZED localMusicFolderGenericName string per ADR-4/ADR-5, but this function has no BuildContext/AppLocalizations access by design (pure, unit-testable without a widget tree). So the fallback text is threaded in as a plain String nombreGenerico parameter — the call site (pantalla_ajustes.dart, which already holds l10n) supplies l10n.localMusicFolderGenericName. Precedent: pantalla_reproductor.dart's _formatearDuracion(AppLocalizations l10n, ...) pattern, minus the Flutter-generated-class coupling.

Phase 6: Localization

  • 6.1 Added localMusicFolderGenericName key (phone UI only, per ADR-5) to all 13 lib/l10n/app_{ar,bn,de,en,es,fr,hi,id,it,ja,pt,ru,zh}.arb — no "Más…" arb key anywhere. Values matched each locale's existing localMusicFolder*/localMusicChoosePath register precedent (e.g. es "Carpeta seleccionada", en "Selected folder", de "Ausgewählter Ordner", fr "Dossier sélectionné", ar "مجلد محدد", ja "選択したフォルダー", zh "已选文件夹", etc).

Phase 7: Settings UI Wiring

  • 7.1 _SeccionMusicaLocal (lib/pantallas/pantalla_ajustes.dart ~line 361): renders nombreCarpetaDesdeUri(carpeta, nombreGenerico: l10n.localMusicFolderGenericName) instead of raw carpeta when configured; unconfigured case (null/empty) keeps l10n.localMusicFolderNotConfigured. Widget test added in test/pantallas/pantalla_ajustes_test.dart (group "_SeccionMusicaLocal — friendly folder name (Phase 7)") but COULD NOT be executed in this sandbox — see Known Deviations below; this is a pre-existing environment gap, not introduced by this change.

Phase 8: Regression / Manual Follow-Up

  • 8.1 Confirmed esArchivoAudio tests (musica_local_auto_test.dart, 10/10 pass) and raiz()/esCarpetaLocalMediaId/idCarpetaLocalDesde/plain carpeta_local: hijosMusicaLocal tests (navegacion_auto_test.dart, 85/85 pass) pass with no behavior change — this change only extends itemsLocales internals and adds a new branch, root-folder-count/dispatch logic untouched.
  • 8.2 DEVIATED (manual follow-up, not executable here): flutter analyze, flutter test (full suite), flutter gen-l10n — run manually, same convention as prior changes.

Known Deviations (apply-time)

  1. pantalla_ajustes_test.dart Phase 7 widget tests could not be run in this sandbox. The tracked lib/l10n/gen/app_localizations.dart is stale — it predates even the PARENT local-music change (missing localMusicFolderNotConfigured, localMusicSectionTitle, etc., which the file already referenced before this apply pass touched anything). Regenerating it requires flutter gen-l10n, which is documented to hang in this environment and was explicitly out of scope for this apply run. This is a pre-existing gap, confirmed by compiler errors pointing exclusively at PRE-EXISTING lines (320-383) before any of my new code was even reached. The new tests are written and structurally correct (seed SharedPreferences with a musica_local_uri, assert the friendly name renders and the raw content:// URI never does) and will pass once flutter gen-l10n is run as part of the normal manual follow-up (task 8.2).
  2. itemsLocales' 60-item cap test was intentionally rewritten, not just signature-patched (see 3.5) — this is a deliberate behavior change mandated by the spec delta (Modified Requirement "Local Music Folder Item Cap and Paging": cap-and-truncate → cap-and-page), not a regression.