Files
pluriwave/openspec/changes/archive/2026-07-19-android-auto-local-music-phase2/tasks.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

14 KiB

Tasks: Android Auto Local Music — Phase 2 (Metadata, Sort, Name Buckets)

Review Workload Forecast

Field Value
Estimated changed lines ~950-1250 (native ~180-230, pista_local.dart ~60, musica_local_auto.dart ~180-220, navegacion_auto.dart ~260-320, new/extended tests ~350-450)
400-line budget risk High
Chained PRs recommended Yes
Suggested split PR 1 -> PR 2 -> PR 3 (see Suggested Work Units)
Delivery strategy single-pr with size:exception (resolved at apply time — user's established preference this session)
Chain strategy N/A — single PR, not chained

Decision needed before apply: No — resolved as single-pr + size:exception Chained PRs recommended: Yes (forecast unchanged; overridden by explicit size:exception) Chain strategy: N/A 400-line budget risk: High (accepted via size:exception)

Rationale: this is the async-conversion blast radius the design explicitly calls out as "the main implementation hazard" — itemsLocales sync-to-async touches every caller (hijosMusicaLocal, both existing callers already await it so the call sites are compatible, but the signature change ripples through ConstructorArbolAuto tests), plus one native batched-extraction method + LRU cache + quality sort + bucket partitioning + media-id codec extension, each independently testable but collectively larger than Phase 1's original browse-tree change.

Suggested Work Units

Unit Goal Likely PR Notes
1 Model + native metadata surface (Tasks 1-3) PR 1 PistaLocal/MetadatosPista DTOs, readAudioMetadataBatch + art cache/LRU trim (static-review-only). Independent — no Dart call sites yet.
2 Metadata cache + async itemsLocales conversion (Tasks 4-5, 10-11) PR 2 THE load-bearing unit — base = PR 1 branch. CacheMetadatosSesion, metadatosDe, async itemsLocales, regression suite. Ships metadata-backed titles/art with existing name-sort browse tree; no sort/bucket UI yet.
3 Sort mode + buckets + media-id prefixes + wiring (Tasks 6-9, 12) PR 3 base = PR 2 branch. New _ord/_bucket prefixes, quality comparator, bucket partitioning, page-0 mode entries.

Phase 1: Foundation — Model & Native Metadata Surface

  • 1.1 RED: test/modelos/pista_local_test.dart (new file) — construct MetadatosPista with all fields null; assert no throw, all getters return null.
  • 1.2 GREEN: lib/modelos/pista_local.dart — add MetadatosPista DTO (titulo, artista, artUri as String?; bitrate, sampleRate as int?), all-nullable const constructor.
  • 1.3 GREEN: extend PistaLocal (lib/modelos/pista_local.dart:29-46) with artista, embeddedArtUri, bitrate, sampleRate fields (nullable, default null), update doc comment (remove stale "Phase 1 minimal shape" note).
  • 1.4 Native (static-review-only, thin): MainActivity.kt — add "readAudioMetadataBatch" case to the file_actions handler (after "hasPersistedPermission", MainActivity.kt:307-313), extracting treeUri: String + documentIds: List<String> args, delegating to a new private readAudioMetadataBatch(treeUri, documentIds): List<Map<String, Any?>>.
  • 1.5 Native (static-review-only): implement readAudioMetadataBatch — per-docId MediaMetadataRetriever extract (METADATA_KEY_TITLE, _ARTIST, _BITRATE, getEmbeddedPicture(); METADATA_KEY_SAMPLERATE gated Build.VERSION.SDK_INT >= 31 per ADR-5), each entry wrapped in its own try/catch -> all-null-but-documentId row on failure, retriever.release() in finally, whole-call try/catch -> []; never throws across the channel boundary (mirrors listAudioChildren/resolvePlayableUri shape, MainActivity.kt:373-426).
  • 1.6 Native (static-review-only): embedded-art cache write — inside the same extract loop, when getEmbeddedPicture() is non-null, write bytes to cacheDir/pluriwave_art/<hash(documentId)> (skip write if file already exists), return content://${applicationId}.fileprovider/cache/pluriwave_art/<hash> (reuses AndroidManifest.xml:97-102 authority + pluriwave_file_paths.xml:6-8 cache-path path="." — confirmed present, zero manifest changes needed).
  • 1.7 Native (static-review-only): after each art write, trim pluriwave_art/ by lastModified (oldest first) while count > 256 OR totalBytes > 32MB.
  • 1.8 Flag clearly in PR description: Tasks 1.4-1.7 are Kotlin, static-review-only per project precedent (no build/DHU here, mirrors listAudioChildren/resolvePlayableUri/pickMusicFolder review treatment).

Phase 2: Metadata Cache & Async itemsLocales Conversion (load-bearing)

  • 2.1 RED: test/servicios/musica_local_auto_test.dartCacheMetadatosSesion group: store 256 entries then a 257th, assert the least-recently-accessed entry (not just least-recently-inserted) is evicted; assert obtener() on a hit refreshes recency order.
  • 2.2 GREEN: lib/servicios/musica_local_auto.dart — add CacheMetadatosSesion (flat LinkedHashMap<String, MetadatosPista>, bound 256, LRU-by-access: obtener re-inserts on hit, guardar evicts entries.first.key when length > 256 after insert).
  • 2.3 RED: musica_local_auto_test.dartFuenteMusicaLocalAutoImpl.metadatosDe group: empty documentIds -> {} without a channel call; channel throws -> {} (never propagates); a native null/missing field in a row -> that key's MetadatosPista has the corresponding field null, not a crash.
  • 2.4 GREEN: lib/servicios/musica_local_auto.dart — add metadatosDe(List<String> documentIds) to FuenteMusicaLocalAuto interface (per design contract) and FuenteMusicaLocalAutoImpl: try/catch-wrapped readAudioMetadataBatch invocation (same pattern as hijos, musica_local_auto.dart:169-189), map rows to Map<String, MetadatosPista> keyed by echoed documentId.
  • 2.5 RED: test/servicios/navegacion_auto_test.dart — extend the ConstructorArbolAuto.itemsLocales group with a metadata-resolution spy test mirroring the existing call-count invariant test (navegacion_auto_test.dart:493-551): 200 nodes, a fake metadatosDe that records the exact documentIds list it received; assert on page 0 it receives EXACTLY the 50 page docIds (not all 200), and page 3 receives exactly the trailing 50 — proves the resolve-only-the-page invariant holds through the async conversion.
  • 2.6 RED: navegacion_auto_test.dart — metadata-present case: a node whose docId resolves to a MetadatosPista with titulo/artUri set -> built MediaItem.title/artUri reflect the metadata, not the filename/placeholder.
  • 2.7 RED: navegacion_auto_test.dart — metadata-absent/failed case: docId not present in the resolved map (or metadatosDe returns {} entirely) -> MediaItem.title falls back to _tituloDesdeNombre, artUri falls back to artUriLocal (exactly Phase 1 behavior) — no exception.
  • 2.8 GREEN: lib/servicios/navegacion_auto.dart — convert itemsLocales (navegacion_auto.dart:363-378) to Future<List<MediaItem>>: after paginaDe slices the page (unchanged, still cheap), call fuente.metadatosDe(paginaActual.where((n) => !n.esDirectorio).map((n) => n.documentId).toList()) for ONLY the sliced page's track docIds, then map via an async-aware construirItem (keep @visibleForTesting injection point for the spy test) that consults the resolved map before falling back to filename/placeholder. Preserve the exact sort -> paginaDe -> map(construir) ordering (slice BEFORE metadata fetch, metadata fetch BEFORE MediaItem build).
  • 2.9 GREEN: lib/servicios/navegacion_auto.dart — update _itemLocal (navegacion_auto.dart:380-391) to accept the resolved Map<String, MetadatosPista> (or become instance-scoped per call), building title/artUri/subtitle from metadata when present, falling back to Phase 1 logic (_tituloDesdeNombre, artUriLocal) when absent — folders (esDirectorio) are unaffected (no metadata lookup for directories).
  • 2.10 GREEN: lib/servicios/navegacion_auto.dart — update hijosMusicaLocal (navegacion_auto.dart:529-558) call site: await constructor.itemsLocales(...) (already inside an async function and already implicitly compatible since the call wasn't previously awaited — now becomes a real await).
  • 2.11 Regression: run full test/servicios/navegacion_auto_test.dart + test/servicios/musica_local_auto_test.dart suites; every existing itemsLocales/construirItem call site in both test files (Tasks reference: 25+ call sites per navegacion_auto_test.dart:493-851 grep) must be updated to await the now-Future call — confirm no other production call site exists (only hijosMusicaLocal calls itemsLocales; verified via Grep in this session, single caller).
  • 2.12 Regression: confirm Phase 1 scenarios (empty subfolder, folder browse, playback resolution, art fallback) and the existing paging spy test (navegacion_auto_test.dart:493-551, adapted for the new async signature) still pass unchanged in behavior.

Phase 3: Quality Sort, Name Buckets, Media-ID Wiring

  • 3.1 RED: navegacion_auto_test.dart — quality-sort comparator: tracks with known bitrate sort descending; a track with bitrate: null sorts after all known-bitrate tracks, never throws.
  • 3.2 GREEN: lib/servicios/navegacion_auto.dart — add quality comparator for PistaLocal/MetadatosPista bitrate desc, reusing OrdenEmisoras.calidad's shape (lib/estado/orden_emisoras.dart:14) as the mirrored pattern (no code sharing forced — different types).
  • 3.3 RED: navegacion_auto_test.dart_maxPistasParaOrdenCalidad boundary: folder with 149 tracks -> quality entry present; 150 -> present; 151 -> quality entry OMITTED from page-0 mode entries.
  • 3.4 GREEN: lib/servicios/navegacion_auto.dart — add static const _maxPistasParaOrdenCalidad = 150; quality-sort path batch-parses ALL folder tracks via metadatosDe (not page-scoped — full-folder, per ADR-3), sorts bitrate desc, caches, then applies existing paginaDe.
  • 3.5 RED: test/servicios/navegacion_auto_test.dart (new group bucketsDe) — partitioning: tracks named across multiple letters split into contiguous alphabetical buckets (e.g. A-F/G-M/...); a bucket with zero matches returns [] not an error; partitioning uses ONLY NodoLocal.nombre (metadata-free) — assert via a spy that metadatosDe is never called for bucket partitioning itself.
  • 3.6 GREEN: lib/servicios/navegacion_auto.dart — implement bucketsDe(List<NodoLocal>) pure-Dart, name-only partitioning (no metadata dependency), only offered when folder track count > 50 (design ADR-4).
  • 3.7 RED: navegacion_auto_test.dart — media-id encode/decode round-trip for carpeta_local_ord:<modo>:<pagina>:<docId> and carpeta_local_bucket:<idxBucket>:<pagina>:<docId>, including a docId containing :// surviving verbatim (split-on-first-colon-after-fixed-fields chain, mirroring paginaCarpetaLocalDesde, navegacion_auto.dart:319-325).
  • 3.8 RED: navegacion_auto_test.dart — collision guards: assert esCarpetaLocalOrdMediaId/esCarpetaLocalBucketMediaId never both match the same id, and neither matches any of the other 4 existing prefixes (emisora:, grupo:, eq_preset:, carpeta_local:, carpeta_local_pag:, pista:) for representative sample ids of each.
  • 3.9 GREEN: lib/servicios/navegacion_auto.dart — add _prefijoCarpetaLocalOrd = 'carpeta_local_ord:', _prefijoCarpetaLocalBucket = 'carpeta_local_bucket:' constants + esCarpetaLocalOrdMediaId/esCarpetaLocalBucketMediaId + decode helpers (mirroring paginaCarpetaLocalDesde's split-on-first-colon-after-fixed-fields pattern for the extra modo/idxBucket field).
  • 3.10 RED: navegacion_auto_test.dart — page-0 mode entries: a folder with <=150 tracks and >50 tracks returns BOTH a quality-sort entry AND bucket entries prepended before the name-sorted list on page 0 only (mirrors carpetasFavoritos prepend precedent, navegacion_auto.dart:416-430); page >0 never re-prepends them.
  • 3.11 GREEN: lib/servicios/navegacion_auto.dart — wire mode/bucket entries into itemsLocales/hijosMusicaLocal on page 0 only, hardcoded Spanish labels ("Ordenar por calidad", bucket range labels e.g. "A-F") — matching the established car-tree precedent (_tituloMasLocal = 'Más…', _carpeta(idFavoritos, 'Favoritos'), none of which go through AppLocalizations). Do NOT add new keys to lib/l10n/*.arb — these are car-tree-only labels, not phone UI; the only existing local-music AppLocalizations key (localMusicFolderGenericName) is phone-settings-only (pantalla_ajustes.dart:366), confirming the precedent split. If a genuinely new PHONE-facing string is introduced (none identified in spec/design as of this task pass), scope it into ALL 13 lib/l10n/*.arb files, not just en/es.
  • 3.12 GREEN: lib/servicios/navegacion_auto.dart — route the new _ord/_bucket media ids through hijosMusicaLocal's dispatch (alongside existing carpeta_local:/carpeta_local_pag: branches, navegacion_auto.dart:536-546).

Phase 4: Art Fallback & Final Regression

  • 4.1 RED: navegacion_auto_test.dart — art fallback matrix: cache-miss (native returns artUri: null) -> placeholder; parse-failure (metadata entry all-null for that docId) -> placeholder; never an empty/broken tile in any case.
  • 4.2 GREEN: confirm _itemLocal's (Task 2.9) fallback branch already covers 4.1 — no new production code expected, this task is verification-only; if a gap is found, fix in navegacion_auto.dart.
  • 4.3 Full regression: run entire test/servicios/navegacion_auto_test.dart + test/servicios/musica_local_auto_test.dart + new test/modelos/pista_local_test.dart suites; confirm Phase 1 scenarios and paging invariant (Task 2.12) remain green.
  • 4.4 Deviated/manual-follow-up (not executable here, same convention as prior changes): flutter analyze, flutter test --coverage, flutter gen-l10n (only if 3.11's l10n gap is ever confirmed) — run manually before merge, not part of this task list's automated scope.
  • 4.5 Deviated/manual-follow-up: on-device hardware validation of _maxPistasParaOrdenCalidad = 150 and the 256-file/32MB art budget (design "Open Questions") — no DHU/emulator coverage in this task list.

Requirement Traceability

Spec Requirement Tasks
Local Music Browsable Tree (metadata title/art) 1.1-1.7, 2.1-2.12, 4.1-4.3
Local Music Sort Mode Navigation 3.1-3.4, 3.9-3.12
Local Music Alphabetical Name Buckets 3.5-3.6, 3.9-3.12
Local Track Embedded Album Art Display 1.6-1.7, 2.6-2.9, 4.1-4.2