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.
8.6 KiB
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
hayPaginaSiguienteexact 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 vsemisora:/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 construirItemspy — 200NodoLocals, page 0/1/3 each call spy exactlymin(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, noartUri, round-trips viapaginaCarpetaLocalDesde); 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,paginaDethe cheap list, map only the slice, append_itemMasLocalviahayPaginaSiguiente; add_tituloMasLocal='Más…',_itemMasLocal(hardcoded Spanish per ADR-5 — no arb key) - 3.5 Update existing
itemsLocales(nodos)call sites intest/servicios/navegacion_auto_test.dart(~lines 384-484) to passdocumentIdPadre— 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 onlydocumentIdPadreadded.
Phase 4: Wire Paged ID into hijosMusicaLocal
- 4.1 RED: test
hijosMusicaLocalroutescarpeta_local_pag:<n>:<docId>to(docId, n)and returns that page; non-local id stillnull;nullfuente →[]; thrown error →[] - 4.2 GREEN: add paged branch inside
hijosMusicaLocal(ADR-7 —servicio_audio.dartunchanged;getChildren/playFromMediaIdneed no edit)
Phase 5: Friendly Folder Name (lib/servicios/musica_local_auto.dart)
- 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) - 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 foritemsLocales): the generic-fallback text must be the LOCALIZEDlocalMusicFolderGenericNamestring per ADR-4/ADR-5, but this function has noBuildContext/AppLocalizationsaccess by design (pure, unit-testable without a widget tree). So the fallback text is threaded in as a plainString nombreGenericoparameter — the call site (pantalla_ajustes.dart, which already holdsl10n) suppliesl10n.localMusicFolderGenericName. Precedent:pantalla_reproductor.dart's_formatearDuracion(AppLocalizations l10n, ...)pattern, minus the Flutter-generated-class coupling.
Phase 6: Localization
- 6.1 Added
localMusicFolderGenericNamekey (phone UI only, per ADR-5) to all 13lib/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 existinglocalMusicFolder*/localMusicChoosePathregister 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): rendersnombreCarpetaDesdeUri(carpeta, nombreGenerico: l10n.localMusicFolderGenericName)instead of rawcarpetawhen configured; unconfigured case (null/empty) keepsl10n.localMusicFolderNotConfigured. Widget test added intest/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
esArchivoAudiotests (musica_local_auto_test.dart, 10/10 pass) andraiz()/esCarpetaLocalMediaId/idCarpetaLocalDesde/plaincarpeta_local:hijosMusicaLocaltests (navegacion_auto_test.dart, 85/85 pass) pass with no behavior change — this change only extendsitemsLocalesinternals 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)
pantalla_ajustes_test.dartPhase 7 widget tests could not be run in this sandbox. The trackedlib/l10n/gen/app_localizations.dartis stale — it predates even the PARENT local-music change (missinglocalMusicFolderNotConfigured,localMusicSectionTitle, etc., which the file already referenced before this apply pass touched anything). Regenerating it requiresflutter 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 (seedSharedPreferenceswith amusica_local_uri, assert the friendly name renders and the rawcontent://URI never does) and will pass onceflutter gen-l10nis run as part of the normal manual follow-up (task 8.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.