Phase 1: pick a device folder via SAF (persisted grant, no new permission), browse its nested subfolders/tracks as a 5th Android Auto root folder (hidden until configured), and play tracks through the existing pipeline (EQ, art rotation, cold-start-safe source). No metadata/sort/filter/shuffle yet -- filename is the title, generic rotating art is the placeholder; deferred to a follow-up phase. Adds a new pluriwave/file_actions native method (listAudioChildren) and an onActivityResult override in MainActivity for the SAF folder picker -- both static-review-only, no Android build available here.
382 lines
24 KiB
Markdown
382 lines
24 KiB
Markdown
# Tasks: Android Auto Local Music — Phase 1
|
|
|
|
**Apply status: all 12 task groups complete (single-pass batch, `size:exception`).**
|
|
See `openspec/changes/android-auto-local-music/apply-progress.md` for the
|
|
full implementation report, TDD evidence table, and deviations.
|
|
|
|
Scope: Phase 1 only (foundational plumbing). Grounded against live code as of this
|
|
writing — see "Grounding notes" per task for exact file:line anchors re-verified
|
|
during this pass (not trusted from spec/design alone).
|
|
|
|
Strict TDD Mode is ACTIVE for this project. Every pure-Dart, behavior-changing task
|
|
below follows red → green → refactor. Native Kotlin and any on-device SAF picker flow
|
|
is **static-review-only** (this project's established precedent — same as the
|
|
Android Auto EQ-presets and browsable-tree changes) and is flagged explicitly per
|
|
task. `flutter build`/`flutter analyze`/`flutter gen-l10n` are NOT executable tasks
|
|
here — they are manual/CI follow-ups, same convention as prior archived changes.
|
|
|
|
## Grounding corrections vs. spec/design (read first)
|
|
|
|
- **Root folder count test**: `test/servicios/navegacion_auto_test.dart:221-244`
|
|
currently asserts `raiz()` returns exactly 4 folders (`hasLength(4)`), a `Set` of
|
|
the 4 ids, and `raiz.last.id == idEcualizador`. This MUST become 5, same pattern
|
|
as the EQ-presets change had to update this same block. Confirmed live, not
|
|
assumed from design.
|
|
- **`raiz()` signature**: `lib/servicios/navegacion_auto.dart:171-176` currently
|
|
takes no parameters. Design's `raiz(incluirMusicaLocal: ...)` is a real signature
|
|
change — every existing call site of `raiz()` must be checked
|
|
(`lib/servicios/servicio_audio.dart:739` is the only call site found).
|
|
- **`file_actions` channel**: `MainActivity.kt:215-250` currently has exactly 3
|
|
methods (`openDirectory`, `viewDirectory`, `openFile`), all synchronous, all using
|
|
`startActivity` (never `startActivityForResult`). There is **no existing
|
|
`onActivityResult` override in this Activity** — confirmed via full-file read.
|
|
Adding `pickMusicFolder` via `startActivityForResult` requires adding an
|
|
`onActivityResult` override (or an `ActivityResultLauncher`) that does not exist
|
|
today. This is new surface on `MainActivity`, exactly as design's "Open Questions"
|
|
flags — call this out again at task level since it's the highest-risk native piece.
|
|
- **Manifest/pubspec claim — CONFIRMED correct**: `AndroidManifest.xml` (root,
|
|
1-122) has no `READ_MEDIA_AUDIO`/storage permission today, and `pubspec.yaml`
|
|
already lists `file_picker: ^8.1.7` (used elsewhere for `_SeccionGrabaciones`'s
|
|
path picker, NOT for SAF tree URIs) with no `shared_storage` or similar. Design's
|
|
"no manifest/pubspec changes" claim holds — `ACTION_OPEN_DOCUMENT_TREE` +
|
|
`takePersistableUriPermission` need no manifest entry. Flagging as verified, not
|
|
assumed.
|
|
- **Design deliberately does NOT reuse `file_picker`'s `getDirectoryPath()`** (the
|
|
pattern `_SeccionGrabaciones` uses at `pantalla_ajustes.dart:96`) for the local
|
|
music root — that API returns a plain path, not a URI with a persistable grant.
|
|
The new `_SeccionMusicaLocal` therefore calls the NEW native `pickMusicFolder`
|
|
method directly via the `file_actions` `MethodChannel`, not `FilePicker.platform`.
|
|
- **`_SeccionGrabaciones` is the closest UI precedent** (`pantalla_ajustes.dart:89-279`):
|
|
`PluriGlassSurface` card, `Row` header with icon + title, `FutureBuilder` for the
|
|
current path, `Wrap` of `OutlinedButton.icon`/`FilledButton.tonalIcon` actions,
|
|
`ScaffoldMessenger` snackbar feedback. `_SeccionMusicaLocal` should mirror this
|
|
shape (registered in the `PantallaAjustes` sections list at
|
|
`pantalla_ajustes.dart:63-83`).
|
|
- **SharedPreferences DI pattern**: `servicio_ecualizador.dart:37,54,57` — constructor
|
|
takes an optional injected `SharedPreferences? prefs`, falls back to
|
|
`SharedPreferences.getInstance()`. New code (`FuenteMusicaLocalAuto` impl / a
|
|
settings-side service) should follow this exact injectable pattern for testability.
|
|
|
|
## 1. `PistaLocal` / `NodoLocal` models (pure Dart — unit-testable)
|
|
|
|
- [x] 1.1 Create `lib/modelos/pista_local.dart` with `NodoLocal` (`documentId`,
|
|
`nombre`, `esDirectorio`) and `PistaLocal` (`documentId`, `titulo`
|
|
derived-at-construction or computed, `contentUri`) per design's minimal
|
|
Phase 1 shape — no metadata fields (artist/album/duration) per spec's
|
|
"Not in this delta".
|
|
- [x] 1.2 Unit tests for any parsing/equality helpers on these models (if added).
|
|
If the models are pure DTOs with no logic, skip — do not write tests for
|
|
getter-only classes with no behavior.
|
|
- Requirement: Local Music Browsable Tree (spec, `PistaLocal` resolution).
|
|
- Parallel: yes — no dependency on other tasks.
|
|
|
|
## 2. `esArchivoAudio` — Dart-side re-validation (pure Dart — unit-testable, TDD)
|
|
|
|
- [x] 2.1 RED: write failing tests in a new/extended test file (or
|
|
`navegacion_auto_test.dart` if colocated) for `esArchivoAudio(mime, nombre)`:
|
|
accepts `audio/*` MIME, rejects `null`/non-audio MIME even with an audio-like
|
|
extension, rejects blank/null inputs — belt-and-suspenders per design's
|
|
"Interfaces / Contracts" note (native already filters, Dart re-validates).
|
|
- [x] 2.2 GREEN: implement `esArchivoAudio` (location: `navegacion_auto.dart` or
|
|
`musica_local_auto.dart`, per task 4's file placement) to pass.
|
|
- Requirement: Local Music Browsable Tree (spec — audio files as playable items).
|
|
- Parallel: yes, can run alongside task 1.
|
|
|
|
## 3. Media-id scheme — encode/decode (pure Dart — unit-testable, TDD)
|
|
|
|
- [x] 3.1 RED: tests for `musica_local` root id, `carpeta_local:<docId>` /
|
|
`pista:<docId>` predicates and id-stripping, mirroring the existing
|
|
`esPresetMediaId`/`esCarpetaGrupo` test patterns
|
|
(`navegacion_auto_test.dart:246-264`). Include collision tests against
|
|
`emisora:`, `grupo:`, `eq_preset:` and the bare folder-id constants
|
|
(`idFavoritos`, `idTodas`, `idMisEmisoras`, `idEcualizador`) — same
|
|
collision-free requirement the existing prefixes document at
|
|
`navegacion_auto.dart:14-24,149`.
|
|
Cover the "prefix stripped by length, not by string ops that would mangle a
|
|
documentId containing `:`" case explicitly (design's stated rationale for
|
|
length-based stripping) — pick a docId fixture containing a `:` (SAF
|
|
documentIds commonly look like `primary:Music/Local`).
|
|
- [x] 3.2 GREEN: implement `idMusicaLocal`, `_prefijoCarpetaLocal`,
|
|
`_prefijoPista` constants + `esCarpetaLocalMediaId`/`esPistaMediaId`
|
|
predicates in `navegacion_auto.dart`, following the exact shape of
|
|
`_prefijoGrupo`/`esCarpetaGrupo` (`navegacion_auto.dart:149,225`) and
|
|
`_prefijoPresetEq`/`esPresetMediaId` (`navegacion_auto.dart:18,24`).
|
|
- Requirement: Local Music Browsable Tree (spec — media-id scheme, collision-free).
|
|
- Sequential: blocks tasks 5 and 7 (they route on these predicates).
|
|
|
|
## 4. `FuenteMusicaLocalAuto` abstraction + channel-backed impl
|
|
|
|
- [x] 4.1 RED: unit tests for a FAKE `FuenteMusicaLocalAuto` implementation
|
|
exercising the pure orchestration logic that will consume it (folded into
|
|
task 5/6's tests) — the interface itself
|
|
(`hayCarpetaConfigurada`/`hijos`/`uriContenidoDePista`) has no logic to
|
|
red/green in isolation; test it through its consumers.
|
|
- [x] 4.2 GREEN: create `lib/servicios/musica_local_auto.dart` with the
|
|
`FuenteMusicaLocalAuto` abstract class (design's "Interfaces / Contracts")
|
|
and a channel-backed implementation that calls the `file_actions`
|
|
`MethodChannel`'s `listAudioChildren`/`resolvePlayableUri`/
|
|
`hasPersistedPermission` methods (task 8's native methods), wrapping every
|
|
channel call in try/catch → never-throws per design (mirrors
|
|
`FuenteEmisorasAutoLocal`'s cold-start-safe try/catch shape,
|
|
`navegacion_auto.dart:421-471`).
|
|
- Requirement: Local Music Root Access and Permission Persistence (spec).
|
|
- Sequential: depends on task 8 (native method names/wire shape) for the real
|
|
impl, but the interface + a FAKE impl can be written in parallel with task 8.
|
|
|
|
## 5. `ConstructorArbolAuto` / tree extension (pure Dart — unit-testable, TDD)
|
|
|
|
- [x] 5.1 RED: extend `navegacion_auto_test.dart`'s
|
|
`group('ConstructorArbolAuto.raiz', ...)` (`:221-244`) — the count MUST
|
|
become 5, order MUST be Favoritos, Todas, Mis emisoras, **Música Local**,
|
|
Ecualizador (Música Local now 4th, Ecualizador remains last per design ADR
|
|
"local root ... placed before Ecualizador"). Add a SEPARATE test group for
|
|
the hidden-until-configured case: `raiz(incluirMusicaLocal: false)` (or
|
|
equivalent) omits the folder — 4 folders, matching the OLD assertion shape,
|
|
so the "unconfigured" case is byte-identical to pre-change behavior
|
|
(regression guard, same pattern as the empty-favorite-group precedent design
|
|
cites).
|
|
- [x] 5.2 RED: tests for `itemsLocales` (native node list → `MediaItem` mapping):
|
|
alphabetical sort, `_maxItemsCarpetaLocal = 50` truncation cap (mirror
|
|
`test/.../navegacion_auto_test.dart:398`'s `hasLength(50)` pattern for the
|
|
existing `_maxItemsPorCarpeta` cap), title = filename minus last `.ext`
|
|
(with "whole name if no dot" and "non-empty fallback constant if blank/null"
|
|
cases each as their own case), `artUriLocal` = existing `station_art_*`
|
|
rotation seeded by `documentId` via `indiceArtePara` (reuse, do not
|
|
reimplement — assert against `artUriPara`'s existing rotation constant
|
|
order, `_nombresArte` at `navegacion_auto.dart:33`).
|
|
- [x] 5.3 RED: test empty-subfolder browse returns `[]` not an error (spec
|
|
"browsing an empty subfolder").
|
|
- [x] 5.4 GREEN: implement `idMusicaLocal` addition to `raiz()`
|
|
(`navegacion_auto.dart:171-176`, now parameterized), `itemsLocales(nodos)`,
|
|
title-stripping helper, `artUriLocal(documentId)` reusing
|
|
`indiceArtePara`/`_nombresArte`.
|
|
- Requirement: Local Music Browsable Tree; Local Music Folder Item Cap;
|
|
MODIFIED "Browsable Media Tree" (spec — 5-folder root, order, cap).
|
|
- Sequential: depends on task 3 (predicates/constants) and task 1 (`NodoLocal`).
|
|
|
|
## 6. `getChildren` dispatch wiring (pure Dart — unit-testable, TDD)
|
|
|
|
- [x] 6.1 RED: tests (can live in a new test file exercising
|
|
`PluriWaveAudioHandler.getChildren` the way existing tests exercise it, or
|
|
as pure-function tests if the dispatch logic is extracted into
|
|
`navegacion_auto.dart` first — prefer extraction, matching the existing
|
|
"thin delegation" pattern at `servicio_audio.dart:731-765`) for: root
|
|
request includes/excludes Música Local per `hayCarpetaConfigurada()`;
|
|
`musica_local` id → `fuente.hijos('')`; `carpeta_local:<id>` → `fuente.hijos(id)`;
|
|
cold-start (`fuente` local source unset/errors) → `[]`, never throws,
|
|
mirroring the existing root `try/catch → const []` shape
|
|
(`servicio_audio.dart:736-764`).
|
|
- [x] 6.2 GREEN: add the new branches to `getChildren`
|
|
(`servicio_audio.dart:731-765`) — insert BEFORE the generic
|
|
`_listaParaCarpeta` fallthrough at the bottom, same branch-ordering
|
|
convention as the existing `idFavoritos`/`esCarpetaGrupo` special-cases.
|
|
- Requirement: Local Music Browsable Tree; MODIFIED "Browsable Media Tree" —
|
|
"Browse requested before app state is loaded" regression scenario.
|
|
- Sequential: depends on tasks 3, 4, 5.
|
|
|
|
## 7. `playFromMediaId` wiring for `pista:<docId>` (pure Dart — unit-testable, TDD)
|
|
|
|
- [x] 7.1 RED: tests for `reproducirPistaLocal` (or equivalently named function,
|
|
mirroring `reproducirPorMediaId`'s shape at `navegacion_auto.dart:301-325`):
|
|
resolves via `fuente.uriContenidoDePista(docId)`, builds a `MediaItem` and
|
|
delegates to an injected `reproducir` callback; stale/unknown docId
|
|
(`uriContenidoDePista` returns `null`) is a no-op — `reproducir` is never
|
|
called, no exception (spec "Unknown or stale track id"). Same fake-callback
|
|
test shape as the existing `reproducirPorMediaId` tests.
|
|
- [x] 7.2 RED: EQ regression-guard test (spec "EQ still applies to local track
|
|
playback") — asserts the local-track play path calls the SAME
|
|
`playMediaItem` injection point stations use, with no separate/bypassed
|
|
path. This can be asserted structurally (same signature shape as
|
|
`reproducirPorMediaId`, no alternate EQ seam) plus a wiring test at the
|
|
`PluriWaveAudioHandler.playFromMediaId` level confirming `pista:` routes
|
|
into `playMediaItem` exactly like the existing `emisora:` branch at
|
|
`servicio_audio.dart:805-811`.
|
|
- [x] 7.3 GREEN: implement `reproducirPistaLocal` in `navegacion_auto.dart`; wire
|
|
the `pista:` branch into `playFromMediaId`
|
|
(`servicio_audio.dart:781-820`) — insert as a branch parallel to the
|
|
existing `esPresetMediaId(...)` early-return
|
|
(`servicio_audio.dart:792-804`) and the trailing `reproducirPorMediaId`
|
|
call, preserving the existing outer try/catch (`:786-819`) so a thrown
|
|
resolution error still can't propagate from the handler.
|
|
- Requirement: Local Track Playback Reuses Existing Pipeline (spec, all 3
|
|
scenarios).
|
|
- Sequential: depends on tasks 3 and 4.
|
|
|
|
## 8. Native `file_actions` channel extension (Kotlin — static-review-only)
|
|
|
|
- [x] 8.1 STATIC REVIEW ONLY. Add `pickMusicFolder` to `MainActivity.kt`'s
|
|
`file_actions` handler (`:218-250`): launch
|
|
`Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)` via `startActivityForResult`
|
|
(NEW to this Activity — no existing `onActivityResult` override exists
|
|
today, confirmed via full-file read; this task must ADD one), call
|
|
`contentResolver.takePersistableUriPermission(uri, FLAG_GRANT_READ_URI_PERMISSION)`
|
|
on result, and return the picked tree URI (or `null` on cancel) back to
|
|
Dart via the pending `MethodChannel.Result` held across the
|
|
activity-result round trip. Follow the existing `result.success(...)` /
|
|
`Log.d(tag, "file_actions.<method> ...")` conventions used by
|
|
`openDirectory`/`viewDirectory`/`openFile`.
|
|
- [x] 8.2 STATIC REVIEW ONLY. Add `listAudioChildren(treeUri, parentDocumentId)`:
|
|
resolve the tree via `DocumentFile.fromTreeUri`, walk ONE level (lazy, per
|
|
design "never an eager tree dump"), filter files to `audio/*` MIME, return
|
|
`[{documentId, nombre, esDirectorio}]`.
|
|
- [x] 8.3 STATIC REVIEW ONLY. Add `resolvePlayableUri(treeUri, documentId)`:
|
|
resolve a leaf documentId to its playable `content://` URI.
|
|
- [x] 8.4 STATIC REVIEW ONLY. Add `hasPersistedPermission(treeUri)`: checks
|
|
`contentResolver.persistedUriPermissions` for the stored tree URI, used for
|
|
cold-start / revoked-permission detection (task 9).
|
|
- [x] 8.5 STATIC REVIEW ONLY. Register all 4 new methods in the existing `when
|
|
(call.method)` block (`:219-249`), preserving the existing
|
|
`else -> result.notImplemented()` fallthrough.
|
|
- Requirement: Local Music Root Access and Permission Persistence (spec, all 3
|
|
scenarios); Local Music Browsable Tree.
|
|
- Parallel: independent of the Dart tasks above except for wire-shape agreement
|
|
with task 4's channel-call argument/return names — coordinate field names
|
|
(`documentId`, `nombre`, `esDirectorio`) exactly between 4.2 and 8.2/8.3.
|
|
- FLAG: highest native risk in this delta — `startActivityForResult` +
|
|
`onActivityResult` is genuinely new plumbing on `MainActivity`, cannot be
|
|
runtime-verified in this pass (design's own "Open Questions" says the same).
|
|
Reviewer should pay particular attention to: result-code handling on user
|
|
cancel, and correctly returning to the SAME pending `MethodChannel.Result`
|
|
(not a stale one) if the user backgrounds the app during the picker.
|
|
|
|
## 9. Phone-side settings UI — `_SeccionMusicaLocal` (Flutter widget — manual/limited-test)
|
|
|
|
- [x] 9.1 Create `_SeccionMusicaLocal` in `pantalla_ajustes.dart`, mirroring
|
|
`_SeccionGrabaciones`'s shape (`:89-279`): `PluriGlassSurface` card,
|
|
`FutureBuilder`-driven current-folder display (or "not configured" state),
|
|
a "Choose folder" `OutlinedButton.icon` that invokes the native
|
|
`pickMusicFolder` channel method directly (NOT `FilePicker.platform` — see
|
|
"Grounding corrections"), snackbar feedback via `ScaffoldMessenger`
|
|
following the exact try/catch/snackbar shape at `:100-111`.
|
|
- [x] 9.2 Persist the picked tree URI to SharedPreferences under
|
|
`musica_local_uri`, using the injectable-prefs pattern from
|
|
`servicio_ecualizador.dart:37,54,57` (constructor-injected
|
|
`SharedPreferences?`, falls back to `.getInstance()`) — whichever
|
|
service/class owns this read/write (likely `FuenteMusicaLocalAuto`'s
|
|
concrete impl or a small dedicated settings service).
|
|
- [x] 9.3 Register `_SeccionMusicaLocal()` in `PantallaAjustes`'s section list
|
|
(`pantalla_ajustes.dart:63-83`) — placement is a phone-UI decision, not
|
|
constrained by the car's root-folder ordering; place near
|
|
`_SeccionGrabaciones` given the shared "local files" theme.
|
|
- [x] 9.4 Add new l10n keys to `lib/l10n/app_en.arb` and `lib/l10n/app_es.arb`
|
|
(folder-picker dialog title, "not configured" state text, success/error
|
|
snackbar text — mirroring `recordingsFolderDialogTitle`,
|
|
`recordingsPathUpdated`, `recordingsPathSaveError` keys). The other 12
|
|
locale `.arb` files (`app_ru.arb`, `app_zh.arb`, `app_ja.arb`, `app_pt.arb`,
|
|
`app_fr.arb`, `app_hi.arb`, `app_id.arb`, `app_it.arb`, `app_de.arb`,
|
|
`app_ar.arb`, `app_bn.arb`, plus `gen-l10n` regeneration of
|
|
`lib/l10n/gen/*`) are DEVIATED / manual follow-up — same convention as
|
|
prior archived changes, NOT an executable task here.
|
|
- Requirement: Local Music Root Access and Permission Persistence (spec, "User
|
|
picks a local music root folder").
|
|
- Sequential: depends on task 8 (channel method must exist for 9.1 to call) —
|
|
but the widget SHELL/layout can be built against a stubbed channel call in
|
|
parallel with task 8's implementation.
|
|
- Note: this is Flutter widget code with a native-channel side effect and an
|
|
actual SAF picker dialog — genuinely on-device-only verification for the
|
|
full picker flow (same as task 8's flag). The widget layout/state-management
|
|
logic itself can get light `flutter_test` widget-test coverage (folder-display
|
|
states, button presence) if useful, but the SAF round-trip cannot be unit
|
|
tested — call this out in the PR description.
|
|
|
|
## 10. Cold-start / permission-revoked safety (pure Dart — unit-testable, TDD)
|
|
|
|
- [x] 10.1 RED: tests asserting `hayCarpetaConfigurada()` returning `false` (no
|
|
folder ever picked) yields `raiz()` WITHOUT the Música Local folder — same
|
|
assertion as task 5.1's hidden-folder case, cross-referenced here for the
|
|
"never picked" scenario specifically (spec "Permission revoked or never
|
|
granted", first half).
|
|
- [x] 10.2 RED: tests asserting a FAKE `FuenteMusicaLocalAuto` whose
|
|
`hijos()`/`uriContenidoDePista()` simulate a revoked-permission failure
|
|
(channel throws or returns empty) degrade to `[]`/`null` — never throws out
|
|
of `getChildren`/`playFromMediaId` — mirroring
|
|
`FuenteEmisorasAutoLocal.favoritos()`'s try/catch → `const []` cold-start
|
|
pattern (`navegacion_auto.dart:421-431`).
|
|
- [x] 10.3 GREEN: any missing guard clauses from tasks 4/6/7 to satisfy 10.1/10.2
|
|
(should mostly already be covered if those tasks' try/catch wrapping is
|
|
done correctly — this task exists to make the safety net EXPLICIT and
|
|
independently tested, not just incidentally covered).
|
|
- Requirement: Local Music Root Access and Permission Persistence (spec,
|
|
"Permission revoked or never granted"); MODIFIED "Browsable Media Tree"
|
|
("Browse requested before app state is loaded").
|
|
- Sequential: depends on tasks 4, 5, 6, 7.
|
|
|
|
## 11. Root-folder-count regression update (pure Dart — TDD, explicit "don't forget")
|
|
|
|
- [x] 11.1 **DO NOT SKIP** — same trap as the EQ-presets change: update
|
|
`test/servicios/navegacion_auto_test.dart:221-244`'s
|
|
`group('ConstructorArbolAuto.raiz', ...)` from asserting 4 folders to 5
|
|
(configured case) — already covered by task 5.1, listed here again
|
|
standalone so it cannot be silently dropped if task 5 is split across
|
|
commits/PRs.
|
|
- Requirement: MODIFIED "Browsable Media Tree" — "Car requests the root" scenario.
|
|
- Sequential: must land in the SAME commit as task 5's `raiz()` change (a
|
|
green-but-stale test count is a false-positive regression risk otherwise).
|
|
|
|
## 12. Full regression pass (pure Dart — unit-testable, run don't write)
|
|
|
|
- [x] 12.1 Run the full `navegacion_auto_test.dart` suite plus any
|
|
`servicio_audio_test.dart`/EQ-related tests after tasks 1-11 land —
|
|
confirm no existing radio/favorite-groups/EQ-presets assertions broke from
|
|
the new 5th folder or the extended `getChildren`/`playFromMediaId`
|
|
dispatch. This is a verification run, not new test-writing — flag any
|
|
break found as a task-11-adjacent fix, not a new task.
|
|
- Requirement: all existing spec requirements (regression guard, implicit).
|
|
- Sequential: last — depends on everything above.
|
|
|
|
## Deviated / manual follow-up (not executable tasks here)
|
|
|
|
- `flutter analyze`, `flutter build`, `flutter gen-l10n` — CI/manual, same
|
|
convention as prior archived changes.
|
|
- On-device manual verification of the SAF folder-picker flow (task 8/9) — cannot
|
|
be unit tested, requires an actual Android Auto head unit or emulator + a real
|
|
device folder with audio files.
|
|
- 12 non-English/non-Spanish `.arb` locale files (task 9.4) — translation is
|
|
out of scope for this delta.
|
|
|
|
## Review Workload Forecast
|
|
|
|
**Estimated changed lines**: ~750-950 (additions + deletions), across:
|
|
|
|
| Area | File(s) | Est. lines |
|
|
|---|---|---|
|
|
| New Dart model | `lib/modelos/pista_local.dart` | ~30-50 |
|
|
| New Dart service | `lib/servicios/musica_local_auto.dart` | ~90-130 |
|
|
| Modified Dart | `lib/servicios/navegacion_auto.dart` | ~120-170 (new constants, predicates, `raiz()` signature change, `itemsLocales`, `reproducirPistaLocal`, title/art helpers) |
|
|
| Modified Dart | `lib/servicios/servicio_audio.dart` | ~40-60 (getChildren branches, playFromMediaId branch) |
|
|
| Modified Dart | `lib/pantallas/pantalla_ajustes.dart` | ~130-180 (new `_SeccionMusicaLocal` class, mirrors `_SeccionGrabaciones`'s ~190 lines but narrower scope) |
|
|
| New Kotlin | `MainActivity.kt` | ~120-170 (4 new methods + `onActivityResult` override + pending-result plumbing — genuinely new to this file) |
|
|
| Tests | `navegacion_auto_test.dart` + new test file(s) | ~180-250 |
|
|
| l10n | `app_en.arb`, `app_es.arb` (+ generated) | ~20-30 |
|
|
|
|
**Chained PRs recommended: Yes.** This is the largest and most structurally novel
|
|
change this session — it is the first delta in this project that adds NEW native
|
|
platform-channel surface (`startActivityForResult`/`onActivityResult`, absent
|
|
today) rather than extending an already-lazy, already-tested Dart dispatch pattern
|
|
alone (unlike the EQ-presets and browsable-tree changes, which were pure-Dart
|
|
extensions of existing seams). Combined with a new settings UI screen section and
|
|
a new Dart model, a single PR is very likely to exceed the 400-line budget and mixes
|
|
three independently reviewable/rollback-able concerns (pure-Dart tree logic,
|
|
native channel, phone UI).
|
|
|
|
**400-line budget risk: High.**
|
|
|
|
**Suggested slice boundaries** (if `delivery_strategy` calls for chaining):
|
|
1. Tasks 1-3 (models + `esArchivoAudio` + media-id scheme) — pure Dart, small,
|
|
independently mergeable, ~150-200 lines.
|
|
2. Tasks 4-7 (Dart orchestration: `FuenteMusicaLocalAuto` interface, tree
|
|
extension, `getChildren`/`playFromMediaId` wiring, cold-start safety, test
|
|
updates) — pure Dart, the bulk of the testable logic, ~350-450 lines. Depends
|
|
on slice 1.
|
|
3. Task 8 (native Kotlin channel extension) — static-review-only, isolated
|
|
rollback surface, ~120-170 lines. Can be built in parallel with slice 2 but
|
|
should be its OWN PR given the review-attention flag on
|
|
`startActivityForResult`/`onActivityResult`.
|
|
4. Task 9 (settings UI) — depends on slice 3's channel methods existing;
|
|
~150-210 lines including l10n.
|
|
|
|
**Decision needed before apply: Yes** — recommend `sdd-apply` be scoped to ONE
|
|
slice at a time per the orchestrator's Review Workload Guard, using the cached
|
|
`delivery_strategy`/`chain_strategy`, rather than attempting all of Phase 1 in a
|
|
single work session/PR.
|