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.
121 lines
16 KiB
Markdown
121 lines
16 KiB
Markdown
# 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<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.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<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.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<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.7–3.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.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 `<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: 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.
|