Files
pluriwave/openspec/changes/android-auto-local-music-paging/tasks.md
T
FreeTLab 725169cd31 feat(auto): page local-music folders instead of truncating at 50 [size:exception]
Folders over the 50-item cap now show a "Mas..." item that reveals
the next page on tap, instead of silently dropping the rest. Paging
slices the cheap raw list before building any MediaItem, so items
beyond the requested page are never resolved (art, title) -- proven
by a call-count test. Also swaps the raw SAF content:// URI shown in
settings for a parsed, human-readable folder name with a localized
fallback across all 13 locales.

servicio_audio.dart is untouched; this stays entirely within the
local-music tree/dispatch layer.
2026-07-19 22:14:05 +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.