From 7cc6b64ce22a957c42b76dc36b0e53feaf7614df Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Mon, 5 Oct 2026 01:39:41 -0700 Subject: [PATCH] ask existing users with the setup guide's usage page instead of a dialog --- .../seb3thehacker/gearslip/ui/GearslipApp.kt | 12 +++- .../gearslip/ui/SetupGuideScreen.kt | 61 +++++++++++-------- .../gearslip/ui/UsageStatsChoices.kt | 40 ------------ 3 files changed, 43 insertions(+), 70 deletions(-) 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 87f7026..1a23888 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt @@ -37,8 +37,9 @@ 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.stats.UsageStats -private enum class Screen { HOME, LOGS, SETTINGS, CAR_PREVIEW, CERT_SETUP, SETUP_GUIDE, HELP, WHATS_NEW, RECOMMENDED_APPS } +private enum class Screen { HOME, LOGS, SETTINGS, CAR_PREVIEW, CERT_SETUP, SETUP_GUIDE, USAGE_NOTES, HELP, WHATS_NEW, RECOMMENDED_APPS } /** Three screens and a back stack of depth one: no navigation library needed. */ @Composable @@ -53,6 +54,8 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) { when { needsCertSetup -> Screen.CERT_SETUP needsSetupGuide -> Screen.SETUP_GUIDE + // Set up before usage notes existed: ask once, on the setup guide's own page. + !UsageStats.asked(context) -> Screen.USAGE_NOTES else -> Screen.HOME }, ) @@ -120,6 +123,11 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) { onRequestCallScreening = onRequestCallScreening, onDone = { screen = Screen.HOME }, ) + Screen.USAGE_NOTES -> SetupGuideScreen( + onRequestCallScreening = onRequestCallScreening, + onDone = { screen = Screen.HOME }, + onlyUsageNotes = true, + ) } } // A car keyboard is open: offer the phone's own, for a passenger. @@ -127,8 +135,6 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) { } CompatWarning() - // Anyone who set up before usage notes existed gets asked once, on Home. - if (screen == Screen.HOME) UsageStatsPrompt() } /** diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt index dfaa092..08bd3ce 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt @@ -100,6 +100,21 @@ private fun hasNotificationAccess(context: Context): Boolean { return runCatching { manager.isNotificationListenerAccessGranted(component) }.getOrDefault(false) } +/** + * The usage notes question: the setup guide's last page, and on its own the one screen people + * who set up before the notes existed see once. Leaving it without turning them on is a no. + */ +private fun usageNotesPage(context: Context) = GuidePage( + title = USAGE_STATS_TITLE, + explanation = USAGE_STATS_INTRO, + content = { + UsageStatsChoices() + DisposableEffect(Unit) { + onDispose { if (!UsageStats.asked(context)) UsageStats.set(context, false) } + } + }, +) + private fun pages(context: Context, onRequestCallScreening: () -> Unit): List = buildList { // --- Safety: always first, and never skipped by an already-granted check --------- add( @@ -285,20 +300,9 @@ private fun pages(context: Context, onRequestCallScreening: () -> Unit): List Unit): List Unit, onDone: () -> Unit) { +fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit, onlyUsageNotes: Boolean = false) { val context = LocalContext.current - val pages = remember { pages(context, onRequestCallScreening) } + val pages = remember { if (onlyUsageNotes) listOf(usageNotesPage(context)) else pages(context, onRequestCallScreening) } var index by remember { mutableIntStateOf(0) } // Only ours to handle once there is a previous page to return to; at the first page, the @@ -347,17 +351,20 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) { horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(Modifier.height(24.dp)) - LinearProgressIndicator( - progress = { (index + 1f) / pages.size }, - modifier = Modifier.fillMaxWidth().height(10.dp).clip(MaterialTheme.shapes.extraLarge), - ) - Spacer(Modifier.height(12.dp)) - Text( - "${index + 1} of ${pages.size}", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + // A single page has no progress to show. + if (pages.size > 1) { + LinearProgressIndicator( + progress = { (index + 1f) / pages.size }, + modifier = Modifier.fillMaxWidth().height(10.dp).clip(MaterialTheme.shapes.extraLarge), + ) + Spacer(Modifier.height(12.dp)) + Text( + "${index + 1} of ${pages.size}", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } // The page scrolls between the progress bar and the buttons, so a tall page (the usage // note's choices, say) never pushes Next off the screen. Short pages stay centred. BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) { 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 4a06381..86df37d 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/UsageStatsChoices.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/UsageStatsChoices.kt @@ -2,28 +2,23 @@ package app.seb3thehacker.gearslip.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.toggleable -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.Send -import androidx.compose.material3.AlertDialog import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue @@ -158,38 +153,3 @@ private fun Choice(label: String, checked: Boolean, onChange: (Boolean) -> Unit) Checkbox(checked = checked, onCheckedChange = null, modifier = Modifier.padding(vertical = 8.dp)) } } - -/** - * Asks once, for people who set Gearslip up before usage notes existed and so never saw the - * setup page. Either answer is final; Settings changes it later. - */ -@Composable -internal fun UsageStatsPrompt() { - val context = LocalContext.current - var show by remember { mutableStateOf(!UsageStats.asked(context)) } - if (!show) return - AlertDialog( - // An answer is needed: tapping outside would just bring it back next time. - onDismissRequest = {}, - title = { Text(USAGE_STATS_TITLE) }, - text = { - Column(Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text(USAGE_STATS_INTRO) - Text( - "Turning this on also shares the Android version, the phone model, and the cars you " + - "connect to. You can untick any of them in Settings.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - }, - confirmButton = { - TextButton(onClick = { - UsageStats.set(context, true) - show = false - UsageStats.choicesClosed(context) - }) { Text("Turn on") } - }, - dismissButton = { TextButton(onClick = { UsageStats.set(context, false); show = false }) { Text("No thanks") } }, - ) -}