diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt index f3dbd09..3865c30 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt @@ -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( diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt index cc58673..b02dfa6 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt @@ -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 diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt index 6405eba..202e323 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt @@ -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() { diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/UsageStatsChoices.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/UsageStatsChoices.kt index 66e6180..7818aee 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/UsageStatsChoices.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/UsageStatsChoices.kt @@ -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) }, - ) - } - } -} diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/WirelessCard.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/WirelessButton.kt similarity index 80% rename from gearslip/src/main/java/app/seb3thehacker/gearslip/ui/WirelessCard.kt rename to gearslip/src/main/java/app/seb3thehacker/gearslip/ui/WirelessButton.kt index cd84bdf..a39daa0 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/WirelessCard.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/WirelessButton.kt @@ -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?>(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)) }