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.
This commit is contained in:
2026-07-19 23:59:21 +02:00
parent 352eb9fc37
commit 85dd043cd4
8 changed files with 208 additions and 4 deletions
@@ -94,7 +94,7 @@ Tasks 1.41.8 (`readAudioMetadataBatch`, `extraerMetadatosPista`, `cachearArte
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 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 → 137/137 passed, 0 failed
- navegacion_auto_test.dart: 114/114 - navegacion_auto_test.dart: 114/114
- musica_local_auto_test.dart: 19/19 - musica_local_auto_test.dart: 18/18 (corrected from initially claimed 19)
- pista_local_test.dart: 5/5 - pista_local_test.dart: 5/5
``` ```
@@ -0,0 +1,133 @@
# Archive Report: android-auto-local-music-phase2
**Date**: 2026-07-19
**Change**: android-auto-local-music-phase2
**Artifact Store Mode**: Hybrid (OpenSpec files + Engram persistence)
**Status**: Complete — Change archived and closed
## Executive Summary
The android-auto-local-music-phase2 change is fully implemented, verified with 0 CRITICAL and 2 WARNINGs (documentation-accuracy only, already corrected), and has been archived. Phase 2 of the local-music feature is now complete, delivering real embedded metadata resolution, quality-based sort navigation, and alphabetical name buckets for browsable audio files in Android Auto. Only Phase 3 (subfolder scoping refinements, shuffle) remains as planned future work. Outstanding pre-release gate: native metadata-extraction path (MediaMetadataRetriever, FileProvider art cache/LRU) requires on-device/DHU validation across all local-music phases before production release.
## Change Artifacts
### Specification Artifacts
**Base Spec**: `openspec/specs/android-auto-media/spec.md`
- **Status**: MERGED with delta requirements
- **Changes**:
- MODIFIED: "Local Music Browsable Tree" requirement expanded to include metadata resolution scenarios (title/art reflection, embedded-metadata fallback)
- ADDED: "Local Music Sort Mode Navigation" requirement (name sort, quality sort by bitrate)
- ADDED: "Local Music Alphabetical Name Buckets" requirement (A-F/G-M/N-S/T-Z letter ranges)
- ADDED: "Local Track Embedded Album Art Display" requirement (static URI resolution, fallback handling)
**Delta Spec**: `openspec/changes/archive/2026-07-19-android-auto-local-music-phase2/specs/android-auto-media/spec.md`
- Archived for traceability
### Change Folder Artifacts
**Archived Location**: `openspec/changes/archive/2026-07-19-android-auto-local-music-phase2/`
**Contents**:
- `proposal.md` — Phase 2 scope, intent, capabilities, and ADRs (metadata extraction via `MediaMetadataRetriever`, on-demand parsing, LRU cache)
- `design.md` — Technical architecture (5 ADRs, data flow, file changes, testing strategy, open questions on 150-track cap and 256-file/32MB art budget)
- `tasks.md` — 37 implementation tasks across 4 phases (model/native/cache/sort/buckets/regression)
- `apply-progress.md` — All 37/37 tasks completed, 137/137 tests passing, single PR with `size:exception`, corrected test counts (18 vs 19 claimed)
- `verify-report.md` — PASS WITH WARNINGS; 0 CRITICAL, 2 WARNINGs (test-count inaccuracy corrected before archive), implementation verified sound
- `specs/android-auto-media/spec.md` — Delta spec for archive trail
## Merged Specification Requirements
### Modified Requirement: Local Music Browsable Tree
Expanded to include:
- Track title resolution: shows embedded metadata when available, falls back to filename
- Track art resolution: shows embedded album art when available, falls back to generic placeholder
- Graceful degradation: no exceptions on metadata unavailability, continues browsing
### New Requirement: Local Music Sort Mode Navigation
- Browsable, non-playable sort-mode entries on page 0
- "By name" (filename order) always available
- "By audio quality" (bitrate descending) available when folder <= 150 tracks
- Mirrors `ordenarEmisoras(..., calidad)` comparator pattern
### New Requirement: Local Music Alphabetical Name Buckets
- Browsable, non-playable letter-range folders (A-F, G-M, N-S, T-Z)
- Metadata-free partitioning (uses only filename)
- Available when folder > 50 tracks
- Enables name-based filtering on legacy `MediaBrowserService` surface
### New Requirement: Local Track Embedded Album Art Display
- Static URI resolution via FileProvider cache (`content://...fileprovider...`)
- Art cache with LRU eviction (256 files / 32 MB budget)
- Graceful fallback to generic rotating placeholder art
## Implementation Summary
### Delivery Strategy
- Mode: Single PR with `size:exception` (user's established preference for high-risk changes this session)
- Test Mode: Strict TDD (all Dart behavior-changing code), Static-review-only (native Kotlin per project precedent)
- Result: 1979 changed lines, all 137/137 tests passing
### Test Coverage
- 114 tests in `navegacion_auto_test.dart` (encode/decode, buckets, quality sort, page-0 wiring, fallbacks, collision guards)
- 18 tests in `musica_local_auto_test.dart` (LRU cache, metadata channel call, null tolerance)
- 5 tests in `pista_local_test.dart` (DTO construction)
- **Total verified**: 137/137 pass, 0 failures
### Verification Status
- **Verdict**: PASS WITH WARNINGS
- **CRITICAL Issues**: None
- **WARNINGs** (both documentation-accuracy, corrected):
- Apply-progress claimed 138/138 tests (19 in musica_local_auto_test.dart); actual 137/137 (18 tests). Corrected in apply-progress before archive.
- Apply-progress claimed 9 fixture corrections; actual 8. Corrected in apply-progress before archive.
- **SUGGESTIONs** (non-blocking):
- Native Kotlin code (readAudioMetadataBatch, art cache, LRU trim) remains static-review-only per established project precedent (no build/DHU environment)
- On-device validation of 150-track cap and 256-file/32MB art budget deferred to pre-release gate (design "Open Questions")
- Cosmetic CRLF line-ending warnings (not defects)
### Key Design Decisions Verified
1. **Embedded-art delivery**: FileProvider cache (content:// URI) — reuses existing authority, zero new manifest surface
2. **Metadata cache**: Flat 256-entry LRU-by-access in `navegacion_auto.dart` — ensures paging doesn't evict earlier pages
3. **Quality-sort threshold**: Offered when folder <= 150 tracks; above that, name + buckets only (head-unit latency guard)
4. **Media-ID collision safety**: `carpeta_local_ord:` and `carpeta_local_bucket:` prefixes proven non-colliding against all 6 pre-existing prefixes
5. **Page-scoping invariant**: Metadata resolved only for the requested page's 50 items, never the whole folder (except quality-sort, which is opt-in)
## Rollback Profile
Phase 2 is fully additive over Phase 1. If needed:
- Remove `readAudioMetadataBatch` native call
- Drop `_ord` and `_bucket` media-id prefixes
- Restore filename titles + placeholder art
- Phase 1 browse/play/paging remains fully functional
## Outstanding Pre-Release Gates
**Native code validation** (applies to Phases 1, paging, and 2):
- `MediaMetadataRetriever` bitrate/sample-rate/embedded-picture extraction with real SAF URIs (on-device/DHU)
- FileProvider art cache write/read on actual Android file system
- 150-track quality-sort wall-clock latency (design "Open Questions")
- 256-file / 32 MB art cache eviction under realistic browsing patterns
No code changes required — these are validation gates for go-live, not SDD cycle blockers.
## Archive Checklist
- [x] Delta spec merged into main spec (`openspec/specs/android-auto-media/spec.md`)
- [x] All 4 new/modified requirements integrated into spec source of truth
- [x] Change folder moved to archive with ISO date prefix: `openspec/changes/archive/2026-07-19-android-auto-local-music-phase2/`
- [x] All artifacts (proposal, design, tasks, apply-progress, verify-report, delta spec) copied to archive
- [x] Archive report written to both filesystem and engram
- [x] Verification passed (PASS WITH WARNINGS, 0 CRITICAL)
## SDD Cycle Completion
- **Proposed**: android-auto-local-music-phase2
- **Specified**: 1 modified + 3 added requirements
- **Designed**: 5 ADRs, page-scoping invariant, media-ID codec, LRU cache
- **Tasked**: 37 implementation tasks (4 phases, model/native/cache/sort/buckets)
- **Applied**: All 37 tasks completed, single PR, 1979 changed lines, 137/137 tests green
- **Verified**: PASS WITH WARNINGS (0 CRITICAL, 2 WARNINGs corrected)
- **Archived**: Change folder and all artifacts persisted
**Next**: Phase 3 (subfolder scoping, shuffle) when scheduled. Entire local-music feature remains gated on pre-release native validation.
@@ -60,7 +60,7 @@ None.
### WARNING ### WARNING
1. Apply-progress test-count self-report inaccurate: claims 138/138 (114+19+5); actual is 137/137 (114+18+5). musica_local_auto_test.dart has 18 tests, not 19 (verified by both test-runner output and a static test( count). All 137 real tests pass - this is a reporting-accuracy issue, not a functional defect, but it is the third apply-progress numeric discrepancy flagged this session per the orchestrator own warning, and should be corrected before archive. 1. Apply-progress test-count self-report inaccurate: claims 138/138 (114+19+5); actual is 137/137 (114+18+5). musica_local_auto_test.dart has 18 tests, not 19 (verified by both test-runner output and a static test( count). All 137 real tests pass - this is a reporting-accuracy issue, not a functional defect, but it is the third apply-progress numeric discrepancy flagged this session per the orchestrator own warning, and should be corrected before archive.
2. Apply-progress "9 fixtures" claim off by one: git diff shows 8 pre-existing .single assertions converted to .singleWhere(pista-prefixed), not 9. The mechanism/rationale of the fix is verified genuine and non-weakening (narrows to the track item, does not drop any check), but the count itself is inaccurate. 2. Apply-progress "9 fixtures" claim off by one: git diff shows 8 (not 9) pre-existing .single assertions converted to .singleWhere(pista-prefixed), not 9. The mechanism/rationale of the fix is verified genuine and non-weakening (narrows to the track item, does not drop any check), but the count itself is inaccurate.
### SUGGESTION ### SUGGESTION
- Native Kotlin (readAudioMetadataBatch/art cache/LRU trim) remains static-review-only per established project precedent - no build/DHU available in this environment. Design own "Open Questions" already flags the 150-track cap and 256-file/32MB art budget as needing on-device validation before this ships to real hardware; treat that as a pre-merge/pre-release gate, not a blocker for this SDD cycle. - Native Kotlin (readAudioMetadataBatch/art cache/LRU trim) remains static-review-only per established project precedent - no build/DHU available in this environment. Design own "Open Questions" already flags the 150-track cap and 256-file/32MB art budget as needing on-device validation before this ships to real hardware; treat that as a pre-merge/pre-release gate, not a blocker for this SDD cycle.
+73 -2
View File
@@ -197,14 +197,15 @@ The system MUST let the user select a local-music root folder via SAF (`ACTION_O
### Requirement: Local Music Browsable Tree ### Requirement: Local Music Browsable Tree
The Android Auto browse tree MUST expose a new non-playable local-music root folder alongside the existing station folders. Browsing that root and any nested subfolder MUST recursively mirror the picked SAF folder's structure: subfolders as non-playable `carpeta_local:<id>` items and audio files as playable `pista:<id>` items resolving to `PistaLocal` instances, using the raw filename as the displayed title. The Android Auto browse tree MUST expose a new non-playable local-music root folder alongside the existing station folders. Browsing that root and any nested subfolder MUST recursively mirror the picked SAF folder's structure: subfolders as non-playable `carpeta_local:<id>` items and audio files as playable `pista:<id>` items resolving to `PistaLocal` instances. When embedded metadata (title and/or album art) can be resolved for a track on the requested page, the displayed title and art MUST reflect that metadata instead of the raw filename and generic placeholder art. When metadata is unavailable, unparseable, or resolution otherwise fails, the item MUST fall back to the raw filename as title and the existing generic placeholder art, exactly as in Phase 1.
(Previously: always used the raw filename as the displayed title, with no metadata resolution.)
#### Scenario: Car browses the local-music root #### Scenario: Car browses the local-music root
- GIVEN a local music root folder was picked and its permission is valid - GIVEN a local music root folder was picked and its permission is valid
- WHEN `getChildren` is called with the local-music root folder id - WHEN `getChildren` is called with the local-music root folder id
- THEN it returns the root's immediate subfolders as `carpeta_local:<id>` items and audio files as `pista:<id>` playable items - THEN it returns the root's immediate subfolders as `carpeta_local:<id>` items and audio files as `pista:<id>` playable items
- AND each playable item's title is the file's raw filename - AND each playable item's title reflects resolved metadata when available, else the file's raw filename
#### Scenario: Car browses a nested subfolder #### Scenario: Car browses a nested subfolder
@@ -213,6 +214,76 @@ The Android Auto browse tree MUST expose a new non-playable local-music root fol
- THEN it returns that subfolder's own contents (nested folders and/or tracks), recursively mirroring the on-device structure - THEN it returns that subfolder's own contents (nested folders and/or tracks), recursively mirroring the on-device structure
- AND browsing an empty subfolder returns an empty list, not an error - AND browsing an empty subfolder returns an empty list, not an error
#### Scenario: Track has embedded metadata
- GIVEN an audio file on the requested page has parseable embedded title and/or album art
- WHEN it is mapped to a playable `pista:<id>` `MediaItem`
- THEN its `title` and `artUri` reflect the resolved metadata, not the raw filename or generic placeholder
#### Scenario: Metadata unavailable or resolution fails (graceful fallback)
- GIVEN an audio file's embedded metadata is absent, corrupt, in an unsupported format, or blocked by a permission edge case
- WHEN it is mapped to a playable `pista:<id>` `MediaItem`
- THEN its title falls back to the raw filename and its art falls back to the generic placeholder, exactly as in Phase 1
- AND no exception propagates from metadata resolution, and the rest of that page's items are still returned
### Requirement: Local Music Sort Mode Navigation
The local-music browse tree MUST expose sort mode as navigable, non-playable browse-tree entries (at least "by name" and "by audio quality") for a folder's tracks, since the underlying legacy browse surface cannot re-sort an already-rendered folder in place. Selecting a sort-mode entry MUST return that folder's tracks ordered accordingly: name sort orders by filename; quality sort orders by bitrate descending, reusing the existing `ordenarEmisoras(..., OrdenEmisoras.calidad)` comparator shape.
#### Scenario: Sort-mode entries are available for a folder
- GIVEN the user is browsing a local-music folder containing tracks
- WHEN that folder's sort-mode navigation is requested
- THEN at least a "by name" and a "by audio quality" navigable entry are returned
#### Scenario: User selects sort by quality
- GIVEN the user selects the "by audio quality" entry for a folder
- WHEN its children are resolved
- THEN the folder's tracks are returned ordered by bitrate descending
- AND tracks with unknown bitrate are ordered consistently, without throwing
#### Scenario: User selects sort by name
- GIVEN the user selects the "by name" entry for a folder
- WHEN its children are resolved
- THEN the folder's tracks are returned ordered by filename
### Requirement: Local Music Alphabetical Name Buckets
The local-music browse tree MUST expose alphabetical name-bucket entries (e.g. A-F, G-M, ...) as navigable, non-playable folders for a folder's tracks, as the Phase 2 realization of name-based filtering — the underlying legacy `MediaBrowserService` surface does not support a live text-search box.
#### Scenario: User browses name buckets for a folder
- GIVEN a local-music folder contains tracks spanning multiple starting letters
- WHEN the user browses that folder's name-bucket navigation level
- THEN each returned bucket is a non-playable folder covering a contiguous letter range
- AND selecting a bucket returns only the tracks whose name falls within that range
#### Scenario: Bucket with no matching tracks
- GIVEN a name-bucket range that currently matches zero tracks in a folder
- WHEN that bucket is browsed
- THEN it returns an empty list, not an error
### Requirement: Local Track Embedded Album Art Display
A playable `pista:<id>` `MediaItem` MUST display embedded album art via `artUri` when the track's embedded picture can be resolved and served as a static URI. When embedded art is absent or cannot be resolved (per the browse tree's metadata-resolution fallback), the item MUST fall back to Phase 1's existing generic rotating placeholder art (`station_art_*` rotation). The car head unit MUST NOT display an empty or broken art tile for any local track.
#### Scenario: Track has embedded album art
- GIVEN an audio file on the requested page has a resolvable embedded picture
- WHEN it is mapped to a playable `MediaItem`
- THEN `artUri` points at the resolved embedded art, served as a static URI
#### Scenario: Track has no or unresolvable embedded art
- GIVEN an audio file has no embedded picture, or it cannot be parsed or served
- WHEN it is mapped to a playable `MediaItem`
- THEN `artUri` falls back to the same rotating placeholder used in Phase 1
- AND no broken, empty, or indefinitely-loading art tile is shown
### Requirement: Local Track Playback Reuses Existing Pipeline ### Requirement: Local Track Playback Reuses Existing Pipeline
Selecting a `pista:<id>` item MUST resolve to the corresponding `PistaLocal` and play it through the existing `playMediaItem`/`PluriWaveAudioHandler` pipeline, unchanged, including the shared EQ signal chain. Selecting a `pista:<id>` item MUST resolve to the corresponding `PistaLocal` and play it through the existing `playMediaItem`/`PluriWaveAudioHandler` pipeline, unchanged, including the shared EQ signal chain.