mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +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.audio.CarMic
|
||||||
import app.seb3thehacker.gearslip.media.CarMediaStatus
|
import app.seb3thehacker.gearslip.media.CarMediaStatus
|
||||||
import app.seb3thehacker.gearslip.car.BluetoothAudioCheck
|
import app.seb3thehacker.gearslip.car.BluetoothAudioCheck
|
||||||
|
import app.seb3thehacker.gearslip.car.LocationCheck
|
||||||
import app.seb3thehacker.gearslip.car.CarBluetooth
|
import app.seb3thehacker.gearslip.car.CarBluetooth
|
||||||
import app.seb3thehacker.gearslip.car.ClusterNav
|
import app.seb3thehacker.gearslip.car.ClusterNav
|
||||||
import app.seb3thehacker.gearslip.car.DirectionsVoice
|
import app.seb3thehacker.gearslip.car.DirectionsVoice
|
||||||
@@ -167,6 +168,7 @@ class GearslipRunner(
|
|||||||
CarMic.current = null
|
CarMic.current = null
|
||||||
CarBluetooth.onCarDisconnected()
|
CarBluetooth.onCarDisconnected()
|
||||||
BluetoothAudioCheck.onCarDisconnected()
|
BluetoothAudioCheck.onCarDisconnected()
|
||||||
|
LocationCheck.onCarDisconnected()
|
||||||
mediaStatus?.stop()
|
mediaStatus?.stop()
|
||||||
mediaStatus = null
|
mediaStatus = null
|
||||||
clusterNav?.stop()
|
clusterNav?.stop()
|
||||||
@@ -1016,6 +1018,7 @@ class GearslipRunner(
|
|||||||
projection.onSurfaceReady(surface, width, height, config.density)
|
projection.onSurfaceReady(surface, width, height, config.density)
|
||||||
SessionStatus.projecting()
|
SessionStatus.projecting()
|
||||||
BluetoothAudioCheck.onProjecting()
|
BluetoothAudioCheck.onProjecting()
|
||||||
|
LocationCheck.onProjecting()
|
||||||
}
|
}
|
||||||
}.onFailure { e -> log.e("encoder start failed", e) }
|
}.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
|
* 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
|
* 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.
|
* the driver picks Pair or Not now.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
@@ -41,12 +42,20 @@ internal fun BluetoothPairOverlay() {
|
|||||||
val missing by BluetoothAudioCheck.missing.collectAsState()
|
val missing by BluetoothAudioCheck.missing.collectAsState()
|
||||||
val dismissed by BluetoothAudioCheck.carDismissed.collectAsState()
|
val dismissed by BluetoothAudioCheck.carDismissed.collectAsState()
|
||||||
val warn by CarSettings.bluetoothWarning.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(
|
if (missing && bluetoothAudio && warn && !dismissed) PairCard(
|
||||||
title = "Bluetooth isn't connected",
|
title = "Bluetooth isn't connected",
|
||||||
body = "Music plays over Bluetooth, but your phone isn't connected to the car. " +
|
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.",
|
"Connect it from the car's Bluetooth menu or your phone's.",
|
||||||
secondary = "Don't show again" to BluetoothAudioCheck::neverAgain,
|
secondary = "Don't show again" to BluetoothAudioCheck::neverAgain,
|
||||||
primary = "OK" to BluetoothAudioCheck::dismissOnCar,
|
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(
|
CarBluetooth.Prompt.ASK -> PairCard(
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ object CarServices {
|
|||||||
DirectionsVoice.init(appContext)
|
DirectionsVoice.init(appContext)
|
||||||
CarBluetooth.init(appContext)
|
CarBluetooth.init(appContext)
|
||||||
BluetoothAudioCheck.init(appContext)
|
BluetoothAudioCheck.init(appContext)
|
||||||
|
LocationCheck.init(appContext)
|
||||||
CarKeys.init(appContext)
|
CarKeys.init(appContext)
|
||||||
VoiceReply.init(appContext)
|
VoiceReply.init(appContext)
|
||||||
// Loaded now, while nothing's waiting on it, so the first reply isn't slower than the rest.
|
// 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_PIPE_AUDIO = "car_pipe_audio"
|
||||||
private const val KEY_SPEAK_DIRECTIONS = "car_speak_directions"
|
private const val KEY_SPEAK_DIRECTIONS = "car_speak_directions"
|
||||||
private const val KEY_BLUETOOTH_WARNING = "car_bluetooth_warning"
|
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_AUDIO_CHOICE_ASKED = "car_audio_choice_asked"
|
||||||
private const val KEY_LAST_NAV = "car_last_nav"
|
private const val KEY_LAST_NAV = "car_last_nav"
|
||||||
private const val KEY_LAST_MEDIA = "car_last_media"
|
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 pipeAudioFlow = MutableStateFlow(true)
|
||||||
private val speakDirectionsFlow = MutableStateFlow(true)
|
private val speakDirectionsFlow = MutableStateFlow(true)
|
||||||
private val bluetoothWarningFlow = MutableStateFlow(true)
|
private val bluetoothWarningFlow = MutableStateFlow(true)
|
||||||
|
private val locationWarningFlow = MutableStateFlow(true)
|
||||||
private val lastNavFlow = MutableStateFlow<String?>(null)
|
private val lastNavFlow = MutableStateFlow<String?>(null)
|
||||||
private val lastMediaFlow = MutableStateFlow<String?>(null)
|
private val lastMediaFlow = MutableStateFlow<String?>(null)
|
||||||
private val autoplayFlow = MutableStateFlow(false)
|
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. */
|
/** With Bluetooth audio, warn when the phone isn't connected to the car; see BluetoothAudioCheck. */
|
||||||
val bluetoothWarning: StateFlow<Boolean> = bluetoothWarningFlow
|
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
|
* 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.
|
* 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.
|
// own voice already reaches the car.
|
||||||
speakDirectionsFlow.value = AppSettings.getString(app, KEY_SPEAK_DIRECTIONS, pipeAudioFlow.value.toString()) == "true"
|
speakDirectionsFlow.value = AppSettings.getString(app, KEY_SPEAK_DIRECTIONS, pipeAudioFlow.value.toString()) == "true"
|
||||||
bluetoothWarningFlow.value = AppSettings.getString(app, KEY_BLUETOOTH_WARNING, "true") == "true"
|
bluetoothWarningFlow.value = AppSettings.getString(app, KEY_BLUETOOTH_WARNING, "true") == "true"
|
||||||
|
locationWarningFlow.value = AppSettings.getString(app, KEY_LOCATION_WARNING, "true") == "true"
|
||||||
nightFlow.value = runCatching {
|
nightFlow.value = runCatching {
|
||||||
NightMode.valueOf(AppSettings.getString(app, KEY_NIGHT, NightMode.AUTO.name))
|
NightMode.valueOf(AppSettings.getString(app, KEY_NIGHT, NightMode.AUTO.name))
|
||||||
}.getOrDefault(NightMode.AUTO)
|
}.getOrDefault(NightMode.AUTO)
|
||||||
@@ -304,6 +310,11 @@ private const val KEY_BLUETOOTH_WARNING = "car_bluetooth_warning"
|
|||||||
AppSettings.putString(app, KEY_BLUETOOTH_WARNING, on.toString())
|
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) {
|
fun setExperimentalFeaturesEnabled(on: Boolean) {
|
||||||
experimentalFeaturesFlow.value = on
|
experimentalFeaturesFlow.value = on
|
||||||
AppSettings.putString(app, KEY_EXPERIMENTAL_FEATURES, on.toString())
|
AppSettings.putString(app, KEY_EXPERIMENTAL_FEATURES, on.toString())
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ fun CarSettingsScreen() {
|
|||||||
val pipeAudio by CarSettings.pipeAudio.collectAsState()
|
val pipeAudio by CarSettings.pipeAudio.collectAsState()
|
||||||
val speakDirections by CarSettings.speakDirections.collectAsState()
|
val speakDirections by CarSettings.speakDirections.collectAsState()
|
||||||
val bluetoothWarning by CarSettings.bluetoothWarning.collectAsState()
|
val bluetoothWarning by CarSettings.bluetoothWarning.collectAsState()
|
||||||
|
val locationWarning by CarSettings.locationWarning.collectAsState()
|
||||||
val autoplay by CarSettings.autoplay.collectAsState()
|
val autoplay by CarSettings.autoplay.collectAsState()
|
||||||
val playerTitle by CarSettings.playerTitle.collectAsState()
|
val playerTitle by CarSettings.playerTitle.collectAsState()
|
||||||
val theme by CarSettings.appTheme.collectAsState()
|
val theme by CarSettings.appTheme.collectAsState()
|
||||||
@@ -145,6 +146,11 @@ fun CarSettingsScreen() {
|
|||||||
bluetoothWarning,
|
bluetoothWarning,
|
||||||
) { CarSettings.setBluetoothWarning(it) }
|
) { CarSettings.setBluetoothWarning(it) }
|
||||||
}
|
}
|
||||||
|
ChoiceRow(
|
||||||
|
"Warn when location is off",
|
||||||
|
listOf(false to "Off", true to "On"),
|
||||||
|
locationWarning,
|
||||||
|
) { CarSettings.setLocationWarning(it) }
|
||||||
|
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Spoken directions",
|
"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