# 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`/`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`) - [x] 1.1 RED: test `paginaDe` — mid-list slice, empty list, page beyond range, partial last page (spec: no permanent unreachability) - [x] 1.2 GREEN: implement top-level `paginaDe` (skip/take, generic per ADR-6) - [x] 1.3 RED: test `hayPaginaSiguiente` exact boundary (`total == (pagina+1)*tamano` → false; `>` → true) - [x] 1.4 GREEN: implement top-level `hayPaginaSiguiente` ## Phase 2: Paged Media-ID Scheme - [x] 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:`) - [x] 2.2 GREEN: implement `_prefijoCarpetaLocalPaginada`, `esCarpetaLocalPaginadaMediaId`, `paginaCarpetaLocalDesde` (strip-by-length, split-on-first-`:`, per ADR-1) ## Phase 3: Paged `itemsLocales` + "Más…" Item - [x] 3.1 RED: test call-count invariant via `@visibleForTesting construirItem` spy — 200 `NodoLocal`s, page 0/1/3 each call spy exactly `min(50, remaining)` times (ADR-3 regression guard) - [x] 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 - [x] 3.3 RED: test "Más…" item shape (id `carpeta_local_pag::`, `title='Más…'`, `playable:false`, no `artUri`, round-trips via `paginaCarpetaLocalDesde`); root paging (`documentIdPadre:''`); stale page beyond range → `[]`, no "Más…", no throw - [x] 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) - [x] 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` - [x] 4.1 RED: test `hijosMusicaLocal` routes `carpeta_local_pag::` to `(docId, n)` and returns that page; non-local id still `null`; `null` fuente → `[]`; thrown error → `[]` - [x] 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`) - [x] 5.1 RED: test `nombreCarpetaDesdeUri` — internal storage (`primary:Music/MyFolder`→`MyFolder`), SD card (`1A2B-3C4D:Music`→`Music`), nested `/`-segment, storage-root empty tail (`primary:`)→generic fallback, unparseable→generic fallback, never raw URI/empty (ADR-4) - [x] 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 - [x] 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 - [x] 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 - [x] 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. - [x] 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.