Files
pluriwave/openspec/changes/android-auto-local-music-phase2/apply-progress.md
T
FreeTLab 352eb9fc37 feat(auto): real metadata, quality sort and name buckets for local music [size:exception]
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).
2026-07-19 23:52:08 +02:00

16 KiB
Raw Blame History

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.11.8)

  • 1.11.2 MetadatosPista DTO (all-nullable: titulo, artista, artUri, bitrate, sampleRate) in lib/modelos/pista_local.dart, RED/GREEN via test/modelos/pista_local_test.dart.
  • 1.3 Extended PistaLocal with artista, embeddedArtUri, bitrate, sampleRate (nullable, default null); stale "Phase 1 minimal shape" doc note removed.
  • 1.41.7 Native readAudioMetadataBatch channel case + extraerMetadatosPista (per-docId MediaMetadataRetriever extract, API-31-gated sample rate) + cachearArteEmbebido (FileProvider cache write, skip-if-exists) + trimArtCache (mtime-LRU, 256 files / 32 MB) in MainActivity.kt. Static review only — no build/DHU in this environment, same precedent as listAudioChildren/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.12.12)

  • 2.12.2 CacheMetadatosSesion (flat LinkedHashMap, 256-entry LRU-by-access) in lib/servicios/musica_local_auto.dart.
  • 2.32.4 FuenteMusicaLocalAuto.metadatosDe(List<String>) interface method + FuenteMusicaLocalAutoImpl channel-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 construirItem call-count invariant test exactly.
  • 2.62.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.82.9 itemsLocales converted to Future<List<MediaItem>>; slice-BEFORE-metadata-fetch-BEFORE-build ordering preserved exactly; _itemLocal now builds title/artist/artUri/displaySubtitle from resolved MetadatosPista with Phase-1 fallback when absent/blank.
  • 2.10 hijosMusicaLocal call site: await constructor.itemsLocales(...).
  • 2.11 Regression: every existing itemsLocales call site in navegacion_auto_test.dart updated to await + supplied a metadatosDe function (18 call sites — grep-confirmed single production caller hijosMusicaLocal, already async, already awaits).
  • 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.13.12)

  • 3.13.2 compararCalidadLocal/ordenarPorCalidadLocal (bitrate-desc comparator, unknown-bitrate-sorts-last, mirrors OrdenEmisoras.calidad's shape).
  • 3.33.4 _maxPistasParaOrdenCalidad = 150 boundary (149/150 present, 151 omitted) + itemsLocalesOrdenCalidad (full-folder batched metadatosDe, sort, then paginaDe).
  • 3.53.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 call metadatosDe (signature doesn't receive one).
  • 3.73.9 carpeta_local_ord:<modo>:<pagina>:<docId> / carpeta_local_bucket:<idxBucket>:<pagina>:<docId> prefixes + esCarpetaLocalOrdMediaId/esCarpetaLocalBucketMediaId + ordenLocalDesde/bucketLocalDesde decoders (split-on-first-two-colons chain); collision guards tested against all 6 pre-existing prefixes.
  • 3.103.11 Page-0 mode/bucket prepend wired directly into itemsLocales (quality entry when 0 < totalPistas <= 150; 4 bucket entries when totalPistas > 50; page > 0 never re-prepends). Hardcoded Spanish labels ('Ordenar por calidad', 'A-F'/'G-M'/'N-S'/'T-Z') — no .arb files touched, per the established car-tree-label precedent.
  • 3.12 New _ord/_bucket media ids routed through hijosMusicaLocal's dispatch alongside the existing carpeta_local:/carpeta_local_pag: branches.

Phase 4 — Art Fallback & Final Regression (4.14.5)

  • 4.1 Art-fallback matrix: cache-miss (artUri: null) → placeholder; parse-failure (all-null MetadatosPista) → 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.dart run 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), so gen-l10n is moot; analyze/coverage deferred to manual pre-merge step per this task's own instruction and the orchestrator's constraint (no flutter analyze/build in this environment).
  • 4.5 Deviated/manual-follow-up (flagged, not executed here): on-device hardware validation of _maxPistasParaOrdenCalidad = 150 and 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.11.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.12.2 CacheMetadatosSesion Test written first — Method not found confirmed Implemented, 4/4 pass
2.32.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.62.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.102.12 hijosMusicaLocal await + regression 18 pre-existing call sites updated to await first (compile errors confirmed), then GREEN All 137 tests pass
3.13.2 Quality comparator Test written first — Method not found confirmed Implemented, 2/2 pass
3.33.4 _maxPistasParaOrdenCalidad + itemsLocalesOrdenCalidad Tests written first Implemented, verified boundary (149/150/151)
3.53.6 bucketsDe Tests written first Implemented, 4/4 pass incl. structural no-metadata-call proof
3.73.9 Media-id codec Tests written first Implemented, collision guards pass
3.103.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

  1. Cache-wiring location (inferred, not explicitly specified in design.md): CacheMetadatosSesion is instantiated as a module-level singleton (_cacheMetadatosLocal) in navegacion_auto.dart, wrapped by _metadatosDeConCache, and injected into hijosMusicaLocal's calls to itemsLocales/itemsLocalesOrdenCalidad/itemsLocalesBucket. This mirrors the existing _fuenteMusicaLocalGlobal singleton pattern in servicio_audio.dart and satisfies ADR-2's "paging a large folder must not evict an earlier page's cached metadata" requirement (the cache must outlive a single getChildren call). 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.
  2. Local-track displaySubtitle (design ADR-5's "unknown → omit" discipline + Testing Strategy table list it, but there is no discrete numbered task for it): implemented subtituloCalidadLocal/_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).
  3. ofreceOrdenCalidad lower bound: implemented as 0 < 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.103.12) since they now receive an extra prepended mode entry — corrected by scoping those assertions, not by weakening the new behavior.
  4. 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 97105) and pluriwave_file_paths.xml before writing cachearArteEmbebido — zero manifest changes were needed, as design claimed.

Native Kotlin — Static Review Only

Tasks 1.41.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.