diff --git a/AGENTS.md b/AGENTS.md index 30383807..4959c389 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,34 @@ messages, UI strings, or script output. Plain text only. Also no em-dashes; writ plain human voice (commit subjects are the user-facing changelog). Use words like `PASS`/ `FAIL`, not pictographs. +## The verification bar: a UI change is "verified" ONLY when every box below is checked + +Phases passing + host gates green is NOT the bar - twice in one session (2026-07-23) a change +was declared verified with boxes missing, and the user caught both. Before writing "verified": + +1. **Coverage phases - ONLY the phases the change touches, never the full tour.** The full + multi-hour matrix for a scoped change is a waste of device time and delays the verdict; + `PHASES="..."` exists precisely so a settings change re-runs ~5 min/leg, not ~40. Same + scoping on the OTHER axes: both flavors only when the change can differ between them, the + `SOFTKEYS=off` touch leg only when the change touches a surface the soft-key declutter + diverges (search bar, FABs, place-sheet buttons, Options menus) - a spoke-only settings + change does not need it. Run the touched phases at all 4 geometries; skip axes the change + provably cannot vary across, and SAY which were skipped and why. +2. **Dynamic focus walk** of any changed/added settings spoke + (`AUDIT_SECTIONS= tests/dpad/audit_dynamic.sh`): every row takes focus in ORDER with + the orange ring on each stop. Reusing an enforced component does not waive this. +3. **Ring evidence** for new focusable controls - a walk frame or ring_walk assertion, not an + inference from the component used. +4. **Enumerate EVERY surface the change touches** and verify each one. A theme touches every + screen; a palette role touches every consumer; a shared component touches every caller. If + no phase reaches a touched surface, ADD ONE (same PR) - that is the fix-the-harness rule. +5. **Eyeball the frames yourself**, show them in chat, and put grids in the PR body. +6. **A/B against main** whenever a check fails - regression and pre-existing look identical + until you run the same check on main's build. + +State the boxes explicitly when declaring verification; if one is genuinely unmet, name it and +why instead of letting "verified" slide. + ## Visual verification (HARD RULE, NO EXCEPTIONS) **Anything a user can see MUST be verified VISUALLY, by looking at an actual on-device @@ -1052,7 +1080,7 @@ state - upstream's own 13ac02e8 already made the layers panel a VelaMenu): in-app theme with the composable **`isAppInDarkTheme()`** - never call `isSystemInDarkTheme()` directly in app UI (it ignores the user's Light/Dark/ System choice in Settings → Appearance). FOUR modes since 2026-07-23: SYSTEM / - LIGHT / DARK / **AMOLED** (the dark scheme on true-black surfaces, `AmoledColors` + LIGHT / DARK / **Black** (the dark scheme on true-black surfaces - the AMOLED mode, `AmoledColors` in `Theme.kt`; `isAppInDarkTheme()` is true for it, so the map and every dark branch follow automatically). Light mode uses soft teal-cast off-whites, never pure white (user feedback - harsh). `AppTheme.mode` is a process-wide diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6fefc65f..d152765d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -295,6 +295,12 @@ dependencies { // stays UI-agnostic. implementation(libs.maplibre.android) implementation(libs.androidx.car.app) // Android Auto (projection): templates + car surface + // The PROJECTED HOST CONNECTOR - without it Android Auto never lists the app: the host + // discovers a car app through the CarAppMetadataHolderService this artifact merges into the + // manifest, not through our own service declaration alone (official car-app docs: app-projected + // is the Android Auto artifact). The first-cut port shipped only the base :app template + // library, which is why Vela has never appeared in an AA launcher (device report, OnePlus 12). + implementation(libs.androidx.car.app.projected) debugImplementation(libs.androidx.compose.ui.tooling) } diff --git a/app/src/main/java/app/vela/ui/SheetPalette.kt b/app/src/main/java/app/vela/ui/SheetPalette.kt index a51d354c..d9d227ba 100644 --- a/app/src/main/java/app/vela/ui/SheetPalette.kt +++ b/app/src/main/java/app/vela/ui/SheetPalette.kt @@ -21,14 +21,24 @@ object SheetPalette { val DimLight = Color(0xFF5F6368) val RowDark = Color(0xFF202124) // inset row / chip background val RowLight = Color(0xFFF1F3F4) + // AMOLED: the sheets go TRUE BLACK (they are the app's largest chrome surfaces - a grey sheet + // on a black theme defeats the mode's whole point), with the inset rows stepped to a near-black + // so chips/rows still read as layers. Ink/Dim stay the dark values (contrast is higher on black). + val Amoled = Color(0xFF000000) + val RowAmoled = Color(0xFF000000) // rows too - black is black; hairlines separate + + /** True while the app-wide theme is the AMOLED mode - a state read, so composables that call + * the helpers below recompose when the mode flips (same mechanism as isAppInDarkTheme). */ + private val amoled: Boolean + get() = app.vela.ui.theme.AppTheme.mode.value == app.vela.ui.theme.ThemeMode.AMOLED // Shared traffic-coded colours (route ETAs, the route line, the steps header). val TrafficGreen = Color(0xFF1E8E3E) val TrafficAmber = Color(0xFFE8923D) val TrafficRed = Color(0xFFD93838) - fun bg(dark: Boolean) = if (dark) Dark else Light + fun bg(dark: Boolean) = if (dark) { if (amoled) Amoled else Dark } else Light fun ink(dark: Boolean) = if (dark) InkDark else InkLight fun dim(dark: Boolean) = if (dark) DimDark else DimLight - fun row(dark: Boolean) = if (dark) RowDark else RowLight + fun row(dark: Boolean) = if (dark) { if (amoled) RowAmoled else RowDark } else RowLight } diff --git a/app/src/main/java/app/vela/ui/place/PlaceSheet.kt b/app/src/main/java/app/vela/ui/place/PlaceSheet.kt index 56e41573..bdf2371e 100644 --- a/app/src/main/java/app/vela/ui/place/PlaceSheet.kt +++ b/app/src/main/java/app/vela/ui/place/PlaceSheet.kt @@ -200,8 +200,9 @@ import java.util.Locale // like Google Maps, instead of a washed-out dynamic tone. // The sheet palette is shared app-wide (see ui/SheetPalette) so the place sheet, // directions panel, route chooser and steps list all match. -private val SheetDark = SheetPalette.Dark -private val SheetLight = SheetPalette.Light +// Resolved through SheetPalette.bg() at every use site (NOT captured as constants): bg() is +// AMOLED-aware - a captured val froze the grey and the place sheet stayed #1F1F1F on the +// true-black theme while every other sheet went black (code-audit find, 2026-07-23). private val InkDark = SheetPalette.InkDark private val InkLight = SheetPalette.InkLight private val DimDark = SheetPalette.DimDark @@ -380,7 +381,7 @@ fun PlaceSheet( Card( modifier.fillMaxWidth().heightIn(max = maxSheetHeight), shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), - colors = CardDefaults.cardColors(containerColor = if (dark) SheetDark else SheetLight), + colors = CardDefaults.cardColors(containerColor = SheetPalette.bg(dark)), ) { // Card background fills to the screen bottom; pad the content up off the nav bar. Column(Modifier.navigationBarsPadding()) { @@ -1083,7 +1084,7 @@ fun DirectionsPanel( Card( modifier.fillMaxWidth(), shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), - colors = CardDefaults.cardColors(containerColor = if (dark) SheetDark else SheetLight), + colors = CardDefaults.cardColors(containerColor = SheetPalette.bg(dark)), ) { Column(Modifier.navigationBarsPadding().padding(start = 20.dp, end = 8.dp, top = 8.dp, bottom = 16.dp)) { // Drag handle - swipe down to minimise the chooser (peek the route on the @@ -1531,7 +1532,7 @@ fun TransitNavSheet( val dim = if (dark) DimDark else DimLight val itin = nav.itinerary val step = itin.steps.getOrNull(nav.stepIndex) - Surface(Modifier.fillMaxSize(), color = if (dark) SheetDark else SheetLight) { + Surface(Modifier.fillMaxSize(), color = SheetPalette.bg(dark)) { Column(Modifier.fillMaxSize().statusBarsPadding().navigationBarsPadding().padding(16.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { Text( @@ -2042,7 +2043,7 @@ fun RouteDetailSheet( step?.alightStop?.let { a -> if (mid.none { it.name == a.name } && step.boardStop?.name != a.name) add(a) } } } - Surface(Modifier.fillMaxSize(), color = if (dark) SheetDark else SheetLight) { + Surface(Modifier.fillMaxSize(), color = SheetPalette.bg(dark)) { Column(Modifier.fillMaxSize().statusBarsPadding().navigationBarsPadding()) { Row( Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp), @@ -2597,7 +2598,7 @@ private fun FullScreenReviews(featureId: String, place: Place, ink: Color, dim: // to the WebView cleanly once the page loads. No-op under touch. val reviewsBackFocus = rememberDpadAutoFocus() Dialog(onDismissRequest = onClose, properties = DialogProperties(usePlatformDefaultWidth = false)) { - Surface(Modifier.fillMaxSize(), color = if (dark) SheetDark else SheetLight, contentColor = ink) { + Surface(Modifier.fillMaxSize(), color = SheetPalette.bg(dark), contentColor = ink) { Column(Modifier.fillMaxSize().statusBarsPadding()) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/app/vela/ui/softkey/VelaSoftkeys.kt b/app/src/main/java/app/vela/ui/softkey/VelaSoftkeys.kt index 1fb1e5b2..71674229 100644 --- a/app/src/main/java/app/vela/ui/softkey/VelaSoftkeys.kt +++ b/app/src/main/java/app/vela/ui/softkey/VelaSoftkeys.kt @@ -116,12 +116,13 @@ object VelaSoftkeys { // visible flash on each screen change. So rebuild ONLY when the theme actually flipped; a plain // label change just re-set()s in place (no churn, no flash). val dark = app.vela.ui.theme.isAppInDarkTheme() + val themeMode = app.vela.ui.theme.AppTheme.mode.value val modal = modalDepth.intValue > 0 // a VelaDialog is up -> hide the bar (keys go to it) - val lastDark = remember { mutableStateOf(null) } - LaunchedEffect(left?.label, right?.label, dark, modal) { + val lastTheme = remember { mutableStateOf(null) } + LaunchedEffect(left?.label, right?.label, dark, themeMode, modal) { val ctl = Softkeys.of(activity) - val themeFlipped = lastDark.value?.let { it != dark } == true - lastDark.value = dark + val themeFlipped = lastTheme.value?.let { it != themeMode } == true + lastTheme.value = themeMode applyThemeColors(dark) if (modal || (leftNow == null && rightNow == null)) { ctl.clear() @@ -139,9 +140,12 @@ object VelaSoftkeys { /** Paint the bar for the in-app theme. A dark toolbar in dark, a light one in light - both * single-ink, matching Vela's map chrome. Applied before a rebuild (see [MapSoftkeys]). */ private fun applyThemeColors(dark: Boolean) { + // AMOLED: the bar goes true black with a slightly stronger divider so it still separates + // from a black map bezel; every other colour keeps the dark set (contrast only improves). + val amoled = app.vela.ui.theme.AppTheme.mode.value == app.vela.ui.theme.ThemeMode.AMOLED Yapchik.style.apply { if (dark) { - backgroundColor = 0xFF14343A.toInt() + backgroundColor = if (amoled) 0xFF000000.toInt() else 0xFF14343A.toInt() textColor = 0xFFECECEC.toInt() pressedTextColor = 0xFF4DD0C4.toInt() dividerColor = 0x33FFFFFF diff --git a/app/src/main/java/app/vela/ui/theme/Theme.kt b/app/src/main/java/app/vela/ui/theme/Theme.kt index a7d92eb7..e70a49a8 100644 --- a/app/src/main/java/app/vela/ui/theme/Theme.kt +++ b/app/src/main/java/app/vela/ui/theme/Theme.kt @@ -43,18 +43,19 @@ private val DarkColors = darkColorScheme( tertiary = VelaAmber, ) -// AMOLED: the dark scheme on TRUE BLACK surfaces (every lit pixel costs battery on OLED, and -// pure black is its own look). Container roles step up in near-blacks so cards and the title bar -// still read as layers; the thin borders on Settings cards carry the structure. +// Black: the dark scheme on TRUE BLACK - every surface AND container role is #000000 ("black +// should be black", user 2026-07-23; the earlier near-black stepping read as grey areas). The +// hairline outlines and the Settings cards' thin borders carry ALL of the structure; nothing +// separates by fill any more. private val AmoledColors = DarkColors.copy( background = androidx.compose.ui.graphics.Color(0xFF000000), surface = androidx.compose.ui.graphics.Color(0xFF000000), surfaceDim = androidx.compose.ui.graphics.Color(0xFF000000), surfaceContainerLowest = androidx.compose.ui.graphics.Color(0xFF000000), - surfaceContainerLow = androidx.compose.ui.graphics.Color(0xFF060809), - surfaceContainer = androidx.compose.ui.graphics.Color(0xFF0B0E0F), - surfaceContainerHigh = androidx.compose.ui.graphics.Color(0xFF121617), - surfaceContainerHighest = androidx.compose.ui.graphics.Color(0xFF191E1F), + surfaceContainerLow = androidx.compose.ui.graphics.Color(0xFF000000), + surfaceContainer = androidx.compose.ui.graphics.Color(0xFF000000), + surfaceContainerHigh = androidx.compose.ui.graphics.Color(0xFF000000), + surfaceContainerHighest = androidx.compose.ui.graphics.Color(0xFF000000), ) /** diff --git a/app/src/main/java/app/vela/web/ReviewsPanel.kt b/app/src/main/java/app/vela/web/ReviewsPanel.kt index e1e9b96f..4173970d 100644 --- a/app/src/main/java/app/vela/web/ReviewsPanel.kt +++ b/app/src/main/java/app/vela/web/ReviewsPanel.kt @@ -473,9 +473,11 @@ private fun buildPanelWebView( * Maintenance passes keep re-applying for a while (the SPA re-attaches chrome on interaction). */ private fun carveScript(dark: Boolean, fullScreen: Boolean): String { - // Vela's own sheet colour (SheetPalette Dark/Light) - the panel matches it EXACTLY so there's - // no seam with the surrounding place sheet. - val bg = if (dark) "#1f1f1f" else "#ffffff" + // Vela's own sheet colour - the panel matches SheetPalette.bg() EXACTLY so there's no seam + // with the surrounding place sheet (incl. the AMOLED true-black variant). + val bg = if (dark) { + if (app.vela.ui.theme.AppTheme.mode.value == app.vela.ui.theme.ThemeMode.AMOLED) "#000000" else "#1f1f1f" + } else "#ffffff" // Dark = a scoped invert on the PANEL CONTENT ONLY (main), NOT its background. The Vela colour // lives on (which the filter doesn't touch - it's on main), and main + every ancestor // are made transparent so that colour is the panel's backdrop; only Google's content inverts. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 459766b1..3f21e7e4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -16,7 +16,7 @@ System folgen Hell Dunkel - AMOLED-Schwarz + Schwarz Hell/Dunkel gilt nur für Vela, das System-Design Ihres Telefons wird nicht verändert. Kartenstil OpenFreeMap (Standard) ist schluessellos und detailliert. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 463e5290..f5de8fdd 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -16,7 +16,7 @@ Seguir el sistema Claro Oscuro - Negro AMOLED + Negro Claro/Oscuro se aplica solo a Vela; no cambia el tema del sistema de tu teléfono. Estilo del mapa OpenFreeMap (predeterminado) no necesita clave y es detallado. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a72bbc23..152ad4b3 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -16,7 +16,7 @@ Suivre le système Clair Sombre - Noir AMOLED + Noir Le mode clair/sombre s\'applique uniquement à Vela - il ne modifie pas le thème système de votre téléphone. Style de carte OpenFreeMap (par defaut) est sans cle et detaille. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 50f677b8..0dab89a6 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -16,7 +16,7 @@ Come il sistema Chiaro Scuro - Nero AMOLED + Nero Chiaro/Scuro si applica solo a Vela, non modifica il tema di sistema del telefono. Stile mappa OpenFreeMap (predefinito) e senza chiave e dettagliato. diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index e6931b66..9169ce5e 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -16,7 +16,7 @@ לפי הגדרת המערכת בהיר כהה - שחור AMOLED + שחור חל על Vela בלבד. סגנון מפה OpenFreeMap (ברירת המחדל) מפורט ואינו דורש מפתח. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index a279222d..78a08dbf 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -30,7 +30,7 @@ システムに合わせる ライト ダーク - AMOLEDブラック + ブラック Vela にのみ適用されます。 地図のスタイル OpenFreeMap(デフォルト)はキー不要で詳細です。 diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 230ffc69..e32a6e8c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -16,7 +16,7 @@ Systeem volgen Licht Donker - AMOLED-zwart + Zwart Licht/donker geldt alleen voor Vela - het thema van uw telefoon wordt niet gewijzigd. Kaartstijl OpenFreeMap (standaard) is sleutelloos en gedetailleerd. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 987e5f7f..38f6b230 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -16,7 +16,7 @@ Jak w systemie Jasny Ciemny - Czern AMOLED + Czarny Tryb jasny/ciemny dotyczy tylko Vela - nie zmienia motywu systemowego telefonu. Styl mapy OpenFreeMap (domyslny) jest szczegolowy i nie wymaga klucza. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 44a75ba9..3916c288 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -16,7 +16,7 @@ Seguir o sistema Claro Escuro - Preto AMOLED + Preto Claro/Escuro aplica-se apenas ao Vela - não altera o tema do sistema do seu telemóvel. Estilo do mapa OpenFreeMap (padrao) dispensa chave e e detalhado. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d6b53d4c..46dc17c8 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -16,7 +16,7 @@ Как в системе Светлая Тёмная - AMOLED-чёрный + Чёрный Светлая или тёмная тема применяется только к Vela - системная тема телефона не меняется. Стиль карты OpenFreeMap (по умолчанию) детальный и не требует ключа. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 8c4e3041..9046109f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -16,7 +16,7 @@ Följ systemet Ljust Mörkt - AMOLED-svart + Svart Ljust/Mörkt gäller endast Vela, det påverkar inte telefonens systemtema. Kartstil OpenFreeMap (standard) ar nyckellost och detaljerat. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index e7ddcd35..922e030c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -16,7 +16,7 @@ Як у системі Світла Темна - AMOLED-чорний + Чорний Світла/темна тема застосовується лише до Vela - системна тема телефона не змінюється. Стиль карти OpenFreeMap (типово) детальна і не потребує ключа. diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8ebe33a5..60c36038 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -30,7 +30,7 @@ 跟隨系統 淺色 深色 - AMOLED純黑 + 純黑 僅套用於 Vela。 地圖樣式 OpenFreeMap(預設)無需金鑰且細節豐富。 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index d6fc9b47..2e3ddba8 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -30,7 +30,7 @@ 跟随系统 浅色 深色 - AMOLED纯黑 + 纯黑 仅对 Vela 生效。 地图样式 OpenFreeMap(默认)无需密钥且细节丰富。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 29dfe223..37569db3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,7 +31,7 @@ Follow system Light Dark - AMOLED black + Black Light/Dark applies to Vela only. It won\'t touch your phone\'s system theme. Map style OpenFreeMap (the default) is keyless and detailed. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index ad0b5d7d..1b2d9cad 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -21,7 +21,7 @@ Status legend: [x] done · [~] partial / in progress · [ ] planned - [x] **Closing a far-away place keeps the camera there** - a programmatic jump >1 km from your fix drops free-drive follow like a manual pan, so closing the sheet no longer glides the map home; the locate button re-arms follow. - [x] **Compact nav banner on small screens** - under 500dp of screen height the maneuver banner shrinks (36dp glyph, smaller distance type, tighter padding) and every text line is capped, so a long arrive card can never bury the map (issue #41, F21 Pro). - [x] **Settings reorganized + nav UI refresh.** Sections in usage order (Appearance, Map, Place pages, Navigation, Voice, Offline, ...); vibrate-on-turns is one row of per-mode chips; the in-drive banner and bottom bar are soft-cornered floating cards. -- [x] **Settings redesigned hub-and-spoke (2026-07-22).** The single extremely long page became a short hub of category rows (icon + title + one-line subtitle), each opening its own small sub-screen - on a keypad phone any section is a few DOWN presses + OK away instead of ~70 presses down one scroll. Every page shares one `SettingsScaffold` (Back auto-focus, the two focus bridges, the horizontal swallow); BACK peels spoke -> hub -> map and the hub re-focuses the row you came back from; the restricted flavor hides the Place pages row; the onboarding offline prompt deep-links into the Offline spoke. A voice download in flight shows its progress on the hub's Voice row. The spokes use a standard flat settings layout: accent section heads, hairline separators, every toggle's hint as a short line inside its row (33 verbose hint strings were cut to one line across all 15 locales), compact 44dp interactive sizing, and single-line-safe button rows (a squeezed label wraps as a whole pill, never as two-line button text). The hub rows carry thin outline borders and the title bar separates via the neutral elevated surface role. New app-wide AMOLED black theme (Appearance -> AMOLED black): the dark scheme on true-black surfaces; light mode moved off pure white onto soft teal-cast off-whites. +- [x] **Settings redesigned hub-and-spoke (2026-07-22).** The single extremely long page became a short hub of category rows (icon + title + one-line subtitle), each opening its own small sub-screen - on a keypad phone any section is a few DOWN presses + OK away instead of ~70 presses down one scroll. Every page shares one `SettingsScaffold` (Back auto-focus, the two focus bridges, the horizontal swallow); BACK peels spoke -> hub -> map and the hub re-focuses the row you came back from; the restricted flavor hides the Place pages row; the onboarding offline prompt deep-links into the Offline spoke. A voice download in flight shows its progress on the hub's Voice row. The spokes use a standard flat settings layout: accent section heads, hairline separators, every toggle's hint as a short line inside its row (33 verbose hint strings were cut to one line across all 15 locales), compact 44dp interactive sizing, and single-line-safe button rows (a squeezed label wraps as a whole pill, never as two-line button text). The hub rows carry thin outline borders and the title bar separates via the neutral elevated surface role. New app-wide true-black theme (Appearance -> Black; the AMOLED mode): the dark scheme on true-black surfaces; light mode moved off pure white onto soft teal-cast off-whites. - [x] **Arrive step names the destination.** The banner, step list and arrival card show the business name and address, falling back to address then coordinates. - [x] **Perf audit batch 2: upload churn off the render thread (upstream 07d3f315, applicable slices).** applyData's tail is identity-gated like its head (alternates, location dot, preview pin, browse gradient, me-layer properties, ensure* style probes all skip when nothing changed; every gate resets on style reload); route-mode visibility flips run once per transition; ambient POI uploads defer while a camera flight is in the air and flush at landing; the in-nav route split re-uploads only a 3 km leading window per tick with the far tail on a twin layer (also sharpens the traffic gradient near the puck); the speed-limit poll skips during flights and pinches; and the browse follow's cold engage from a far-out camera is one owned flight instead of a teleport (f95f0b96's fix, applicable now that the fork has the browse ticker). Upstream's walk-dot regen and result-pin bitmap cache patch machinery the fork does not have (image-dash walking dots, no result-pin rasterizer). - [x] **Camera-seam hitch fixes (upstream f95f0b96, applicable slices).** Compass updates cap at ~5 pushes a second instead of up to 16 whole-screen recompositions during exactly the pan gestures that stutter; point map sources (pins, ambient POIs, traffic controls, transit stops, ALPR cameras, the location dot) cap their internal tile pyramid at z12, cutting re-tile work on every zoom change; the nav camera teardown is one-shot per real drive, so picking an alternate in the chooser no longer kills the route-fit flight; and a reroute while the nav camera is detached no longer falls through to the pre-nav route-fit and flies an uninvited whole-route fit mid-drive. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 410d228a..7804b7aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,6 +40,7 @@ detekt = "1.23.8" [libraries] androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "carApp" } +androidx-car-app-projected = { group = "androidx.car.app", name = "app-projected", version.ref = "carApp" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } diff --git a/tests/devices/full_coverage.sh b/tests/devices/full_coverage.sh index ae9d45da..bc956f62 100644 --- a/tests/devices/full_coverage.sh +++ b/tests/devices/full_coverage.sh @@ -18,7 +18,7 @@ # SOFTKEYS=off bash ... sonim-x320 # the TOUCH layout (search bar/gear/Park FAB on screen) -> full-touch/ # DENS=225 bash ... sonim-x320 # the reported 480x854 @ 225 dpi variant -> full-dens225/ # (Both write to their own dir so they never clobber the default soft-key/@320 committed goldens.) -# Phases: firstrun map search place directions settings voice parking. A partial run reports +# Phases: firstrun map search place directions settings voice parking blacktheme. A partial run reports # PARTIAL, not a verdict - only a full run (no PHASES) can call a device FULLY COVERED. # RULE: every NEW feature adds its surfaces as its OWN phase here (own numbered frames), so # verifying it on a device never needs the full tour. @@ -53,7 +53,7 @@ cover_one() { # redoing the whole ~13 min tour (e.g. PHASES=settings to iterate just the deep Settings sub-sections). # Each phase pins its own screenshot NUMBER base, so a subset writes the SAME 01..16 filenames a full # run would - a partial run overwrites only its slice and leaves the rest in place. Default = all. - local ALLPHASES="firstrun map search place streetview directions settings voice parking softkey" + local ALLPHASES="firstrun map search place streetview directions settings voice parking softkey blacktheme" local phases="${PHASES:-$ALLPHASES}" full=0; [ "$phases" = "$ALLPHASES" ] && full=1 phase() { case " $phases " in *" $1 "*) return 0;; *) return 1;; esac; } # Flavor-aware output: a run against the RESTRICTED flavor writes to its own directory so it can @@ -281,7 +281,7 @@ cover_one() { return 0 } if set_theme "Light"; then mark "settings-theme-light" 1; else mark "settings-theme-light" 0; fi - if set_theme "AMOLED black"; then mark "settings-theme-amoled" 1; else mark "settings-theme-amoled" 0; fi + if set_theme "Black"; then mark "settings-theme-amoled" 1; else mark "settings-theme-amoled" 0; fi set_theme "Follow system" || true key "$K_BACK" 1 else @@ -293,6 +293,56 @@ cover_one() { fi fi # phase settings + # --- Black theme, app-wide surfaces --------------------------------------------------------- + # "App-wide AMOLED black" is a claim about MORE than Settings: the place sheet's backgrounds + # (SheetPalette Amoled/RowAmoled), the reviews carve, and the soft-key bar all flip to true + # black. The settings phase only proves the theme PICKER; this phase flips Black on, walks + # those surfaces, records them, and flips back. Frames 57+ (after the settings phase's 41-55). + if phase blacktheme; then i=56 + pick_theme() { #