ask for car audio on connect, media search behind experimental

This commit is contained in:
Seb3thehacker committed 2026-10-05 17:03:43 -07:00
1 parent 24d7e4c415
commit 672a68cd1b
4 files changed
+36 -8

No files matched your search

@@ -38,6 +38,7 @@ object CarSettings {
private const val KEY_EXPERIMENTAL_FEATURES = "car_experimental_features"
private const val KEY_VOICE_ASSISTANT = "car_voice_assistant"
private const val KEY_CAR_SENSORS = "car_sensors_enabled"
private const val KEY_MEDIA_SEARCH = "car_media_search_enabled"
private const val KEY_SEEN_SAFETY_WARNING = "car_seen_safety_warning"
private const val KEY_UNITS = "car_units"
private const val KEY_WHATS_NEW = "car_whats_new_after_updates"
@@ -71,6 +72,7 @@ object CarSettings {
private val experimentalFeaturesFlow = MutableStateFlow(false)
private val voiceAssistantFlow = MutableStateFlow(false)
private val carSensorsFlow = MutableStateFlow(false)
private val mediaSearchFlow = MutableStateFlow(false)
private val seenSafetyWarningFlow = MutableStateFlow(false)
private val unitsFlow = MutableStateFlow(Units.AUTO)
@@ -171,6 +173,13 @@ object CarSettings {
*/
val carSensorsEnabled: StateFlow<Boolean> = carSensorsFlow
/**
* Searching inside a media app is experimental and off by default: many apps claim to take
* searches and then ignore them or play something else. The media screen's search button
* shows only once this and [experimentalFeaturesEnabled] are both on.
*/
val mediaSearchEnabled: StateFlow<Boolean> = mediaSearchFlow
/**
* Whether the car screen's own "drive safely" warning has already been shown and dismissed
* once, from this same install. Separate from the phone side's own compat/safety dialogs -
@@ -207,6 +216,7 @@ object CarSettings {
experimentalFeaturesFlow.value = AppSettings.getString(app, KEY_EXPERIMENTAL_FEATURES, "false") == "true"
voiceAssistantFlow.value = AppSettings.getString(app, KEY_VOICE_ASSISTANT, "false") == "true"
carSensorsFlow.value = AppSettings.getString(app, KEY_CAR_SENSORS, "false") == "true"
mediaSearchFlow.value = AppSettings.getString(app, KEY_MEDIA_SEARCH, "false") == "true"
seenSafetyWarningFlow.value = AppSettings.getString(app, KEY_SEEN_SAFETY_WARNING, "false") == "true"
unitsFlow.value = runCatching {
Units.valueOf(AppSettings.getString(app, KEY_UNITS, Units.AUTO.name))
@@ -275,6 +285,11 @@ object CarSettings {
AppSettings.putString(app, KEY_CAR_SENSORS, on.toString())
}
fun setMediaSearchEnabled(on: Boolean) {
mediaSearchFlow.value = on
AppSettings.putString(app, KEY_MEDIA_SEARCH, on.toString())
}
fun setNightMode(mode: NightMode) {
nightFlow.value = mode
AppSettings.putString(app, KEY_NIGHT, mode.name)
@@ -60,6 +60,7 @@ fun CarSettingsScreen() {
val experimentalFeatures by CarSettings.experimentalFeaturesEnabled.collectAsState()
val voiceAssistant by CarSettings.voiceAssistantEnabled.collectAsState()
val carSensors by CarSettings.carSensorsEnabled.collectAsState()
val mediaSearch by CarSettings.mediaSearchEnabled.collectAsState()
val display by CarEnvironment.display.collectAsState()
val vehicle by CarEnvironment.vehicle.collectAsState()
val context = LocalContext.current
@@ -126,14 +127,13 @@ fun CarSettingsScreen() {
ChoiceRow(
"Media audio",
listOf(false to "Phone output", true to "Through Gearslip"),
listOf(false to "Phone audio", true to "Car audio"),
pipeAudio,
) { CarSettings.setPipeAudio(it) }
Text(
"Through Gearslip sends the sound over the USB link. Android asks for consent the " +
"first time a media app plays in the car, and some apps refuse to be captured at " +
"all. Phone output needs no permission: the app plays as it normally would, for " +
"example over Bluetooth to the car.",
"Car audio sends the phone's sound to the car over USB. Android asks for permission " +
"when the car connects, and a few apps block it. Phone audio plays as the phone " +
"normally would, for example over Bluetooth to the car.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -148,7 +148,7 @@ fun CarSettingsScreen() {
experimentalFeatures,
) { CarSettings.setExperimentalFeaturesEnabled(it) }
if (experimentalFeatures) {
ExperimentalFeaturesList(voiceAssistant, carSensors)
ExperimentalFeaturesList(voiceAssistant, carSensors, mediaSearch)
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
@@ -205,7 +205,7 @@ private fun <T> ChoiceRow(title: String, options: List<Pair<T, String>>, selecte
* so the list reads fine whether there's one entry or several.
*/
@Composable
private fun ExperimentalFeaturesList(voiceAssistant: Boolean, carSensors: Boolean) {
private fun ExperimentalFeaturesList(voiceAssistant: Boolean, carSensors: Boolean, mediaSearch: Boolean) {
Column(
Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp),
@@ -228,6 +228,13 @@ private fun ExperimentalFeaturesList(voiceAssistant: Boolean, carSensors: Boolea
"guess, not a fact, until you've watched one change for yourself.",
checked = carSensors,
) { CarSettings.setCarSensorsEnabled(it) }
ExperimentalFeatureRow(
title = "Media search",
description = "Adds a search button to the media screen. Some apps ignore the search " +
"or play something else.",
checked = mediaSearch,
) { CarSettings.setMediaSearchEnabled(it) }
}
}
@@ -122,6 +122,10 @@ fun CarUi() {
}
remember(appContext) { Prefetch.warm(appContext, force = true); CarServices.init(appContext) }
DisposableEffect(Unit) { onDispose { CarServices.shutdown() } }
// Ask for the phone's sound as soon as the car connects, not only when a media app opens:
// a video in the web browser or a map's voice needs it too. Asks once, after the car offers
// somewhere to play it.
LaunchedEffect(Unit) { if (CarSettings.pipeAudio.value) app.seb3thehacker.gearslip.audio.CarAudio.request() }
// The map follows the light outside, whatever the app's own theme is set to.
LaunchedEffect(darkOutside) { CarServices.nav.pushConfiguration() }
@@ -37,6 +37,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -91,6 +92,7 @@ fun MediaScreen(app: MediaApp, onExit: () -> Unit) {
val search by media.search.collectAsStateWithLifecycle()
var tab by remember { mutableStateOf(Tab.NOW_PLAYING) }
var searching by remember { mutableStateOf(false) }
val searchAllowed = CarSettings.experimentalFeaturesEnabled.collectAsState().value && CarSettings.mediaSearchEnabled.collectAsState().value
// The connection outlives this screen: the home screen's player is the same one.
LaunchedEffect(app) { CarServices.openMedia(app) }
@@ -180,7 +182,7 @@ fun MediaScreen(app: MediaApp, onExit: () -> Unit) {
MediaTab("Browse", tab == Tab.BROWSE, Modifier.weight(1f)) { tab = Tab.BROWSE }
MediaTab("Up next", tab == Tab.QUEUE, Modifier.weight(1f)) { tab = Tab.QUEUE }
MediaTab("Lyrics", tab == Tab.LYRICS, Modifier.weight(1f)) { tab = Tab.LYRICS }
if (now.canSearch || media.canSearchLibrary) {
if (searchAllowed && (now.canSearch || media.canSearchLibrary)) {
GsIconButton(
Icons.Filled.Search,
"Search ${app.label}",