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.AppSettings
import app.seb3thehacker.gearslip.BuildConfig import app.seb3thehacker.gearslip.BuildConfig
import app.seb3thehacker.gearslip.CertProvider 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. */ /** Three screens and a back stack of depth one: no navigation library needed. */
@Composable @Composable
@@ -53,6 +54,8 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
when { when {
needsCertSetup -> Screen.CERT_SETUP needsCertSetup -> Screen.CERT_SETUP
needsSetupGuide -> Screen.SETUP_GUIDE 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 else -> Screen.HOME
}, },
) )
@@ -120,6 +123,11 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
onRequestCallScreening = onRequestCallScreening, onRequestCallScreening = onRequestCallScreening,
onDone = { screen = Screen.HOME }, 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. // A car keyboard is open: offer the phone's own, for a passenger.
@@ -127,8 +135,6 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
} }
CompatWarning() 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) 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 { private fun pages(context: Context, onRequestCallScreening: () -> Unit): List<GuidePage> = buildList {
// --- Safety: always first, and never skipped by an already-granted check --------- // --- Safety: always first, and never skipped by an already-granted check ---------
add( 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 // --- The very end, every time: usage notes. On a replay, the switch shows what was
// without turning them on is a no; on a replay, the switch shows what was chosen before. // chosen before.
add( add(usageNotesPage(context))
GuidePage(
title = USAGE_STATS_TITLE,
explanation = USAGE_STATS_INTRO,
content = {
UsageStatsChoices()
DisposableEffect(Unit) {
onDispose { if (!UsageStats.asked(context)) UsageStats.set(context, false) }
}
},
),
)
} }
/** /**
@@ -308,9 +312,9 @@ private fun pages(context: Context, onRequestCallScreening: () -> Unit): List<Gu
* driver takes on trust, with "Skip" sitting right next to it. * driver takes on trust, with "Skip" sitting right next to it.
*/ */
@Composable @Composable
fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) { fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit, onlyUsageNotes: Boolean = false) {
val context = LocalContext.current 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) } var index by remember { mutableIntStateOf(0) }
// Only ours to handle once there is a previous page to return to; at the first page, the // 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, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
LinearProgressIndicator( // A single page has no progress to show.
progress = { (index + 1f) / pages.size }, if (pages.size > 1) {
modifier = Modifier.fillMaxWidth().height(10.dp).clip(MaterialTheme.shapes.extraLarge), LinearProgressIndicator(
) progress = { (index + 1f) / pages.size },
Spacer(Modifier.height(12.dp)) modifier = Modifier.fillMaxWidth().height(10.dp).clip(MaterialTheme.shapes.extraLarge),
Text( )
"${index + 1} of ${pages.size}", Spacer(Modifier.height(12.dp))
style = MaterialTheme.typography.titleMedium, Text(
fontWeight = FontWeight.SemiBold, "${index + 1} of ${pages.size}",
color = MaterialTheme.colorScheme.onSurfaceVariant, 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 // 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. // note's choices, say) never pushes Next off the screen. Short pages stay centred.
BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) { BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) {
@@ -2,28 +2,23 @@ package app.seb3thehacker.gearslip.ui
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue 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)) 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") } },
)
}