help: say most cars work, group the steps, and show the adb steps only for USB audio

This commit is contained in:
Seb3thehacker committed 2026-10-13 15:39:20 -07:00
1 parent 3869258e0e
commit 6faad9fdd5
1 file changed
+85 -56
@@ -1,6 +1,7 @@
package app.seb3thehacker.gearslip.ui package app.seb3thehacker.gearslip.ui
import app.seb3thehacker.gearslip.BuildConfig import app.seb3thehacker.gearslip.BuildConfig
import app.seb3thehacker.gearslip.car.CarSettings
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -23,13 +24,15 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
/** /**
* One step of the setup list: a sentence that says what to do, and optionally how. * One step: a sentence that says what to do, and optionally how.
* *
* [command] is for a step that cannot be done on the phone; it is shown as something to copy * [command] is for a step that cannot be done on the phone; it is shown as something to copy
* rather than to read, because a mistyped package name fails silently. [onClick] is for a step * rather than to read, because a mistyped package name fails silently. [onClick] is for a step
@@ -43,47 +46,57 @@ private class Step(
val onClick: (() -> Unit)? = null, val onClick: (() -> Unit)? = null,
) )
private fun setupSteps(onRequestCallScreening: () -> Unit) = listOf( private val FIRST_DRIVE = listOf(
Step("Use a car from 2020 or earlier.", "Newer head units reject the certificate."), Step("Turn off Android Auto.", "Disable or uninstall the Android Auto app, so the car talks to Gearslip instead."),
Step("Disable or uninstall the Android Auto app."), Step("Load a certificate.", "In Settings, download one or import your own."),
Step("Load a certificate.", "Open Settings, then download or import a certificate."),
Step( Step(
"Allow the microphone, so music can reach the car.", "Allow the microphone.",
"Android asks for the microphone before it will let any app pass music along. " + "Android won't let an app pass music to the car without it. Gearslip listens only when " +
"Gearslip listens only when you tap Reply or press the voice button. The rest of the " + "you tap the mic or press the talk button on your wheel.",
"time it copies what a music app is playing, and nothing else.", ),
)
private fun inTheCar(onRequestCallScreening: () -> Unit) = listOf(
Step(
"Plug in, or connect wirelessly.",
"Gearslip opens by itself when the car connects. For wireless, pair the phone with the " +
"car first, then tap Connect wirelessly on Home.",
), ),
Step( Step(
"Optional: stop Android asking about audio every drive.", "Pair over Bluetooth for calls.",
"Connect the phone to a computer with USB debugging turned on, and run this command. " + "Calls play through the car once your phone and the car are paired. Gearslip offers to " +
"It tells Android to trust Gearslip with audio from now on. Skip it and everything " + "pair the first time you plug in.",
"still works, but you have to tap Start once each time you set off.",
command = "adb shell appops set ${BuildConfig.APPLICATION_ID} PROJECT_MEDIA allow",
), ),
Step( Step(
"Optional: keep notifications readable while music plays.", "Answer calls from the car screen.",
"Android treats sending audio to the car like sharing your screen, and hides what your " + "Set Gearslip as your caller ID and spam app. Without it, calls still ring, but only " +
"notifications say. Run this command too, and Gearslip switches that off only while " + "the phone can answer them.",
"music is going to the car, then puts it back.",
command = "adb shell pm grant ${BuildConfig.APPLICATION_ID} android.permission.WRITE_SECURE_SETTINGS",
),
Step(
"Optional: let the car screen answer and decline calls.",
"This sets Gearslip as your phone's caller ID and spam-blocking app, the same kind of " +
"consent a call-blocking app asks for. Without it, calls still ring, but only the " +
"phone itself can answer or decline them.",
buttonLabel = "Set up", buttonLabel = "Set up",
onClick = onRequestCallScreening, onClick = onRequestCallScreening,
), ),
Step("Plug the phone into the car.", "Gearslip opens by itself when the head unit connects."),
) )
/** The full setup reference, behind the help button: what the one-time guide walks through, kept private val USB_AUDIO = listOf(
* here too so it can be reread without repeating the guide itself. */ Step(
"Stop the audio prompt each drive.",
"Connect the phone to a computer with USB debugging on, and run this once. Skip it and " +
"you tap Start each time you set off.",
command = "adb shell appops set ${BuildConfig.APPLICATION_ID} PROJECT_MEDIA allow",
),
Step(
"Keep notifications readable.",
"Android hides what notifications say while it shares audio. Run this too, and Gearslip " +
"lifts that only while music goes to the car.",
command = "adb shell pm grant ${BuildConfig.APPLICATION_ID} android.permission.WRITE_SECURE_SETTINGS",
),
)
/** Which cars work, and the setup the one-time guide walks through, to reread at any time. */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HelpScreen(onBack: () -> Unit, onRequestCallScreening: () -> Unit) { fun HelpScreen(onBack: () -> Unit, onRequestCallScreening: () -> Unit) {
val steps = remember(onRequestCallScreening) { setupSteps(onRequestCallScreening) } val car = remember(onRequestCallScreening) { inTheCar(onRequestCallScreening) }
val usb by CarSettings.pipeAudio.collectAsState()
Scaffold( Scaffold(
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
@@ -104,39 +117,55 @@ fun HelpScreen(onBack: () -> Unit, onRequestCallScreening: () -> Unit) {
.padding(padding) .padding(padding)
.padding(horizontal = 24.dp) .padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(24.dp),
) { ) {
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(0.dp))
SettingsSection("Which cars work") {
Text(
"Most cars work. Starting around 2020, some cars added newer security " +
"standards that reject Gearslip's certificate, and Gearslip can't run in those cars.",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(16.dp),
)
}
Steps("Before your first drive", FIRST_DRIVE)
Steps("In the car", car)
if (usb) Steps("USB audio", USB_AUDIO)
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Column(verticalArrangement = Arrangement.spacedBy(18.dp)) { }
steps.forEachIndexed { index, step -> }
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { }
Text(
"${index + 1}", /** A titled card of numbered steps. */
style = MaterialTheme.typography.titleMedium, @Composable
fontWeight = FontWeight.Bold, private fun Steps(title: String, steps: List<Step>) {
color = MaterialTheme.colorScheme.primary, SettingsSection(title) {
modifier = Modifier.width(16.dp), steps.forEachIndexed { index, step ->
) if (index > 0) SettingsDivider()
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Row(
Text(step.action, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold) Modifier.fillMaxWidth().padding(16.dp),
step.detail?.let { horizontalArrangement = Arrangement.spacedBy(16.dp),
Text( ) {
it, Text(
style = MaterialTheme.typography.bodyMedium, "${index + 1}",
color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.titleMedium,
) fontWeight = FontWeight.Bold,
} color = MaterialTheme.colorScheme.primary,
step.command?.let { CopyableCommand(it) } modifier = Modifier.width(16.dp),
if (step.onClick != null) { )
FilledTonalButton(onClick = step.onClick, modifier = Modifier.padding(top = 4.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(step.buttonLabel ?: "Open") Text(step.action, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold)
} step.detail?.let {
} Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
step.command?.let { CopyableCommand(it) }
if (step.onClick != null) {
FilledTonalButton(onClick = step.onClick, modifier = Modifier.padding(top = 4.dp)) {
Text(step.buttonLabel ?: "Open")
} }
} }
} }
} }
Spacer(Modifier.height(32.dp))
} }
} }
} }