From 672a68cd1b645550551e2daf2c14dc28227fa787 Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Mon, 5 Oct 2026 17:03:43 -0700 Subject: [PATCH] ask for car audio on connect, media search behind experimental --- .../seb3thehacker/gearslip/car/CarSettings.kt | 15 +++++++++++++ .../gearslip/car/CarSettingsScreen.kt | 21 ++++++++++++------- .../app/seb3thehacker/gearslip/car/CarUi.kt | 4 ++++ .../seb3thehacker/gearslip/car/MediaScreen.kt | 4 +++- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettings.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettings.kt index 588bab4..99616be 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettings.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettings.kt @@ -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 = 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 = 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) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettingsScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettingsScreen.kt index fcfce00..fe1a617 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettingsScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettingsScreen.kt @@ -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 ChoiceRow(title: String, options: List>, 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) } } } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt index 9729ca9..03baae2 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt @@ -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() } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/MediaScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/MediaScreen.kt index 97168f7..40fc17e 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/MediaScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/MediaScreen.kt @@ -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}",