Home warns when the phone charges but no car answers, and Help covers GrapheneOS's USB data setting

This commit is contained in:
Seb3thehacker committed 2026-11-03 11:10:11 -08:00
1 parent ea64329f2f
commit d302caf3a4
4 files changed
+76 -6

No files matched your search

@@ -226,11 +226,11 @@ object UsageStats {
* build names, not confirmed on a real phone.
*/
fun osName(context: Context): String {
fun installed(pkg: String) = runCatching { context.packageManager.getPackageInfo(pkg, 0) }.isSuccess
fun installed(pkg: String) = installed(context, pkg)
val names = listOf("ro.build.flavor", "ro.build.display.id", "ro.lineage.version")
.joinToString(" ") { prop(it) } + " " + Build.DISPLAY + " " + Build.FINGERPRINT
return when {
installed("app.grapheneos.setupwizard") -> "GrapheneOS"
isGrapheneOs(context) -> "GrapheneOS"
prop("ro.calyxos.version").isNotEmpty() -> "CalyxOS"
installed("foundation.e.apps") -> "/e/OS"
names.contains("iode", ignoreCase = true) -> "iodéOS"
@@ -240,6 +240,12 @@ object UsageStats {
}
}
/** GrapheneOS, told apart from stock Pixel firmware by its own setup app. */
fun isGrapheneOs(context: Context) = installed(context, "app.grapheneos.setupwizard")
private fun installed(context: Context, pkg: String) =
runCatching { context.packageManager.getPackageInfo(pkg, 0) }.isSuccess
/** A system property, or "" when it isn't set or can't be read. */
private fun prop(name: String): String = runCatching {
Class.forName("android.os.SystemProperties").getMethod("get", String::class.java)
@@ -5,6 +5,7 @@ import androidx.compose.ui.res.stringResource
import app.seb3thehacker.gearslip.R
import app.seb3thehacker.gearslip.BuildConfig
import app.seb3thehacker.gearslip.car.CarSettings
import app.seb3thehacker.gearslip.stats.UsageStats
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -59,11 +60,13 @@ private val FIRST_DRIVE = listOf(
),
)
private fun inTheCar(onRequestCallScreening: () -> Unit) = listOf(
private fun inTheCar(grapheneOs: Boolean, onRequestCallScreening: () -> Unit) = listOfNotNull(
Step(
R.string.help_plug_in,
R.string.help_plug_in_detail,
),
// GrapheneOS can turn off data on the USB port, and then the car never sees the phone (#71).
if (grapheneOs) Step(R.string.help_usb_data, R.string.help_usb_data_detail) else null,
Step(
R.string.help_pair,
R.string.help_pair_detail,
@@ -93,9 +96,9 @@ private val USB_AUDIO = listOf(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HelpScreen(onBack: () -> Unit, onRequestCallScreening: () -> Unit) {
val car = remember(onRequestCallScreening) { inTheCar(onRequestCallScreening) }
val usb by CarSettings.pipeAudio.collectAsState()
val context = LocalContext.current
val car = remember(onRequestCallScreening) { inTheCar(UsageStats.isGrapheneOs(context), onRequestCallScreening) }
val usb by CarSettings.pipeAudio.collectAsState()
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
@@ -25,6 +25,15 @@ import app.seb3thehacker.gearslip.car.CarSettings
import app.seb3thehacker.gearslip.car.BluetoothAudioCheck
import android.provider.Settings
import android.content.Intent
import android.content.BroadcastReceiver
import android.content.IntentFilter
import android.os.BatteryManager
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import app.seb3thehacker.gearslip.stats.UsageStats
import kotlinx.coroutines.delay
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -258,6 +267,9 @@ private fun StatusCard(status: SessionStatus.Snapshot) {
}
val live = status.phase == SessionStatus.Phase.CONNECTING || status.phase == SessionStatus.Phase.PROJECTING
val context = LocalContext.current
val grapheneOs = remember { UsageStats.isGrapheneOs(context) }
val noCar = chargingWithNoCar(status.phase == SessionStatus.Phase.IDLE)
Surface(color = container, contentColor = content, shape = MaterialTheme.shapes.extraLarge) {
Column(Modifier.fillMaxWidth().padding(24.dp)) {
Row(
@@ -286,7 +298,13 @@ private fun StatusCard(status: SessionStatus.Snapshot) {
SessionStatus.Phase.FAILED -> status.headline
}
val detail = when (status.phase) {
SessionStatus.Phase.IDLE -> stringResource(R.string.status_idle_detail)
SessionStatus.Phase.IDLE -> stringResource(
when {
!noCar -> R.string.status_idle_detail
grapheneOs -> R.string.status_no_car_graphene
else -> R.string.status_no_car
},
)
SessionStatus.Phase.PROJECTING -> stringResource(R.string.status_projecting_detail)
SessionStatus.Phase.DISCONNECTED -> stringResource(R.string.status_disconnected_detail)
else -> status.detail
@@ -296,6 +314,13 @@ private fun StatusCard(status: SessionStatus.Snapshot) {
Text(detail, style = MaterialTheme.typography.bodyMedium)
}
}
// GrapheneOS keeps its USB-C port setting under Security & privacy.
if (noCar && grapheneOs) {
Spacer(Modifier.height(12.dp))
Button(onClick = {
runCatching { context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS)) }
}) { Text(stringResource(R.string.status_open_settings)) }
}
// The other way in: wireless, right under the cable's instructions.
if (!live) {
Spacer(Modifier.height(20.dp))
@@ -305,6 +330,37 @@ private fun StatusCard(status: SessionStatus.Snapshot) {
}
}
/**
* True once the phone has charged over a cable for [NO_CAR_MS] while Gearslip sat idle. With
* USB data blocked, by a charge-only cable or GrapheneOS's port setting, Android never tells
* Gearslip a car is there, so charging with no car is the only sign there is. A wall charger
* looks the same, which is why the hint only asks the driver to check.
*/
@Composable
private fun chargingWithNoCar(idle: Boolean): Boolean {
val context = LocalContext.current
var plugged by remember { mutableStateOf(false) }
DisposableEffect(context) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) {
val how = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0)
plugged = how == BatteryManager.BATTERY_PLUGGED_USB || how == BatteryManager.BATTERY_PLUGGED_AC
}
}
// The battery broadcast is sticky, so registering hands back the current state at once.
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))?.let { receiver.onReceive(context, it) }
onDispose { context.unregisterReceiver(receiver) }
}
var waited by remember { mutableStateOf(false) }
LaunchedEffect(idle && plugged) {
waited = false
if (idle && plugged) { delay(NO_CAR_MS); waited = true }
}
return idle && plugged && waited
}
private const val NO_CAR_MS = 10_000L
/** The version shown on screen, straight from the manifest - one place to bump per release. */
private fun Context.appVersionName(): String =
runCatching { packageManager.getPackageInfo(packageName, 0).versionName }.getOrNull() ?: "?"
+5
View File
@@ -269,6 +269,8 @@
<string name="help_mic_detail">Android won\'t let an app pass music to the car without it. Gearslip listens only when you tap the mic or press the talk button on your wheel.</string>
<string name="help_plug_in">Plug in, or connect wirelessly.</string>
<string name="help_plug_in_detail">Gearslip opens by itself when the car connects. For wireless, pair the phone with the car first, then tap Connect wirelessly on Home.</string>
<string name="help_usb_data">Allow USB data.</string>
<string name="help_usb_data_detail">GrapheneOS can block data on the USB port. If USB-C port control is set to Charging-only, the car never sees the phone, even with a data cable. Set it to allow data.</string>
<string name="help_pair">Pair over Bluetooth for calls.</string>
<string name="help_pair_detail">Calls play through the car once your phone and the car are paired. Gearslip offers to pair the first time you plug in.</string>
<string name="help_answer">Answer calls from the car screen.</string>
@@ -858,6 +860,9 @@
<!-- Connection status on Home -->
<string name="status_idle">Not connected</string>
<string name="status_idle_detail">Plug into the car\'s USB port, or connect wirelessly. Gearslip opens by itself when the car answers.</string>
<string name="status_no_car">The phone is charging, but no car has answered. Some cables only charge; try one that carries data.</string>
<string name="status_no_car_graphene">The phone is charging, but no car has answered. Check that USB-C port control, under Security &amp; privacy, allows data and isn\'t set to Charging-only.</string>
<string name="status_open_settings">Open settings</string>
<string name="status_connecting">Connecting</string>
<string name="status_projecting">Projecting</string>
<string name="status_projecting_detail">Streaming to the head unit.</string>