# 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 - [x] 1.1 RED: `test/modelos/pista_local_test.dart` (new file) — construct `MetadatosPista` with all fields null; assert no throw, all getters return `null`. - [x] 1.2 GREEN: `lib/modelos/pista_local.dart` — add `MetadatosPista` DTO (`titulo`, `artista`, `artUri` as `String?`; `bitrate`, `sampleRate` as `int?`), all-nullable const constructor. - [x] 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). - [x] 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` args, delegating to a new private `readAudioMetadataBatch(treeUri, documentIds): List>`. - [x] 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`). - [x] 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/` (skip write if file already exists), return `content://${applicationId}.fileprovider/cache/pluriwave_art/` (reuses `AndroidManifest.xml:97-102` authority + `pluriwave_file_paths.xml:6-8` `cache-path path="."` — confirmed present, zero manifest changes needed). - [x] 1.7 Native (static-review-only): after each art write, trim `pluriwave_art/` by `lastModified` (oldest first) while `count > 256 OR totalBytes > 32MB`. - [x] 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) - [x] 2.1 RED: `test/servicios/musica_local_auto_test.dart` — `CacheMetadatosSesion` 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. - [x] 2.2 GREEN: `lib/servicios/musica_local_auto.dart` — add `CacheMetadatosSesion` (flat `LinkedHashMap`, bound 256, LRU-by-access: `obtener` re-inserts on hit, `guardar` evicts `entries.first.key` when `length > 256` after insert). - [x] 2.3 RED: `musica_local_auto_test.dart` — `FuenteMusicaLocalAutoImpl.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. - [x] 2.4 GREEN: `lib/servicios/musica_local_auto.dart` — add `metadatosDe(List 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` keyed by echoed `documentId`. - [x] 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. - [x] 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. - [x] 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. - [x] 2.8 GREEN: `lib/servicios/navegacion_auto.dart` — convert `itemsLocales` (`navegacion_auto.dart:363-378`) to `Future>`: 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). - [x] 2.9 GREEN: `lib/servicios/navegacion_auto.dart` — update `_itemLocal` (`navegacion_auto.dart:380-391`) to accept the resolved `Map` (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). - [x] 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`). - [x] 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). - [x] 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 - [x] 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. - [x] 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). - [x] 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. - [x] 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`. - [x] 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. - [x] 3.6 GREEN: `lib/servicios/navegacion_auto.dart` — implement `bucketsDe(List)` pure-Dart, name-only partitioning (no metadata dependency), only offered when folder track count > 50 (design ADR-4). - [x] 3.7 RED: `navegacion_auto_test.dart` — media-id encode/decode round-trip for `carpeta_local_ord:::` and `carpeta_local_bucket:::`, including a docId containing `:`/`/` surviving verbatim (split-on-first-colon-after-fixed-fields chain, mirroring `paginaCarpetaLocalDesde`, `navegacion_auto.dart:319-325`). - [x] 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. - [x] 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). - [x] 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. - [x] 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. - [x] 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 - [x] 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. - [x] 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`. - [x] 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. - [x] 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. - [x] 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 |