home: show only the car's status; help, preview, logs and the dev test message move to Settings

This commit is contained in:
Seb3thehacker committed 2026-10-13 15:38:40 -07:00
1 parent 7d9eb20ff0
commit 3869258e0e
5 files changed
+86 -125

No files matched your search

@@ -60,10 +60,13 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
var screen by rememberSaveable { mutableStateOf(if (Intro.due(context)) Screen.INTRO else afterIntro) }
// A replay from About goes back to Settings; the launch one carries on into setup.
var introReturn by rememberSaveable { mutableStateOf(afterIntro) }
// Home's usage notes row opens Settings at that section rather than at the top.
// Home's "it worked" card opens Settings at usage notes rather than at the top.
var settingsAtUsageNotes by rememberSaveable { mutableStateOf(false) }
BackHandler(enabled = screen != Screen.HOME && screen != Screen.INTRO) {
screen = if (screen == Screen.RECOMMENDED_APPS) Screen.SETTINGS else Screen.HOME
screen = when (screen) {
Screen.RECOMMENDED_APPS, Screen.LOGS, Screen.CAR_PREVIEW, Screen.HELP -> Screen.SETTINGS
else -> Screen.HOME
}
}
// Home stays upright; every other screen turns with the phone.
@@ -101,27 +104,27 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
when (screen) {
Screen.INTRO -> IntroScreen(onDone = { screen = introReturn })
Screen.HOME -> HomeScreen(
onOpenLogs = { screen = Screen.LOGS },
onOpenSettings = { settingsAtUsageNotes = false; screen = Screen.SETTINGS },
onOpenCarPreview = { screen = Screen.CAR_PREVIEW },
onOpenHelp = { screen = Screen.HELP },
onOpenUsageNotes = { settingsAtUsageNotes = true; screen = Screen.SETTINGS },
onDisconnect = onDisconnect,
)
Screen.WHATS_NEW -> WhatsNewScreen(onBack = { screen = Screen.SETTINGS })
Screen.LOGS -> LogsScreen(onBack = { screen = Screen.HOME })
Screen.LOGS -> LogsScreen(onBack = { screen = Screen.SETTINGS })
Screen.SETTINGS -> SettingsScreen(
onBack = { screen = Screen.HOME },
onOpenRecommendedApps = { settingsAtUsageNotes = false; screen = Screen.RECOMMENDED_APPS },
onOpenWhatsNew = { settingsAtUsageNotes = false; screen = Screen.WHATS_NEW },
onOpenHelp = { settingsAtUsageNotes = false; screen = Screen.HELP },
onOpenCarPreview = { settingsAtUsageNotes = false; screen = Screen.CAR_PREVIEW },
onOpenLogs = { settingsAtUsageNotes = false; screen = Screen.LOGS },
onReplayTutorial = { screen = Screen.SETUP_GUIDE },
onReplayIntro = { introReturn = Screen.SETTINGS; screen = Screen.INTRO },
atUsageNotes = settingsAtUsageNotes,
)
Screen.RECOMMENDED_APPS -> RecommendedAppsScreen(onBack = { screen = Screen.SETTINGS })
Screen.CAR_PREVIEW -> CarPreviewScreen(onBack = { screen = Screen.HOME })
Screen.CAR_PREVIEW -> CarPreviewScreen(onBack = { screen = Screen.SETTINGS })
Screen.HELP -> HelpScreen(
onBack = { screen = Screen.HOME },
onBack = { screen = Screen.SETTINGS },
onRequestCallScreening = onRequestCallScreening,
)
Screen.CERT_SETUP -> CertSetupScreen(
@@ -47,10 +47,7 @@ import app.seb3thehacker.gearslip.stats.WorkedPrompt
@Composable
fun HomeScreen(
onOpenLogs: () -> Unit,
onOpenSettings: () -> Unit,
onOpenCarPreview: () -> Unit,
onOpenHelp: () -> Unit,
onOpenUsageNotes: () -> Unit,
onDisconnect: () -> Unit,
) {
@@ -108,10 +105,6 @@ fun HomeScreen(
}
Spacer(Modifier.height(16.dp))
UpdateCard()
if (status.phase != SessionStatus.Phase.PROJECTING) {
Spacer(Modifier.height(16.dp))
WirelessCard()
}
}
// Sits at the bottom, where a thumb reaches it.
@@ -120,29 +113,8 @@ fun HomeScreen(
Button(onClick = onDisconnect, modifier = Modifier.fillMaxWidth().height(56.dp)) {
Text("Disconnect")
}
Spacer(Modifier.height(16.dp))
}
// Not while the card above is asking the same thing.
if (workedCar == null || live) {
UsageNotesHomeCard(onOpen = onOpenUsageNotes)
Spacer(Modifier.height(16.dp))
}
Surface(color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = MaterialTheme.shapes.extraLarge) {
Column {
if (!live) {
ActionRow(Icons.Filled.PlayArrow, "Car preview", onOpenCarPreview)
RowDivider()
}
ActionRow(Icons.Filled.List, "Live logs", onOpenLogs)
RowDivider()
ActionRow(Icons.Filled.Info, "Connection help", onOpenHelp)
if (BuildConfig.DEBUG) {
RowDivider()
ActionRow(Icons.Filled.Send, "Send a test message") { TestMessage.post(context) }
}
}
} else {
WirelessButton()
}
}
}
@@ -152,34 +124,6 @@ fun HomeScreen(
}
}
@Composable
private fun ActionRow(icon: ImageVector, label: String, onClick: () -> Unit) {
Row(
Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 20.dp, vertical = 18.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
Text(label, style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f))
Icon(
Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun RowDivider() {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 20.dp),
color = MaterialTheme.colorScheme.outlineVariant,
)
}
@Composable
private fun StatusCard(status: SessionStatus.Snapshot) {
val scheme = MaterialTheme.colorScheme
@@ -19,7 +19,11 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
@@ -60,6 +64,8 @@ import androidx.compose.ui.unit.dp
import app.seb3thehacker.gearslip.AppSettings
import app.seb3thehacker.gearslip.BuildConfig
import app.seb3thehacker.gearslip.CertProvider
import app.seb3thehacker.gearslip.SessionStatus
import app.seb3thehacker.gearslip.notify.TestMessage
import app.seb3thehacker.gearslip.car.CarSettings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -74,12 +80,15 @@ fun SettingsScreen(
onBack: () -> Unit,
onOpenRecommendedApps: () -> Unit,
onOpenWhatsNew: () -> Unit,
onOpenHelp: () -> Unit,
onOpenCarPreview: () -> Unit,
onOpenLogs: () -> Unit,
onReplayTutorial: () -> Unit,
onReplayIntro: () -> Unit,
atUsageNotes: Boolean = false,
) {
val scroll = rememberScrollState()
// Opened from Home's usage notes row: start at that section.
// Opened from Home's "it worked" card: start at usage notes.
var usageNotesY by remember { mutableIntStateOf(-1) }
if (atUsageNotes) {
LaunchedEffect(Unit) {
@@ -111,6 +120,7 @@ fun SettingsScreen(
) {
Spacer(Modifier.height(0.dp))
ConnectionSection()
HelpSection(onOpenHelp, onOpenCarPreview, onOpenLogs)
CarAudioSection()
VoiceModelSection()
if (SHOW_RECOMMENDED_APPS) AppsSection(onOpenRecommendedApps)
@@ -185,6 +195,51 @@ private fun ConnectionSection() {
}
}
/** Connection help, and the tools for checking what Gearslip does. */
@Composable
private fun HelpSection(onOpenHelp: () -> Unit, onOpenCarPreview: () -> Unit, onOpenLogs: () -> Unit) {
val context = LocalContext.current
val status by SessionStatus.state.collectAsState()
val live = status.phase == SessionStatus.Phase.CONNECTING || status.phase == SessionStatus.Phase.PROJECTING
SettingsSection("Help and tools") {
SettingsRow(
"Connection help",
Modifier.clickable(onClick = onOpenHelp),
subtitle = "Which cars work, and what to set up first",
icon = Icons.Filled.Info,
trailing = { Arrow() },
)
// The preview draws the car screen on the phone, so it waits until no car is using it.
if (!live) {
SettingsDivider()
SettingsRow(
"Car preview",
Modifier.clickable(onClick = onOpenCarPreview),
subtitle = "See the car screen on your phone",
icon = Icons.Filled.PlayArrow,
trailing = { Arrow() },
)
}
SettingsDivider()
SettingsRow(
"Live logs",
Modifier.clickable(onClick = onOpenLogs),
subtitle = "What Gearslip and the car are saying to each other",
icon = Icons.AutoMirrored.Filled.List,
trailing = { Arrow() },
)
if (BuildConfig.DEBUG) {
SettingsDivider()
SettingsRow(
"Send a test message",
Modifier.clickable { TestMessage.post(context) },
subtitle = "Dev builds only. Posts a message notification to read on the car",
icon = Icons.AutoMirrored.Filled.Send,
)
}
}
}
/** How music reaches the car: by Bluetooth as usual, or through Gearslip over USB. Same choice as setup. */
@Composable
private fun CarAudioSection() {
@@ -256,44 +256,3 @@ internal fun WorkedCard(car: String, onSeeWhatsSent: () -> Unit, modifier: Modif
}
}
}
/**
* Usage notes on Home, one row with one action either way. While they're off, the row is the
* switch, so opting in is a single tap. Once they're on, it shrinks to a quiet line that opens
* Settings at its usage notes section, so a driver already sharing isn't pitched again.
*/
@Composable
internal fun UsageNotesHomeCard(onOpen: () -> Unit, modifier: Modifier = Modifier) {
val context = LocalContext.current
var on by remember { mutableStateOf(UsageStats.enabled(context)) }
SettingsCard(modifier) {
if (on) {
SettingsRow(
"Sharing usage notes",
Modifier.clickable(onClick = onOpen),
subtitle = buildNote(),
icon = Icons.Filled.Check,
trailing = {
Icon(
Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
},
)
} else {
SettingsRow(
USAGE_STATS_TITLE,
Modifier.toggleable(false, role = Role.Switch) {
UsageStats.set(context, true, UsageStats.choices(context))
on = true
},
subtitle = buildNote() ?: ("A short note after each drive tells the next driver " +
"whether their car works. No name, account, or location"),
icon = Icons.Filled.Send,
trailing = { Switch(checked = false, onCheckedChange = null) },
)
}
}
}
@@ -8,15 +8,13 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -41,11 +39,11 @@ private val WIRELESS_PERMISSIONS = buildList {
}.toTypedArray()
/**
* Test entry point for wireless Android Auto: pick the paired car, and Gearslip connects to it
* over Bluetooth and Wi-Fi. Automatic start comes later.
* Test entry point for wireless Android Auto, at the bottom of Home: pick the paired car, and
* Gearslip connects to it over Bluetooth and Wi-Fi. Automatic start comes later.
*/
@Composable
internal fun WirelessCard() {
internal fun WirelessButton() {
val context = LocalContext.current
var devices by remember { mutableStateOf<List<BluetoothDevice>?>(null) }
var denied by remember { mutableStateOf(false) }
@@ -56,18 +54,21 @@ internal fun WirelessCard() {
if (granted()) devices = WirelessLink.pairedDevices(context) else denied = true
}
SettingsCard {
SettingsRow(
"Connect wirelessly (test)",
Modifier.clickable {
denied = false
if (granted()) devices = WirelessLink.pairedDevices(context) else ask.launch(WIRELESS_PERMISSIONS)
},
subtitle = if (denied) "Gearslip needs Bluetooth and nearby Wi-Fi access for this"
else "Pick your car from the phone's paired devices",
icon = Icons.Filled.Info,
if (denied) {
Text(
"Gearslip needs Bluetooth and nearby Wi-Fi access for this.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 8.dp),
)
}
FilledTonalButton(
onClick = {
denied = false
if (granted()) devices = WirelessLink.pairedDevices(context) else ask.launch(WIRELESS_PERMISSIONS)
},
modifier = Modifier.fillMaxWidth().height(56.dp),
) { Text("Connect wirelessly (test)") }
devices?.let { list ->
AlertDialog(
@@ -96,5 +97,4 @@ internal fun WirelessCard() {
confirmButton = { TextButton(onClick = { devices = null }) { Text("Cancel") } },
)
}
Spacer(Modifier.height(16.dp))
}