mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
car: say when the phone's location is off, ten seconds into a drive, with Don't show again and a setting to turn the warning off
This commit is contained in:
1 parent
9b7734bec8
commit
815a75ad6d
6 files changed
+124
-1
No files matched your search
@@ -5,6 +5,7 @@ import app.seb3thehacker.gearslip.audio.AudioLink
|
||||
import app.seb3thehacker.gearslip.audio.CarMic
|
||||
import app.seb3thehacker.gearslip.media.CarMediaStatus
|
||||
import app.seb3thehacker.gearslip.car.BluetoothAudioCheck
|
||||
import app.seb3thehacker.gearslip.car.LocationCheck
|
||||
import app.seb3thehacker.gearslip.car.CarBluetooth
|
||||
import app.seb3thehacker.gearslip.car.ClusterNav
|
||||
import app.seb3thehacker.gearslip.car.DirectionsVoice
|
||||
@@ -167,6 +168,7 @@ class GearslipRunner(
|
||||
CarMic.current = null
|
||||
CarBluetooth.onCarDisconnected()
|
||||
BluetoothAudioCheck.onCarDisconnected()
|
||||
LocationCheck.onCarDisconnected()
|
||||
mediaStatus?.stop()
|
||||
mediaStatus = null
|
||||
clusterNav?.stop()
|
||||
@@ -1016,6 +1018,7 @@ class GearslipRunner(
|
||||
projection.onSurfaceReady(surface, width, height, config.density)
|
||||
SessionStatus.projecting()
|
||||
BluetoothAudioCheck.onProjecting()
|
||||
LocationCheck.onProjecting()
|
||||
}
|
||||
}.onFailure { e -> log.e("encoder start failed", e) }
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ import androidx.compose.ui.unit.dp
|
||||
/**
|
||||
* Asks once per car whether to pair it over Bluetooth, then waits while the phone shows its
|
||||
* pairing dialog. See [CarBluetooth]. With no pairing to do, says when Bluetooth audio isn't
|
||||
* connected; see [BluetoothAudioCheck]. Unlike the call prompt, a tap outside the card does nothing:
|
||||
* connected (see [BluetoothAudioCheck]), or else when the phone's location is off (see
|
||||
* [LocationCheck]). Unlike the call prompt, a tap outside the card does nothing:
|
||||
* the driver picks Pair or Not now.
|
||||
*/
|
||||
@Composable
|
||||
@@ -41,12 +42,20 @@ internal fun BluetoothPairOverlay() {
|
||||
val missing by BluetoothAudioCheck.missing.collectAsState()
|
||||
val dismissed by BluetoothAudioCheck.carDismissed.collectAsState()
|
||||
val warn by CarSettings.bluetoothWarning.collectAsState()
|
||||
val locationOff by LocationCheck.off.collectAsState()
|
||||
val locationDismissed by LocationCheck.carDismissed.collectAsState()
|
||||
if (missing && bluetoothAudio && warn && !dismissed) PairCard(
|
||||
title = "Bluetooth isn't connected",
|
||||
body = "Music plays over Bluetooth, but your phone isn't connected to the car. " +
|
||||
"Connect it from the car's Bluetooth menu or your phone's.",
|
||||
secondary = "Don't show again" to BluetoothAudioCheck::neverAgain,
|
||||
primary = "OK" to BluetoothAudioCheck::dismissOnCar,
|
||||
) else if (locationOff && !locationDismissed) PairCard(
|
||||
title = "Location is off",
|
||||
body = "Map apps can't show where you are or give directions. Turn on location " +
|
||||
"in your phone's quick settings when you're parked.",
|
||||
secondary = "Don't show again" to LocationCheck::neverAgain,
|
||||
primary = "OK" to LocationCheck::dismissOnCar,
|
||||
)
|
||||
}
|
||||
CarBluetooth.Prompt.ASK -> PairCard(
|
||||
|
||||
@@ -68,6 +68,7 @@ object CarServices {
|
||||
DirectionsVoice.init(appContext)
|
||||
CarBluetooth.init(appContext)
|
||||
BluetoothAudioCheck.init(appContext)
|
||||
LocationCheck.init(appContext)
|
||||
CarKeys.init(appContext)
|
||||
VoiceReply.init(appContext)
|
||||
// Loaded now, while nothing's waiting on it, so the first reply isn't slower than the rest.
|
||||
|
||||
@@ -30,6 +30,7 @@ object CarSettings {
|
||||
private const val KEY_PIPE_AUDIO = "car_pipe_audio"
|
||||
private const val KEY_SPEAK_DIRECTIONS = "car_speak_directions"
|
||||
private const val KEY_BLUETOOTH_WARNING = "car_bluetooth_warning"
|
||||
private const val KEY_LOCATION_WARNING = "car_location_warning"
|
||||
private const val KEY_AUDIO_CHOICE_ASKED = "car_audio_choice_asked"
|
||||
private const val KEY_LAST_NAV = "car_last_nav"
|
||||
private const val KEY_LAST_MEDIA = "car_last_media"
|
||||
@@ -62,6 +63,7 @@ private const val KEY_BLUETOOTH_WARNING = "car_bluetooth_warning"
|
||||
private val pipeAudioFlow = MutableStateFlow(true)
|
||||
private val speakDirectionsFlow = MutableStateFlow(true)
|
||||
private val bluetoothWarningFlow = MutableStateFlow(true)
|
||||
private val locationWarningFlow = MutableStateFlow(true)
|
||||
private val lastNavFlow = MutableStateFlow<String?>(null)
|
||||
private val lastMediaFlow = MutableStateFlow<String?>(null)
|
||||
private val autoplayFlow = MutableStateFlow(false)
|
||||
@@ -163,6 +165,9 @@ private const val KEY_BLUETOOTH_WARNING = "car_bluetooth_warning"
|
||||
/** With Bluetooth audio, warn when the phone isn't connected to the car; see BluetoothAudioCheck. */
|
||||
val bluetoothWarning: StateFlow<Boolean> = bluetoothWarningFlow
|
||||
|
||||
/** Warn on the car when the phone's location is off; see LocationCheck. */
|
||||
val locationWarning: StateFlow<Boolean> = locationWarningFlow
|
||||
|
||||
/**
|
||||
* Whether the driver has already been offered Bluetooth after turning down the capture
|
||||
* dialog. Offered once only: someone who chose to keep the USB route shouldn't be nagged.
|
||||
@@ -223,6 +228,7 @@ private const val KEY_BLUETOOTH_WARNING = "car_bluetooth_warning"
|
||||
// own voice already reaches the car.
|
||||
speakDirectionsFlow.value = AppSettings.getString(app, KEY_SPEAK_DIRECTIONS, pipeAudioFlow.value.toString()) == "true"
|
||||
bluetoothWarningFlow.value = AppSettings.getString(app, KEY_BLUETOOTH_WARNING, "true") == "true"
|
||||
locationWarningFlow.value = AppSettings.getString(app, KEY_LOCATION_WARNING, "true") == "true"
|
||||
nightFlow.value = runCatching {
|
||||
NightMode.valueOf(AppSettings.getString(app, KEY_NIGHT, NightMode.AUTO.name))
|
||||
}.getOrDefault(NightMode.AUTO)
|
||||
@@ -304,6 +310,11 @@ private const val KEY_BLUETOOTH_WARNING = "car_bluetooth_warning"
|
||||
AppSettings.putString(app, KEY_BLUETOOTH_WARNING, on.toString())
|
||||
}
|
||||
|
||||
fun setLocationWarning(on: Boolean) {
|
||||
locationWarningFlow.value = on
|
||||
AppSettings.putString(app, KEY_LOCATION_WARNING, on.toString())
|
||||
}
|
||||
|
||||
fun setExperimentalFeaturesEnabled(on: Boolean) {
|
||||
experimentalFeaturesFlow.value = on
|
||||
AppSettings.putString(app, KEY_EXPERIMENTAL_FEATURES, on.toString())
|
||||
|
||||
@@ -55,6 +55,7 @@ fun CarSettingsScreen() {
|
||||
val pipeAudio by CarSettings.pipeAudio.collectAsState()
|
||||
val speakDirections by CarSettings.speakDirections.collectAsState()
|
||||
val bluetoothWarning by CarSettings.bluetoothWarning.collectAsState()
|
||||
val locationWarning by CarSettings.locationWarning.collectAsState()
|
||||
val autoplay by CarSettings.autoplay.collectAsState()
|
||||
val playerTitle by CarSettings.playerTitle.collectAsState()
|
||||
val theme by CarSettings.appTheme.collectAsState()
|
||||
@@ -145,6 +146,11 @@ fun CarSettingsScreen() {
|
||||
bluetoothWarning,
|
||||
) { CarSettings.setBluetoothWarning(it) }
|
||||
}
|
||||
ChoiceRow(
|
||||
"Warn when location is off",
|
||||
listOf(false to "Off", true to "On"),
|
||||
locationWarning,
|
||||
) { CarSettings.setLocationWarning(it) }
|
||||
|
||||
ChoiceRow(
|
||||
"Spoken directions",
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package app.seb3thehacker.gearslip.car
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.location.LocationManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import app.seb3thehacker.gearslip.GearslipLog
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Map apps can't find the car with the phone's location turned off. Ten seconds after the car
|
||||
* shows Gearslip, this checks, and the car says so until location comes on or the drive ends.
|
||||
*/
|
||||
object LocationCheck {
|
||||
|
||||
private val _off = MutableStateFlow(false)
|
||||
/** True while the phone's location is turned off during a drive. */
|
||||
val off: StateFlow<Boolean> = _off
|
||||
|
||||
private val _carDismissed = MutableStateFlow(false)
|
||||
/** The driver tapped OK on the car's card; it stays away for the rest of this drive. */
|
||||
val carDismissed: StateFlow<Boolean> = _carDismissed
|
||||
|
||||
private val log = GearslipLog.tagged("location")
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
private var appContext: Context? = null
|
||||
private var armed = false
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
|
||||
fun init(context: Context) {
|
||||
appContext = context.applicationContext
|
||||
}
|
||||
|
||||
/** The car shows Gearslip. Called again whenever the video restarts, so it arms once per drive. */
|
||||
fun onProjecting() = main.post {
|
||||
if (armed) return@post
|
||||
armed = true
|
||||
main.postDelayed(::start, WAIT_MS)
|
||||
}
|
||||
|
||||
fun onCarDisconnected() = main.post {
|
||||
armed = false
|
||||
main.removeCallbacksAndMessages(null)
|
||||
stopWatching()
|
||||
_off.value = false
|
||||
_carDismissed.value = false
|
||||
}
|
||||
|
||||
fun dismissOnCar() {
|
||||
_carDismissed.value = true
|
||||
}
|
||||
|
||||
/** "Don't show again" on the car: turns the warning off in Settings, for every car. */
|
||||
fun neverAgain() {
|
||||
CarSettings.setLocationWarning(false)
|
||||
log.i("location warning turned off")
|
||||
_carDismissed.value = true
|
||||
_off.value = false
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
if (!armed) return
|
||||
check()
|
||||
// Turning location on clears the warning, and turning it off again brings it back.
|
||||
val context = appContext ?: return
|
||||
val r = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) = check()
|
||||
}
|
||||
context.registerReceiver(r, IntentFilter(LocationManager.MODE_CHANGED_ACTION), Context.RECEIVER_EXPORTED)
|
||||
receiver = r
|
||||
}
|
||||
|
||||
private fun check() {
|
||||
if (!armed) return
|
||||
val manager = appContext?.getSystemService(LocationManager::class.java) ?: return
|
||||
val off = CarSettings.locationWarning.value && !manager.isLocationEnabled
|
||||
if (_off.value == off) return
|
||||
_off.value = off
|
||||
log.i(if (off) "the phone's location is off" else "the phone's location is on")
|
||||
}
|
||||
|
||||
private fun stopWatching() {
|
||||
val r = receiver ?: return
|
||||
receiver = null
|
||||
runCatching { appContext?.unregisterReceiver(r) }
|
||||
}
|
||||
|
||||
private const val WAIT_MS = 10_000L
|
||||
}
|
||||
Reference in new issue
Block a user