Android Auto no longer copies the launcher icon as placeholder art; it rotates through the same 4 on-brand station_art assets the phone UI already uses, keyed by the same per-station hash for visual parity. Malformed or unusable favicon URLs (including a Dart Uri quirk where 'http://' reports hasAuthority=true with an empty host) now fail the validity gate instead of being handed to the OS media browser as-is. Browsable items also show codec/bitrate as a subtitle when known.
202 lines
22 KiB
Markdown
202 lines
22 KiB
Markdown
# Tasks: auto-media-art-quality
|
|
|
|
Strict TDD active for Dart layers. Behavioral task = RED (failing test) -> GREEN (minimal impl) -> REFACTOR (cleanup, still green). Native/XML/PNG/build.gradle tasks have no `flutter test` coverage (no Android build env) — marked **[static-review-only]**; `flutter build`/`flutter run`/`flutter analyze` MUST NOT be executed in this environment (they hang). "Parallel" tasks have no file overlap with concurrently-listed siblings; "Sequential" tasks depend on a prior task's output.
|
|
|
|
Affected files: `lib/servicios/navegacion_auto.dart`, `android/app/src/main/res/drawable/station_art_aurora.png`, `android/app/src/main/res/drawable/station_art_cosmic.png`, `android/app/src/main/res/drawable/station_art_pulse.png`, `android/app/src/main/res/drawable/station_art_nova.png`, `android/app/src/main/res/drawable/default_station_art.png` (deleted), `test/servicios/navegacion_auto_test.dart`.
|
|
|
|
Baseline (verified against live code, not trusted from spec/design prose):
|
|
- `lib/servicios/navegacion_auto.dart:12-17` — `_defaultArtUri` const, to be dropped.
|
|
- `lib/servicios/navegacion_auto.dart:102-108` — `itemEmisora()`, sets `artUri` only, no `displaySubtitle`.
|
|
- `lib/servicios/navegacion_auto.dart:110-113` — `_artUriPara(e)`: null/empty favicon check only, no URL-shape validation.
|
|
- `lib/widgets/tarjeta_emisora.dart:361-370` — `_fallbackArtFor(seed)`: `[aurora, cosmic, pulse, nova]` order, `seed.codeUnits.fold<int>(0,(a,b)=>a+b) % arts.length`, assets at `assets/images/station_art_<name>.png`.
|
|
- `lib/modelos/emisora.dart:60-78` — `Emisora.fromMap` (SQLite favorites row) assigns `favicon: map['favicon'] as String?` with **no sanitization** (unlike `Emisora.fromApi:47`, which runs `_nonEmpty`). Confirms design.md's claim: malformed/whitespace favicon values from favorites rows reach the tree unfiltered today.
|
|
- `android/app/src/main/res/drawable/default_station_art.png` exists (only PNG currently in that directory) — to be deleted.
|
|
- `assets/images/station_art_{aurora,cosmic,pulse,nova}.png` exist — source images to copy into `android/app/src/main/res/drawable/`.
|
|
- `android/app/build.gradle.kts:34` — `applicationId = "es.freetimelab.pluriwave"`, matches the existing `android.resource://es.freetimelab.pluriwave/drawable/...` authority used by `_defaultArtUri` and the design's new `station_art_<name>` URIs. No manifest/build.gradle change is needed — adding `res/drawable` PNGs requires no manifest declaration (unlike the parent change's `automotive_app_desc.xml` meta-data, which pointed at an XML resource).
|
|
- `test/servicios/navegacion_auto_test.dart:45-63` — existing test asserts fallback to `_defaultArtUri` on null/empty favicon; must be replaced (not just extended) since `_defaultArtUri` is retired.
|
|
- `audio_service-0.18.18` (`lib/audio_service.dart:622`): `MediaItem.displaySubtitle` is `String?` — nullable, safe to pass a nullable `subtituloCalidad(e)` result directly.
|
|
|
|
---
|
|
|
|
## Phase 1 — Favicon validity gate (`faviconUsable`)
|
|
|
|
### 1.1 [x] [RED] `faviconUsable` rejects malformed/non-http(s) favicons, accepts valid http(s) (Sequential — first task, defines the seam)
|
|
- Satisfies: Design Decision "Case B detection" (static validity gate); Spec "Playable Item Metadata" / Scenario "Station's logo URL is present but unreachable (Case B)" (malformed subset).
|
|
- File: `test/servicios/navegacion_auto_test.dart`.
|
|
- New `group('faviconUsable', ...)`. Table-driven cases, each asserting `faviconUsable(input) == expected`:
|
|
- `null` → `false`
|
|
- `''` → `false`
|
|
- `' '` (whitespace only) → `false`
|
|
- `'ftp://cdn.example.com/logo.png'` (non-http(s) scheme) → `false`
|
|
- `'cdn.example.com/logo.png'` (no scheme, bare host) → `false`
|
|
- `'http://'` (scheme present, no authority) → `false`
|
|
- `'/relative/path/logo.png'` (relative, no scheme) → `false`
|
|
- `'not a url at all $$$ ///'` (unparseable/garbage) → `false`
|
|
- `'http://cdn.example.com/logo.png'` → `true`
|
|
- `'https://cdn.example.com/logo.png'` → `true`
|
|
- Run: fails (`faviconUsable` not implemented / not exported).
|
|
|
|
### 1.2 [x] [GREEN] Implement `faviconUsable` (Sequential — depends on 1.1)
|
|
- File: `lib/servicios/navegacion_auto.dart`.
|
|
- Top-level (or static) `bool faviconUsable(String? favicon)`: trim; if null/empty after trim → `false`. `Uri.tryParse(trimmed)`; if parse fails → `false`. Require `scheme == 'http' || scheme == 'https'` AND `uri.hasAuthority` (non-empty host) → `true`, else `false`.
|
|
- Run: 1.1 passes.
|
|
|
|
---
|
|
|
|
## Phase 2 — Rotation parity (`indiceArtePara`)
|
|
|
|
### 2.1 [x] [RED] `indiceArtePara` matches `tarjeta_emisora.dart`'s `_fallbackArtFor` formula and order (Sequential — depends on Phase 1 file existing, independent logic)
|
|
- Satisfies: Design Decision "Fallback-art selection — port `_fallbackArtFor` verbatim"; Spec "Fallback art matches phone-UI per-station selection".
|
|
- File: `test/servicios/navegacion_auto_test.dart`.
|
|
- New `group('indiceArtePara', ...)`. Assert `indiceArtePara(seed)` for a handful of sample uuids reproduces `seed.codeUnits.fold<int>(0,(a,b)=>a+b) % 4` computed inline in the test (do not hardcode indices without showing the formula), covering at least: an empty string (`''` → index 0, since `fold` over no elements is 0), a short seed, and a real-looking uuid.
|
|
- Assert the canonical order constant (or equivalent) used for mapping index→name is exactly `['aurora', 'cosmic', 'pulse', 'nova']`, matching `tarjeta_emisora.dart:362-367`.
|
|
- Run: fails.
|
|
|
|
### 2.2 [x] [GREEN] Implement `indiceArtePara` + canonical art-name list (Sequential — depends on 2.1)
|
|
- File: `lib/servicios/navegacion_auto.dart`.
|
|
- `int indiceArtePara(String seed) => seed.codeUnits.fold<int>(0, (a, b) => a + b) % 4;` — verbatim port, same formula as `tarjeta_emisora.dart:368`.
|
|
- Private `const _nombresArte = ['aurora', 'cosmic', 'pulse', 'nova']` — same order as `tarjeta_emisora.dart:363-366`. Add a code comment cross-linking to `tarjeta_emisora.dart`'s `_fallbackArtFor` to mitigate reorder drift (per design.md's stated mitigation, since there is no structural enforcement of order — see Phase 6 for the added test-level guard).
|
|
- Run: 2.1 passes.
|
|
|
|
---
|
|
|
|
## Phase 3 — `artUriPara` integration (favicon gate + rotating drawable, retires `_defaultArtUri`)
|
|
|
|
### 3.1 [x] [RED] `artUriPara` returns the favicon when usable, a rotating `station_art_<name>` drawable URI otherwise (Sequential — depends on 1.2, 2.2)
|
|
- Satisfies: Design Decision 1 (validity gate) + Decision 2 (rotation) combined at the `artUriPara` seam; Spec "Playable Item Metadata" (all 5 scenarios).
|
|
- File: `test/servicios/navegacion_auto_test.dart`.
|
|
- New `group('artUriPara', ...)`:
|
|
- Valid `https://` favicon → `artUriPara(e) == e.favicon`.
|
|
- `null` favicon → `artUriPara(e) == 'android.resource://es.freetimelab.pluriwave/drawable/station_art_<expected-name-for-uuid>'` (compute expected name via `indiceArtePara(e.uuid)` + canonical order in the test, not a hardcoded guess).
|
|
- `''` favicon → same rotating-drawable behavior as null.
|
|
- Malformed favicon (e.g. `'ftp://x/y.png'`, `'not a url'`) → same rotating-drawable behavior (Case B malformed subset).
|
|
- Two different uuids that hash to different indices → assert their resolved `artUriPara` values differ (parity smoke check, not full enumeration — that's Phase 6).
|
|
- Same uuid called twice → identical result (determinism).
|
|
- Run: fails.
|
|
|
|
### 3.2 [x] [GREEN] Implement `artUriPara`; drop `_defaultArtUri` and the old `_artUriPara` (Sequential — depends on 3.1)
|
|
- File: `lib/servicios/navegacion_auto.dart`.
|
|
- Replace `_artUriPara` (lines 110-113) with `String artUriPara(Emisora e) => faviconUsable(e.favicon) ? e.favicon! : 'android.resource://es.freetimelab.pluriwave/drawable/station_art_${_nombresArte[indiceArtePara(e.uuid)]}';`.
|
|
- Delete `_defaultArtUri` const (lines 12-17) — dead now that rotation is total (design.md Decision 3: uuid always present, empty-uuid → index 0 → aurora, no 5th fallback reachable).
|
|
- Update `itemEmisora()` (line 106) call site from `_artUriPara(e)` to `artUriPara(e)`.
|
|
- Run: 3.1 passes.
|
|
|
|
### 3.3 [x] [GREEN] Replace the obsolete default-art test in `itemEmisora` group (Sequential — depends on 3.2)
|
|
- File: `test/servicios/navegacion_auto_test.dart`.
|
|
- Replace the test at lines 45-63 (`'cae al arte por defecto cuando el favicon es null o vacío'`, which asserts the now-deleted `_defaultArtUri` literal) with an assertion that both null- and empty-favicon cases resolve to the correct rotating `station_art_<name>` URI per `indiceArtePara`, consistent with the Phase 3.1 cases.
|
|
- Run: full `navegacion_auto_test.dart` green.
|
|
|
|
---
|
|
|
|
## Phase 4 — Quality subtitle (`subtituloCalidad`)
|
|
|
|
### 4.1 [x] [RED] `subtituloCalidad` matrix: both known, codec-only, bitrate-only, both unknown, bitrate<=0 (Sequential — independent of Phase 1-3, same file)
|
|
- Satisfies: Design Decision "`displaySubtitle` quality format"; Spec "Station has known codec and bitrate" / "Station has unknown codec or bitrate".
|
|
- File: `test/servicios/navegacion_auto_test.dart`.
|
|
- New `group('subtituloCalidad', ...)`, one case per design.md's table plus the `bitrate <= 0` edge (Radio Browser stores 0 for unknown):
|
|
- `codec: 'mp3', bitrate: 128` → `'128 kbps · MP3'` (codec upper-cased; `·` is U+00B7).
|
|
- `codec: null, bitrate: 128` → `'128 kbps'`.
|
|
- `codec: 'mp3', bitrate: null` → `'MP3'`.
|
|
- `codec: 'mp3', bitrate: 0` → `'MP3'` (bitrate ≤ 0 treated as unknown).
|
|
- `codec: null, bitrate: null` → `null` (not `''`, not the literal string `'null'`).
|
|
- `codec: null, bitrate: 0` → `null`.
|
|
- `codec: ' '` (whitespace-only codec), `bitrate: null` → `null` (whitespace codec counts as unknown per design.md's "empty/whitespace codec count as unknown").
|
|
- `codec: ' mp3 '` (whitespace-padded codec), `bitrate: 128` → `'128 kbps · MP3'` (trimmed).
|
|
- Explicitly assert none of the outputs contain the substring `'null'` (defends the "never render literal null" requirement directly, not just via exact-match).
|
|
- Run: fails.
|
|
|
|
### 4.2 [x] [GREEN] Implement `subtituloCalidad` (Sequential — depends on 4.1)
|
|
- File: `lib/servicios/navegacion_auto.dart`.
|
|
- `String? subtituloCalidad(Emisora e)`: normalize codec via `trim().toUpperCase()`, treat empty-after-trim as unknown; treat `bitrate == null || bitrate! <= 0` as unknown.
|
|
- Both known → `'$bitrate kbps · $codecUpper'`. Bitrate only → `'$bitrate kbps'`. Codec only → `codecUpper`. Both unknown → `null` (never `''`, never a string containing `'null'`).
|
|
- Run: 4.1 passes.
|
|
|
|
---
|
|
|
|
## Phase 5 — `itemEmisora` wiring (`displaySubtitle`)
|
|
|
|
### 5.1 [x] [RED] `itemEmisora` sets `displaySubtitle` from `subtituloCalidad`; omits it (leaves `null`) when both unknown (Sequential — depends on 3.2, 4.2)
|
|
- Satisfies: Spec "Browsable Media Tree" / Scenario "Station has known codec and bitrate" + "Station has unknown codec or bitrate", applied at the leaf-building integration point.
|
|
- File: `test/servicios/navegacion_auto_test.dart`.
|
|
- Extend the existing `group('ConstructorArbolAuto.itemEmisora', ...)`:
|
|
- `Emisora` with `codec: 'mp3', bitrate: 128` → `item.displaySubtitle == '128 kbps · MP3'`.
|
|
- `Emisora` with `codec: null, bitrate: null` → `item.displaySubtitle == null`.
|
|
- Run: fails (current `itemEmisora` at lines 102-108 never sets `displaySubtitle`).
|
|
|
|
### 5.2 [x] [GREEN] Wire `displaySubtitle` into `itemEmisora` (Sequential — depends on 5.1)
|
|
- File: `lib/servicios/navegacion_auto.dart`.
|
|
- Add `displaySubtitle: subtituloCalidad(e)` to the `MediaItem(...)` construction in `itemEmisora` (lines 102-108), alongside the existing `artUri: Uri.parse(artUriPara(e))`.
|
|
- Run: 5.1 passes; full `itemEmisora` group green.
|
|
|
|
### 5.3 [x] [REFACTOR] Cleanup pass on `navegacion_auto.dart` (Sequential — depends on 3.2, 4.2, 5.2 all green)
|
|
- Re-read top to bottom: doc-comment `faviconUsable`/`artUriPara`/`indiceArtePara`/`subtituloCalidad` per the Design "Interfaces / Contracts" signatures; confirm no leftover reference to `_defaultArtUri` or the old `_artUriPara` anywhere in the file (grep the file for both identifiers — should return zero matches).
|
|
- Full `navegacion_auto_test.dart` suite stays green.
|
|
|
|
---
|
|
|
|
## Phase 6 — Downstream coverage & drift guards (favorites sanitization gap, phone/car order parity)
|
|
|
|
### 6.1 [x] [RED→GREEN, coverage-only] Test that `faviconUsable`/`artUriPara` catches the unsanitized `Emisora.fromMap` favicon gap (Sequential — depends on 3.2)
|
|
- Satisfies: Design Decision 1's explicit callout that `Emisora.fromMap` (SQLite favorites, `lib/modelos/emisora.dart:60-78`) does not sanitize `favicon` (no `_nonEmpty`, unlike `Emisora.fromApi:47`) — confirms the validity gate is the correct place to catch this, rather than silently assuming it.
|
|
- File: `test/servicios/navegacion_auto_test.dart`.
|
|
- Build an `Emisora` via `Emisora.fromMap({...})` (not the `_emisora()` test helper) with a malformed/whitespace favicon value in the map (e.g. `'favicon': ' '`, or `'favicon': 'not-a-url'`) that `fromMap` passes through unsanitized. Assert `artUriPara(builtEmisora)` resolves to a rotating `station_art_<name>` URI, not the malformed string.
|
|
- This is a regression-style test, not a design-doc gap-fix: it documents the current architecture's division of responsibility (sanitize-at-read-boundary vs sanitize-at-render-boundary) and pins it in place. If it fails, `fromMap` started sanitizing (fine) or the gate regressed (not fine) — either way the test makes the boundary explicit instead of assumed.
|
|
- Run: passes on first run once Phase 3 lands (this is a characterization test of already-implemented behavior, so RED/GREEN collapses to a single assertion pass — still write it before considering Phase 3 "done" in spirit, but there is no separate implementation step).
|
|
|
|
### 6.2 [x] [RED→GREEN, coverage-only] Parity guard: phone-UI asset order and Android-Auto drawable order stay identical (Sequential — depends on 2.2)
|
|
- Satisfies: Design.md's explicitly flagged risk — "reorder drift" between `tarjeta_emisora.dart`'s `_fallbackArtFor` asset list and `navegacion_auto.dart`'s `_nombresArte` list, called out as having "no structural enforcement," mitigated only by a code comment + this test.
|
|
- File: `test/servicios/navegacion_auto_test.dart`.
|
|
- New `group('parity: phone/auto art order', ...)`. Since `_fallbackArtFor`'s asset list is private to `tarjeta_emisora.dart` and returns `assets/...` paths (not directly importable/comparable in form), this test hardcodes the canonical order `['aurora', 'cosmic', 'pulse', 'nova']` **once**, with a comment explaining it must be kept in sync with `tarjeta_emisora.dart:363-367`'s literal list, and asserts `navegacion_auto.dart`'s exposed art-name list/mapping matches it element-for-element, in order.
|
|
- This does not eliminate drift risk (design.md accepts that — no structural enforcement chosen) but ensures any future reorder in `navegacion_auto.dart` alone breaks a test immediately; a reorder in `tarjeta_emisora.dart` alone still requires a human to update this test's hardcoded list (documented limitation, same as design.md's own acknowledgment).
|
|
- Run: passes once Phase 2 lands.
|
|
|
|
---
|
|
|
|
## Phase 7 — Android native drawables (retiring `default_station_art.png`) [static-review-only — no Android build env, `flutter build`/`flutter run`/`flutter analyze` MUST NOT be executed]
|
|
|
|
### 7.1 [x] Add 4 native drawable copies (Parallel with 7.2 — different files within the same directory, no overlapping content)
|
|
- Satisfies: Spec "Fallback art matches phone-UI per-station selection" + "Fallback art is on-brand, not the launcher icon"; Design "Native drawables + retire `default_station_art.png`".
|
|
- Files: `android/app/src/main/res/drawable/station_art_aurora.png`, `station_art_cosmic.png`, `station_art_pulse.png`, `station_art_nova.png` (new binary assets).
|
|
- Copy verbatim from `assets/images/station_art_{aurora,cosmic,pulse,nova}.png` — same technique the parent change used for `default_station_art.png` (Phase 6.3 there).
|
|
- Static review: filenames match exactly (case-sensitive) the names produced by `_nombresArte`/`indiceArtePara` in `navegacion_auto.dart` (`station_art_<name>`, no extension in the `android.resource://` URI), matching the pattern already verified working for `default_station_art`.
|
|
|
|
### 7.2 [x] Delete `default_station_art.png` (Parallel with 7.1 — different file)
|
|
- Satisfies: Design Decision "Native drawables + retire `default_station_art.png`" — rotation is total (uuid always present, empty-uuid → index 0 → aurora), so this drawable is unreachable dead weight and a byte-for-byte launcher-icon lookalike (parent change's WARNING #2).
|
|
- File: `android/app/src/main/res/drawable/default_station_art.png` (delete).
|
|
- Static review: confirm no remaining reference to `default_station_art` anywhere in the codebase after Phase 3.2 removes the Dart-side const (grep `default_station_art` across `lib/` and `android/` — should return zero matches after this task, other than this tasks.md/spec/design history).
|
|
|
|
### 7.3 [x] Verify `applicationId`/authority match for the new drawable URIs (Parallel with 7.1/7.2 — read-only verification, no file changes expected)
|
|
- Satisfies: precedent from parent change's WARNING notes (authority/applicationId must match for `android.resource://` URIs to resolve).
|
|
- Already verified during task planning: `android/app/build.gradle.kts:34` sets `applicationId = "es.freetimelab.pluriwave"`, matching the authority segment (`es.freetimelab.pluriwave`) used in both the retired `_defaultArtUri` and the new `station_art_<name>` URIs built in `artUriPara` (Phase 3.2). **No manifest or build.gradle change is required** — adding PNGs to `res/drawable` needs no manifest declaration, unlike the parent change's `automotive_app_desc.xml` meta-data entry (a `res/xml` resource that did require a `<meta-data>` reference).
|
|
- Static review only: re-confirm this holds at apply time (`applicationId` unchanged) before closing Phase 7.
|
|
|
|
---
|
|
|
|
## Phase 8 — Full regression sweep (Sequential — depends on all prior phases green)
|
|
|
|
### 8.1 [x] Run full `flutter test` suite (or the closest safe equivalent in this environment)
|
|
- Satisfies: Design "Testing Strategy" unit rows; Spec-level success criteria across all modified scenarios.
|
|
- Run the entire suite to catch cross-file regressions, not just `navegacion_auto_test.dart`.
|
|
- **Expected deviation (execution constraint, same as parent change)**: if a full-suite run is not safely executable in this environment, run targeted files instead — at minimum `navegacion_auto_test.dart` in full — and record which files were actually exercised. A genuine full-suite run is recommended before merge/CI, same caveat the parent change recorded.
|
|
|
|
### 8.2 [x] `flutter analyze` — DO NOT RUN in this environment
|
|
- **DEVIATION (execution constraint, same as parent change)**: `flutter analyze` hangs in this environment and MUST NOT be executed. Perform manual static review instead: confirm `artUriPara`, `faviconUsable`, `indiceArtePara`, `subtituloCalidad` are used (no unused-symbol risk), confirm no leftover references to `_defaultArtUri`/old `_artUriPara`, confirm all new code compiles implicitly by virtue of the modified file's tests passing in 8.1. A real `flutter analyze` pass is still recommended before merge/CI.
|
|
|
|
### 8.3 [x] `flutter build`/`flutter run` — DO NOT RUN in this environment
|
|
- **DEVIATION (execution constraint, same as parent change)**: these commands hang in this environment and MUST NOT be executed. Phase 7's native drawable work is static-review-only for this reason; a real Android build (or DHU session) is required before shipping to confirm the 4 new `station_art_*` drawables resolve correctly via `android.resource://` (Design's remaining Open Question).
|
|
|
|
### 8.4 [x] Manual DHU verification note (optional, non-blocking, not coded)
|
|
- Satisfies: Design "Testing Strategy" manual row; Design "Open Questions" (confirm OS art loader accepts `android.resource://` for the 4 new drawables).
|
|
- Record in the commit/PR description that a Desktop Head Unit (or real car) session is recommended before shipping to visually confirm: (a) all 4 rotating arts render correctly per station, (b) `displaySubtitle` quality text renders as expected on the browse row, (c) no stale `default_station_art` reference lingers anywhere in the built APK's resources.
|
|
|
|
---
|
|
|
|
## Review Workload Forecast
|
|
|
|
- Files touched: `lib/servicios/navegacion_auto.dart` (~40-60 changed/added lines: drop `_defaultArtUri` (~6 lines removed), add `faviconUsable`, `indiceArtePara`, `_nombresArte`, `artUriPara`, `subtituloCalidad` (~35-45 lines added), one-line `displaySubtitle` wiring in `itemEmisora`), `test/servicios/navegacion_auto_test.dart` (~130-170 added/changed lines: 6 new test groups — `faviconUsable`, `indiceArtePara`, `artUriPara`, `subtituloCalidad`, `itemEmisora` extension, parity guard — plus replacing the obsolete default-art test), 4 new binary PNGs under `android/app/src/main/res/drawable/` (`station_art_aurora/cosmic/pulse/nova.png`, copied from existing `assets/images/`, zero text-line cost), 1 binary deletion (`default_station_art.png`).
|
|
- Estimated total changed/added lines (text only, excluding binary PNG bytes): roughly 170-230 lines.
|
|
- Chained PRs recommended: No — this is well inside the 400-line single-PR budget even with generous test-heavy estimates. No manifest/build.gradle changes are needed (verified: `applicationId` already matches, `res/drawable` PNGs need no manifest declaration), which removes the one file class that made the parent change's Phase 6 riskier.
|
|
- 400-line budget risk: Low. Total estimate (~170-230 text lines + 4 binary adds + 1 binary delete) sits comfortably under budget as a single commit/PR.
|
|
- Decision needed before apply: No — proceed as a single delivery. The only structural caveat carried over from the parent change is that Phase 7 (native drawables) remains static-review-only (no Android build env here), same constraint class as the parent change's Phase 6, but it does not change file-count or line-count risk enough to warrant chaining.
|
|
- Ownership/dependency note: Phase 1 (validity gate) and Phase 2 (rotation) are independent of each other and can proceed in any order — both are prerequisites for Phase 3 (`artUriPara` integration), which retires `_defaultArtUri`. Phase 4 (subtitle) is fully independent of Phases 1-3 (different concern, same file) and could be done in parallel by a second contributor without conflict risk beyond normal same-file merge care. Phase 5 depends on both Phase 3 and Phase 4 landing. Phase 6 is coverage-only and depends on Phase 3 (6.1) and Phase 2 (6.2) respectively but not on each other. Phase 7 has zero Dart dependency and may be done anytime in parallel with all Dart phases (mirrors the parent change's Phase 6 independence). Phase 8 is a hard sequential gate depending on everything above.
|