Local tracks now show embedded title/artist/album art (via native MediaMetadataRetriever, cached through the existing FileProvider) instead of the raw filename, falling back gracefully when a file has no usable tags. Adds two navigable entry points per folder: sort by audio quality (bitrate, capped at 150 tracks per folder to bound worst-case latency) and alphabetical name buckets -- the closest realistic form of "filtering" given Android Auto has no text-search UI in this integration. Metadata resolves only for the page actually being browsed (same slice-cheap-then-map discipline as the paging change), backed by a flat 256-entry LRU session cache that survives across pages. No new permission, no new pub dependency, no l10n changes (car-tree labels stay hardcoded Spanish, matching every existing label in the tree).
16 KiB
Apply Progress: Android Auto Local Music — Phase 2 (Metadata, Sort, Name Buckets)
Change: android-auto-local-music-phase2
Mode: Strict TDD (all behavior-changing Dart) + Static-review-only (native Kotlin, per established project precedent — no Android build/DHU available in this environment)
Delivery: Single PR with size:exception (resolved delivery strategy — user's established preference this session, matching the two prior high-risk changes today). All tasks implemented in one apply batch; no slicing.
Batch: First and only apply batch — no prior apply-progress existed.
Completed Tasks
All 37/37 checkbox items in tasks.md are marked [x]. Summary by phase:
Phase 1 — Foundation: Model & Native Metadata Surface (1.1–1.8)
- 1.1–1.2
MetadatosPistaDTO (all-nullable:titulo,artista,artUri,bitrate,sampleRate) inlib/modelos/pista_local.dart, RED/GREEN viatest/modelos/pista_local_test.dart. - 1.3 Extended
PistaLocalwithartista,embeddedArtUri,bitrate,sampleRate(nullable, defaultnull); stale "Phase 1 minimal shape" doc note removed. - 1.4–1.7 Native
readAudioMetadataBatchchannel case +extraerMetadatosPista(per-docIdMediaMetadataRetrieverextract, API-31-gated sample rate) +cachearArteEmbebido(FileProvider cache write, skip-if-exists) +trimArtCache(mtime-LRU, 256 files / 32 MB) inMainActivity.kt. Static review only — no build/DHU in this environment, same precedent aslistAudioChildren/resolvePlayableUri/pickMusicFolder. - 1.8 Flagged in this report (see "Native Kotlin — Static Review Only" section below).
Phase 2 — Metadata Cache & Async itemsLocales Conversion (load-bearing) (2.1–2.12)
- 2.1–2.2
CacheMetadatosSesion(flatLinkedHashMap, 256-entry LRU-by-access) inlib/servicios/musica_local_auto.dart. - 2.3–2.4
FuenteMusicaLocalAuto.metadatosDe(List<String>)interface method +FuenteMusicaLocalAutoImplchannel-backed implementation (never throws, empty-input short-circuit, null-field-tolerant row mapping). - 2.5 THE load-bearing test: metadata-resolution spy proving page 0 of a 200-node folder resolves EXACTLY the 50 page docIds (not all 200), page 3 resolves exactly the trailing 50 — mirrors the existing
construirItemcall-count invariant test exactly. - 2.6–2.7 Metadata-present case (title/artUri/artist reflect resolved metadata) and metadata-absent/failed case (falls back to filename + placeholder, no exception) — both tested.
- 2.8–2.9
itemsLocalesconverted toFuture<List<MediaItem>>; slice-BEFORE-metadata-fetch-BEFORE-build ordering preserved exactly;_itemLocalnow builds title/artist/artUri/displaySubtitle from resolvedMetadatosPistawith Phase-1 fallback when absent/blank. - 2.10
hijosMusicaLocalcall site:await constructor.itemsLocales(...). - 2.11 Regression: every existing
itemsLocalescall site innavegacion_auto_test.dartupdated toawait+ supplied ametadatosDefunction (18 call sites — grep-confirmed single production callerhijosMusicaLocal, alreadyasync, alreadyawaits). - 2.12 Phase 1 scenarios (empty subfolder, folder browse, playback resolution, art fallback) and the paging spy test confirmed still green under the new async signature.
Phase 3 — Quality Sort, Name Buckets, Media-ID Wiring (3.1–3.12)
- 3.1–3.2
compararCalidadLocal/ordenarPorCalidadLocal(bitrate-desc comparator, unknown-bitrate-sorts-last, mirrorsOrdenEmisoras.calidad's shape). - 3.3–3.4
_maxPistasParaOrdenCalidad = 150boundary (149/150 present, 151 omitted) +itemsLocalesOrdenCalidad(full-folder batchedmetadatosDe, sort, thenpaginaDe). - 3.5–3.6
bucketsDe(List<NodoLocal>)— pure, name-only partitioning into 4 fixed ranges (A-F/G-M/N-S/T-Z); a bucket with 0 matches returns[], not an error; structurally cannot callmetadatosDe(signature doesn't receive one). - 3.7–3.9
carpeta_local_ord:<modo>:<pagina>:<docId>/carpeta_local_bucket:<idxBucket>:<pagina>:<docId>prefixes +esCarpetaLocalOrdMediaId/esCarpetaLocalBucketMediaId+ordenLocalDesde/bucketLocalDesdedecoders (split-on-first-two-colons chain); collision guards tested against all 6 pre-existing prefixes. - 3.10–3.11 Page-0 mode/bucket prepend wired directly into
itemsLocales(quality entry when0 < totalPistas <= 150; 4 bucket entries whentotalPistas > 50; page > 0 never re-prepends). Hardcoded Spanish labels ('Ordenar por calidad','A-F'/'G-M'/'N-S'/'T-Z') — no.arbfiles touched, per the established car-tree-label precedent. - 3.12 New
_ord/_bucketmedia ids routed throughhijosMusicaLocal's dispatch alongside the existingcarpeta_local:/carpeta_local_pag:branches.
Phase 4 — Art Fallback & Final Regression (4.1–4.5)
- 4.1 Art-fallback matrix: cache-miss (
artUri: null) → placeholder; parse-failure (all-nullMetadatosPista) → placeholder; never an empty/broken tile — dedicated test group added. - 4.2 Verification-only:
_itemLocal's Task-2.9 fallback branch already covers 4.1 — no gap found, no new production code needed. - 4.3 Full regression:
navegacion_auto_test.dart+musica_local_auto_test.dart+pista_local_test.dartrun together — 137/137 pass (see "Test Results" below). - 4.4 Deviated/manual-follow-up (flagged, not executed here):
flutter analyze,flutter test --coverage,flutter gen-l10n— no l10n gap was found (no new phone-facing string introduced), sogen-l10nis moot;analyze/coveragedeferred to manual pre-merge step per this task's own instruction and the orchestrator's constraint (noflutter analyze/build in this environment). - 4.5 Deviated/manual-follow-up (flagged, not executed here): on-device hardware validation of
_maxPistasParaOrdenCalidad = 150and the 256-file/32 MB art budget — no DHU/emulator available in this environment (Design "Open Questions", explicitly out of this task list's automated scope).
Files Changed
| File | Action | What Was Done |
|---|---|---|
lib/modelos/pista_local.dart |
Modified | Added MetadatosPista DTO; extended PistaLocal with artista/embeddedArtUri/bitrate/sampleRate |
test/modelos/pista_local_test.dart |
Created | 5 tests for MetadatosPista + extended PistaLocal construction |
lib/servicios/musica_local_auto.dart |
Modified | Added CacheMetadatosSesion (LRU-by-access, 256 entries); added metadatosDe to FuenteMusicaLocalAuto interface + channel-backed impl |
test/servicios/musica_local_auto_test.dart |
Modified | Added CacheMetadatosSesion group (4 tests) + FuenteMusicaLocalAutoImpl.metadatosDe group (4 tests) |
lib/servicios/navegacion_auto.dart |
Modified | Async itemsLocales + metadata-backed _itemLocal (title/artist/artUri/subtitle); subtituloCalidadLocal; compararCalidadLocal/ordenarPorCalidadLocal; BucketLocal/bucketsDe; _ord/_bucket prefixes + codec; itemsLocalesOrdenCalidad/itemsLocalesBucket; page-0 mode/bucket prepend; hijosMusicaLocal routing extended; module-level _cacheMetadatosLocal + _metadatosDeConCache cache wrapper |
test/servicios/navegacion_auto_test.dart |
Modified | Extensive: async conversion of all 18 pre-existing itemsLocales call sites; new load-bearing metadata-spy test; metadata-present/absent tests; quality-comparator, threshold, bucket-partitioning, media-id codec, collision-guard, page-0-prepend, itemsLocalesOrdenCalidad, itemsLocalesBucket, hijosMusicaLocal _ord/_bucket routing, and art-fallback-matrix test groups; _FakeFuenteMusicaLocalAuto extended with metadatosDe |
android/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt |
Modified | readAudioMetadataBatch channel case + extraerMetadatosPista + cachearArteEmbebido + trimArtCache + hashDocumentId. Static review only. |
openspec/changes/android-auto-local-music-phase2/tasks.md |
Modified | All 37 items marked [x]; Review Workload Forecast updated with resolved delivery strategy |
lib/l10n/*.arb (13 files): NOT touched. No genuinely new phone-facing string was introduced — sort-mode/bucket labels are hardcoded Spanish car-tree text, per the corrected grounding note (overrides design.md's File Changes table, which listed .arb changes; the corrected instruction for this apply batch takes precedence and was verified against live code: _tituloMasLocal/_carpeta(idFavoritos, ...) precedent confirmed in navegacion_auto.dart, and localMusicFolderGenericName confirmed phone-settings-only in pantalla_ajustes.dart:366).
TDD Cycle Evidence
| Task | RED | GREEN | REFACTOR |
|---|---|---|---|
1.1–1.2 MetadatosPista |
pista_local_test.dart written first, ran against pre-change model — compile error confirmed |
DTO added, 5/5 pass | Doc comments only |
1.3 PistaLocal extension |
Same file/run as above | Extended fields added, verified green | — |
2.1–2.2 CacheMetadatosSesion |
Test written first — Method not found confirmed |
Implemented, 4/4 pass | — |
2.3–2.4 metadatosDe (impl) |
Test written first — metadatosDe isn't defined confirmed |
Implemented, 4/4 pass | — |
| 2.5 Metadata-resolution spy (load-bearing) | Test written first against sync itemsLocales — compile error confirmed |
Async conversion + docId-slicing implemented, verified page-0/page-3 exact-match | — |
2.6–2.9 Metadata-present/absent + _itemLocal |
Tests written first | _itemLocal metadata-aware rewrite, verified |
Added subtituloCalidadLocal/_formatKhz for completeness (design ADR-5 discipline) beyond the letter of the numbered task, lightly tested in the metadata-present case |
2.10–2.12 hijosMusicaLocal await + regression |
18 pre-existing call sites updated to await first (compile errors confirmed), then GREEN |
All 137 tests pass | — |
| 3.1–3.2 Quality comparator | Test written first — Method not found confirmed |
Implemented, 2/2 pass | — |
3.3–3.4 _maxPistasParaOrdenCalidad + itemsLocalesOrdenCalidad |
Tests written first | Implemented, verified boundary (149/150/151) | — |
3.5–3.6 bucketsDe |
Tests written first | Implemented, 4/4 pass incl. structural no-metadata-call proof | — |
| 3.7–3.9 Media-id codec | Tests written first | Implemented, collision guards pass | — |
3.10–3.12 Page-0 wiring + hijosMusicaLocal routing |
Tests written first | Implemented; 8 pre-existing single-node itemsLocales fixtures broke (corrected during verify from an initially-reported 9) because they used .single/exact-length assertions that didn't anticipate the new prepend — fixed by scoping assertions to pista:-prefixed items (documented as expected regression-test maintenance, not a design deviation) |
— |
| 4.1 Art-fallback matrix | Test written first | Verified via existing fallback branch, 2/2 pass | — |
Deviations from Design
- Cache-wiring location (inferred, not explicitly specified in
design.md):CacheMetadatosSesionis instantiated as a module-level singleton (_cacheMetadatosLocal) innavegacion_auto.dart, wrapped by_metadatosDeConCache, and injected intohijosMusicaLocal's calls toitemsLocales/itemsLocalesOrdenCalidad/itemsLocalesBucket. This mirrors the existing_fuenteMusicaLocalGlobalsingleton pattern inservicio_audio.dartand satisfies ADR-2's "paging a large folder must not evict an earlier page's cached metadata" requirement (the cache must outlive a singlegetChildrencall). Design's "Data Flow" section describes the cache-then-native flow conceptually but doesn't specify exact ownership — this is a reasonable, minimal-footprint resolution, not a functional deviation. - Local-track
displaySubtitle(design ADR-5's "unknown → omit" discipline + Testing Strategy table list it, but there is no discrete numbered task for it): implementedsubtituloCalidadLocal/_formatKhz(kbps · kHz format) for completeness and design fidelity, with light test coverage folded into the metadata-present test rather than a full matrix (kept scope proportionate to the fact that it wasn't its own numbered RED/GREEN task pair). ofreceOrdenCalidadlower bound: implemented as0 < totalPistas <= 150(any non-empty folder up to the cap gets the quality entry, including a 1-track folder) — this is the literal reading of ADR-4 ("IF audio count ≤ 150", no stated lower bound beyond non-empty). This caused 8 pre-existing single-node test fixtures to need updating (see TDD Evidence row 3.10–3.12) since they now receive an extra prepended mode entry — corrected by scoping those assertions, not by weakening the new behavior.- Bucket letter ranges: fixed as 4 ranges (A-F/G-M/N-S/T-Z) since neither spec nor design specifies exact boundaries beyond "e.g. A-F, G-M, ..." — a name not starting with an ASCII letter matches no bucket (no catch-all "other" bucket defined by spec).
Issues Found
None blocking. One design-claim was explicitly re-verified against live code per the grounding instructions rather than assumed: the ${applicationId}.fileprovider FileProvider authority and pluriwave_file_paths.xml's <cache-path path="."/> declaration were both confirmed present in AndroidManifest.xml (lines 97–105) and pluriwave_file_paths.xml before writing cachearArteEmbebido — zero manifest changes were needed, as design claimed.
Native Kotlin — Static Review Only
Tasks 1.4–1.8 (readAudioMetadataBatch, extraerMetadatosPista, cachearArteEmbebido, trimArtCache, hashDocumentId in MainActivity.kt) are static-review-only, per this project's established precedent (mirrors the review treatment of listAudioChildren/resolvePlayableUri/pickMusicFolder from Phase 1) — no Android build or DHU is available in this environment, so these were implemented carefully with per-entry and whole-call try/catch, retriever.release() in finally, and never-throws-across-the-channel-boundary discipline, but could not be exercised by an automated test. No test was fabricated for this code that can't actually run.
Test Results (independently re-confirmed via a second full run before writing this report)
flutter test test/servicios/navegacion_auto_test.dart test/servicios/musica_local_auto_test.dart test/modelos/pista_local_test.dart --concurrency=1 --timeout=60s
→ 137/137 passed, 0 failed
- navegacion_auto_test.dart: 114/114
- musica_local_auto_test.dart: 19/19
- pista_local_test.dart: 5/5
Diff Size
git diff --stat (tracked files): 6 files changed, 1809 insertions(+), 82 deletions(-).
Plus 1 new untracked file: test/modelos/pista_local_test.dart (88 lines).
Total ≈ 1979 changed lines — well above the 400-line review budget, as forecast (400-line budget risk: High). Delivered as a single PR under size:exception per the resolved delivery strategy for this session (matches the same choice made for the two prior high-risk changes today).
Workload / PR Boundary
- Mode: single PR,
size:exception - Current work unit: N/A (not chained)
- Boundary: this batch starts from an empty apply-progress (no prior batch) and finishes with all 37/37 tasks complete, 137/137 tests green
- Estimated review budget impact: High — reviewer should expect a large, multi-concern diff (native Kotlin static review + 3 layered Dart features: async metadata conversion, quality sort, name buckets) explicitly accepted via
size:exception
Remaining Tasks
None — all 37/37 tasks complete. Tasks 4.4/4.5 are explicitly deviated/manual-follow-up per their own task description (not part of this task list's automated scope) and are flagged above, not silently skipped.
Status
37/37 tasks complete. 137/137 tests passing. Ready for verify.