Files
pluriwave/openspec/changes/archive/2026-07-19-android-auto-local-music-phase2/apply-progress.md
T
FreeTLab 85dd043cd4 docs(openspec): archive android-auto-local-music-phase2
Merges its delta requirements into the android-auto-media base spec.
Completes Phase 2; only Phase 3 (subfolder scoping, shuffle) remains.
On-device validation of the native metadata path is still an
outstanding pre-release gate across all local-music phases shipped
today.
2026-07-19 23:59:21 +02:00

121 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
- [x] 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`.
- [x] 1.3 Extended `PistaLocal` with `artista`, `embeddedArtUri`, `bitrate`, `sampleRate` (nullable, default `null`); stale "Phase 1 minimal shape" doc note removed.
- [x] 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`.
- [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.12.12)
- [x] 2.12.2 `CacheMetadatosSesion` (flat `LinkedHashMap`, 256-entry LRU-by-access) in `lib/servicios/musica_local_auto.dart`.
- [x] 2.32.4 `FuenteMusicaLocalAuto.metadatosDe(List<String>)` 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.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.
- [x] 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.
- [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.13.12)
- [x] 3.13.2 `compararCalidadLocal`/`ordenarPorCalidadLocal` (bitrate-desc comparator, unknown-bitrate-sorts-last, mirrors `OrdenEmisoras.calidad`'s shape).
- [x] 3.33.4 `_maxPistasParaOrdenCalidad = 150` boundary (149/150 present, 151 omitted) + `itemsLocalesOrdenCalidad` (full-folder batched `metadatosDe`, sort, then `paginaDe`).
- [x] 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).
- [x] 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.
- [x] 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.
- [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.14.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.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: 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.