feat(auto): browse and play local music folders in Android Auto [size:exception]

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.
This commit is contained in:
2026-07-19 20:30:50 +02:00
parent 99897ec848
commit 6ae7e378c4
28 changed files with 2425 additions and 40 deletions
+46
View File
@@ -0,0 +1,46 @@
/// Local file-system node returned by the native `listAudioChildren`
/// channel call (Design "Interfaces / Contracts"): either a subfolder or an
/// audio file, one SAF tree level deep. Pure DTO — no behavior, so no unit
/// tests are warranted for it on its own (exercised indirectly through its
/// consumers, e.g. `ConstructorArbolAuto.itemsLocales`).
class NodoLocal {
const NodoLocal({
required this.documentId,
required this.nombre,
required this.esDirectorio,
});
/// Opaque SAF document id, unique within the picked tree. May itself
/// contain `:`/`/` (Design "Prefix stripped by length"), so callers must
/// never split/parse it — only wrap it verbatim in a media id.
final String documentId;
/// Raw on-device filename (or folder name), NOT yet title-stripped.
final String nombre;
/// Whether this node is a browsable subfolder (`true`) or a playable
/// audio file (`false`).
final bool esDirectorio;
}
/// Minimal playable local track (Design "Phase 1 minimal shape" — no
/// artist/album/duration metadata per spec's "Not in this delta"). Pure
/// DTO — no behavior, so no unit tests are warranted for it on its own.
class PistaLocal {
const PistaLocal({
required this.documentId,
required this.titulo,
required this.contentUri,
});
/// Opaque SAF document id for this track.
final String documentId;
/// Display title, already computed by the caller (filename minus
/// extension, or a derived fallback — see `navegacion_auto.dart`).
final String titulo;
/// Playable `content://` URI resolved via
/// `FuenteMusicaLocalAuto.uriContenidoDePista`.
final String contentUri;
}