ask existing users with the setup guide's usage page instead of a dialog

This commit is contained in:
Seb3thehacker committed 2026-10-05 01:39:41 -07:00
1 parent b5ff4ec22b
commit 7cc6b64ce2
3 files changed
+43 -70

No files matched your search

@@ -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()
}
/**
@@ -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<GuidePage> = 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<Gu
)
}
// --- The very end, every time: usage notes. The first time through, leaving the page
// without turning them on is a no; on a replay, the switch shows what was chosen before.
add(
GuidePage(
title = USAGE_STATS_TITLE,
explanation = USAGE_STATS_INTRO,
content = {
UsageStatsChoices()
DisposableEffect(Unit) {
onDispose { if (!UsageStats.asked(context)) UsageStats.set(context, false) }
}
},
),
)
// --- The very end, every time: usage notes. On a replay, the switch shows what was
// chosen before.
add(usageNotesPage(context))
}
/**
@@ -308,9 +312,9 @@ private fun pages(context: Context, onRequestCallScreening: () -> Unit): List<Gu
* driver takes on trust, with "Skip" sitting right next to it.
*/
@Composable
fun SetupGuideScreen(onRequestCallScreening: () -> 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()) {
@@ -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") } },
)
}