mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
Home warns when the phone charges but no car answers, and Help covers GrapheneOS's USB data setting
This commit is contained in:
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() ?: "?"
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
Reference in new issue
Block a user