# 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) - [x] 1.1–1.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`. - [x] 1.3 Extended `PistaLocal` with `artista`, `embeddedArtUri`, `bitrate`, `sampleRate` (nullable, default `null`); stale "Phase 1 minimal shape" doc note removed. - [x] 1.4–1.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`. - [x] 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) - [x] 2.1–2.2 `CacheMetadatosSesion` (flat `LinkedHashMap`, 256-entry LRU-by-access) in `lib/servicios/musica_local_auto.dart`. - [x] 2.3–2.4 `FuenteMusicaLocalAuto.metadatosDe(List)` interface method + `FuenteMusicaLocalAutoImpl` channel-backed implementation (never throws, empty-input short-circuit, null-field-tolerant row mapping). - [x] 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. - [x] 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. - [x] 2.8–2.9 `itemsLocales` converted to `Future>`; 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. - [x] 2.10 `hijosMusicaLocal` call site: `await constructor.itemsLocales(...)`. - [x] 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 `await`s). - [x] 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) - [x] 3.1–3.2 `compararCalidadLocal`/`ordenarPorCalidadLocal` (bitrate-desc comparator, unknown-bitrate-sorts-last, mirrors `OrdenEmisoras.calidad`'s shape). - [x] 3.3–3.4 `_maxPistasParaOrdenCalidad = 150` boundary (149/150 present, 151 omitted) + `itemsLocalesOrdenCalidad` (full-folder batched `metadatosDe`, sort, then `paginaDe`). - [x] 3.5–3.6 `bucketsDe(List)` — 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). - [x] 3.7–3.9 `carpeta_local_ord:::` / `carpeta_local_bucket:::` prefixes + `esCarpetaLocalOrdMediaId`/`esCarpetaLocalBucketMediaId` + `ordenLocalDesde`/`bucketLocalDesde` decoders (split-on-first-two-colons chain); collision guards tested against all 6 pre-existing prefixes. - [x] 3.10–3.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. - [x] 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.1–4.5) - [x] 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. - [x] 4.2 Verification-only: `_itemLocal`'s Task-2.9 fallback branch already covers 4.1 — no gap found, no new production code needed. - [x] 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). - [x] 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). - [x] 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.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 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.10–3.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 `` 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: 18/18 (corrected from initially claimed 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.