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_EXPERIMENTAL_FEATURES = "car_experimental_features"
private const val KEY_VOICE_ASSISTANT = "car_voice_assistant" private const val KEY_VOICE_ASSISTANT = "car_voice_assistant"
private const val KEY_CAR_SENSORS = "car_sensors_enabled" 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_SEEN_SAFETY_WARNING = "car_seen_safety_warning"
private const val KEY_UNITS = "car_units" private const val KEY_UNITS = "car_units"
private const val KEY_WHATS_NEW = "car_whats_new_after_updates" private const val KEY_WHATS_NEW = "car_whats_new_after_updates"
@@ -71,6 +72,7 @@ object CarSettings {
private val experimentalFeaturesFlow = MutableStateFlow(false) private val experimentalFeaturesFlow = MutableStateFlow(false)
private val voiceAssistantFlow = MutableStateFlow(false) private val voiceAssistantFlow = MutableStateFlow(false)
private val carSensorsFlow = MutableStateFlow(false) private val carSensorsFlow = MutableStateFlow(false)
private val mediaSearchFlow = MutableStateFlow(false)
private val seenSafetyWarningFlow = MutableStateFlow(false) private val seenSafetyWarningFlow = MutableStateFlow(false)
private val unitsFlow = MutableStateFlow(Units.AUTO) private val unitsFlow = MutableStateFlow(Units.AUTO)
@@ -171,6 +173,13 @@ object CarSettings {
*/ */
val carSensorsEnabled: StateFlow<Boolean> = carSensorsFlow 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 * 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 - * 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" experimentalFeaturesFlow.value = AppSettings.getString(app, KEY_EXPERIMENTAL_FEATURES, "false") == "true"
voiceAssistantFlow.value = AppSettings.getString(app, KEY_VOICE_ASSISTANT, "false") == "true" voiceAssistantFlow.value = AppSettings.getString(app, KEY_VOICE_ASSISTANT, "false") == "true"
carSensorsFlow.value = AppSettings.getString(app, KEY_CAR_SENSORS, "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" seenSafetyWarningFlow.value = AppSettings.getString(app, KEY_SEEN_SAFETY_WARNING, "false") == "true"
unitsFlow.value = runCatching { unitsFlow.value = runCatching {
Units.valueOf(AppSettings.getString(app, KEY_UNITS, Units.AUTO.name)) Units.valueOf(AppSettings.getString(app, KEY_UNITS, Units.AUTO.name))
@@ -275,6 +285,11 @@ object CarSettings {
AppSettings.putString(app, KEY_CAR_SENSORS, on.toString()) 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) { fun setNightMode(mode: NightMode) {
nightFlow.value = mode nightFlow.value = mode
AppSettings.putString(app, KEY_NIGHT, mode.name) AppSettings.putString(app, KEY_NIGHT, mode.name)
@@ -60,6 +60,7 @@ fun CarSettingsScreen() {
val experimentalFeatures by CarSettings.experimentalFeaturesEnabled.collectAsState() val experimentalFeatures by CarSettings.experimentalFeaturesEnabled.collectAsState()
val voiceAssistant by CarSettings.voiceAssistantEnabled.collectAsState() val voiceAssistant by CarSettings.voiceAssistantEnabled.collectAsState()
val carSensors by CarSettings.carSensorsEnabled.collectAsState() val carSensors by CarSettings.carSensorsEnabled.collectAsState()
val mediaSearch by CarSettings.mediaSearchEnabled.collectAsState()
val display by CarEnvironment.display.collectAsState() val display by CarEnvironment.display.collectAsState()
val vehicle by CarEnvironment.vehicle.collectAsState() val vehicle by CarEnvironment.vehicle.collectAsState()
val context = LocalContext.current val context = LocalContext.current
@@ -126,14 +127,13 @@ fun CarSettingsScreen() {
ChoiceRow( ChoiceRow(
"Media audio", "Media audio",
listOf(false to "Phone output", true to "Through Gearslip"), listOf(false to "Phone audio", true to "Car audio"),
pipeAudio, pipeAudio,
) { CarSettings.setPipeAudio(it) } ) { CarSettings.setPipeAudio(it) }
Text( Text(
"Through Gearslip sends the sound over the USB link. Android asks for consent the " + "Car audio sends the phone's sound to the car over USB. Android asks for permission " +
"first time a media app plays in the car, and some apps refuse to be captured at " + "when the car connects, and a few apps block it. Phone audio plays as the phone " +
"all. Phone output needs no permission: the app plays as it normally would, for " + "normally would, for example over Bluetooth to the car.",
"example over Bluetooth to the car.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -148,7 +148,7 @@ fun CarSettingsScreen() {
experimentalFeatures, experimentalFeatures,
) { CarSettings.setExperimentalFeaturesEnabled(it) } ) { CarSettings.setExperimentalFeaturesEnabled(it) }
if (experimentalFeatures) { if (experimentalFeatures) {
ExperimentalFeaturesList(voiceAssistant, carSensors) ExperimentalFeaturesList(voiceAssistant, carSensors, mediaSearch)
} }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { 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. * so the list reads fine whether there's one entry or several.
*/ */
@Composable @Composable
private fun ExperimentalFeaturesList(voiceAssistant: Boolean, carSensors: Boolean) { private fun ExperimentalFeaturesList(voiceAssistant: Boolean, carSensors: Boolean, mediaSearch: Boolean) {
Column( Column(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp), 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.", "guess, not a fact, until you've watched one change for yourself.",
checked = carSensors, checked = carSensors,
) { CarSettings.setCarSensorsEnabled(it) } ) { 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) } remember(appContext) { Prefetch.warm(appContext, force = true); CarServices.init(appContext) }
DisposableEffect(Unit) { onDispose { CarServices.shutdown() } } 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. // The map follows the light outside, whatever the app's own theme is set to.
LaunchedEffect(darkOutside) { CarServices.nav.pushConfiguration() } LaunchedEffect(darkOutside) { CarServices.nav.pushConfiguration() }
@@ -37,6 +37,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -91,6 +92,7 @@ fun MediaScreen(app: MediaApp, onExit: () -> Unit) {
val search by media.search.collectAsStateWithLifecycle() val search by media.search.collectAsStateWithLifecycle()
var tab by remember { mutableStateOf(Tab.NOW_PLAYING) } var tab by remember { mutableStateOf(Tab.NOW_PLAYING) }
var searching by remember { mutableStateOf(false) } 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. // The connection outlives this screen: the home screen's player is the same one.
LaunchedEffect(app) { CarServices.openMedia(app) } 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("Browse", tab == Tab.BROWSE, Modifier.weight(1f)) { tab = Tab.BROWSE }
MediaTab("Up next", tab == Tab.QUEUE, Modifier.weight(1f)) { tab = Tab.QUEUE } MediaTab("Up next", tab == Tab.QUEUE, Modifier.weight(1f)) { tab = Tab.QUEUE }
MediaTab("Lyrics", tab == Tab.LYRICS, Modifier.weight(1f)) { tab = Tab.LYRICS } MediaTab("Lyrics", tab == Tab.LYRICS, Modifier.weight(1f)) { tab = Tab.LYRICS }
if (now.canSearch || media.canSearchLibrary) { if (searchAllowed && (now.canSearch || media.canSearchLibrary)) {
GsIconButton( GsIconButton(
Icons.Filled.Search, Icons.Filled.Search,
"Search ${app.label}", "Search ${app.label}",