mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-12 00:32:26 +01:00
Car maps keep their GPS with the phone locked once location is allowed all the time
A car that connected by itself with the phone locked left navigation apps without a fix until Gearslip was opened (#99). Home now asks for Allow all the time while a car is connected, and the location service starts as soon as it's granted. The weather no longer reads the location when Gearslip is opened with no car connected.
This commit is contained in:
1 parent
ba86685935
commit
3860e978ac
6 files changed
+100
-1
No files matched your search
@@ -21,6 +21,11 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<!--
|
||||
Lets LocationKeepAlive start with the phone locked, when a car connects by itself. Only
|
||||
asked for from Home while a car is connected, and only used while one is (issue #99).
|
||||
-->
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<!-- Keeps a wireless start going with the phone locked; see WirelessKeepAlive. -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.location.Geocoder
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import app.seb3thehacker.gearslip.GearslipLog
|
||||
import app.seb3thehacker.gearslip.SessionStatus
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.Locale
|
||||
@@ -66,8 +67,21 @@ object Weather {
|
||||
|
||||
private var placeCache: Pair<String, String>? = null // rounded "lat,lon" to place name
|
||||
|
||||
/** Set while the car preview is on the phone's screen, where the dashboard shows the weather too. */
|
||||
@Volatile var previewOpen = false
|
||||
|
||||
/**
|
||||
* Location is read only while a car is connected or the preview is open. Opening Gearslip on
|
||||
* the phone used to warm the forecast too, which read the location with no car anywhere.
|
||||
*/
|
||||
private val mayLocate: Boolean
|
||||
get() = previewOpen || SessionStatus.state.value.phase.let {
|
||||
it == SessionStatus.Phase.CONNECTING || it == SessionStatus.Phase.PROJECTING
|
||||
}
|
||||
|
||||
/** Runs off the main thread. Fresh data younger than [TTL_MS] is kept unless [force]. */
|
||||
fun refresh(context: Context, force: Boolean = false) {
|
||||
if (!mayLocate) return
|
||||
val current = flow.value
|
||||
if (!force && current is WeatherState.Ready && System.currentTimeMillis() - current.data.fetchedAt < TTL_MS) return
|
||||
val app = context.applicationContext
|
||||
|
||||
@@ -46,7 +46,8 @@ class LocationKeepAlive : Service() {
|
||||
}.onSuccess {
|
||||
settle(State.ON)
|
||||
}.onFailure {
|
||||
GearslipLog.w("location keep-alive: could not start the foreground service - ${it.message}")
|
||||
val background = if (allowedInBackground(this)) "allowed all the time" else "not allowed all the time"
|
||||
GearslipLog.w("location keep-alive: could not start the foreground service ($background) - ${it.message}")
|
||||
settle(State.FAILED)
|
||||
stopSelf()
|
||||
}
|
||||
@@ -101,6 +102,21 @@ class LocationKeepAlive : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the service again if a car still holds it but it was refused, as it is when a car
|
||||
* connects with the phone locked and "Allow all the time" isn't granted. Called once the
|
||||
* driver grants it, so the map gets its fix without reconnecting.
|
||||
*/
|
||||
fun retry(context: Context) {
|
||||
main.post {
|
||||
if (holders > 0 && state == State.FAILED) begin(context.applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether Android lets this service start with the phone locked (issue #99). */
|
||||
fun allowedInBackground(context: Context) =
|
||||
context.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
/** Lets go of one hold; the last one stops the service. */
|
||||
fun release(context: Context) {
|
||||
main.post {
|
||||
|
||||
@@ -72,6 +72,11 @@ fun CarPreviewScreen(onBack: () -> Unit) {
|
||||
// The preview is something to watch, like a map on a dash mount, so the phone stays awake on
|
||||
// it. Only here: the flag goes with this screen, and every other screen sleeps as usual.
|
||||
val view = androidx.compose.ui.platform.LocalView.current
|
||||
// Set during composition, before the car UI below warms the forecast.
|
||||
remember { app.seb3thehacker.gearslip.car.Weather.previewOpen = true }
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { app.seb3thehacker.gearslip.car.Weather.previewOpen = false }
|
||||
}
|
||||
DisposableEffect(view) {
|
||||
view.keepScreenOn = true
|
||||
onDispose { view.keepScreenOn = false }
|
||||
|
||||
@@ -112,6 +112,7 @@ fun HomeScreen(
|
||||
Spacer(Modifier.height(16.dp))
|
||||
AudioSwitch()
|
||||
if (live) BluetoothMissingCard()
|
||||
if (live) BackgroundLocationCard()
|
||||
BluetoothAddressCard()
|
||||
VoiceDownloadCard()
|
||||
// The first drive that worked asks once to share it, right under the result.
|
||||
@@ -239,6 +240,60 @@ private fun BluetoothMissingCard() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks for "Allow all the time" location, shown only while a car is connected (issue #99).
|
||||
* Without it, a car that connects with the phone locked leaves the car's map without GPS
|
||||
* until Gearslip is opened. Once granted, the location service starts at once.
|
||||
*/
|
||||
@Composable
|
||||
private fun BackgroundLocationCard() {
|
||||
val context = LocalContext.current
|
||||
var allowed by remember { mutableStateOf(app.seb3thehacker.gearslip.host.LocationKeepAlive.allowedInBackground(context)) }
|
||||
var dismissed by remember { mutableStateOf(false) }
|
||||
if (allowed || dismissed) return
|
||||
fun recheck() {
|
||||
allowed = app.seb3thehacker.gearslip.host.LocationKeepAlive.allowedInBackground(context)
|
||||
if (allowed) app.seb3thehacker.gearslip.host.LocationKeepAlive.retry(context)
|
||||
}
|
||||
// On Android 11 and later this opens the app's location settings, where "Allow all the time" is.
|
||||
val askBackground = androidx.activity.compose.rememberLauncherForActivityResult(
|
||||
androidx.activity.result.contract.ActivityResultContracts.RequestPermission(),
|
||||
) { recheck() }
|
||||
// Android only offers all the time once the ordinary location permission is granted.
|
||||
val askForeground = androidx.activity.compose.rememberLauncherForActivityResult(
|
||||
androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions(),
|
||||
) { result ->
|
||||
if (result[android.Manifest.permission.ACCESS_FINE_LOCATION] == true) {
|
||||
askBackground.launch(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
}
|
||||
}
|
||||
val dark = gearslipDark()
|
||||
val container = if (dark) Color(0xFF4A3B00) else Color(0xFFFFE08A)
|
||||
val content = if (dark) Color(0xFFFFE08A) else Color(0xFF3A2E00)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Surface(color = container, contentColor = content, shape = MaterialTheme.shapes.extraLarge) {
|
||||
Column(Modifier.fillMaxWidth().padding(20.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(stringResource(R.string.home_location_title), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
|
||||
Text(stringResource(R.string.home_location_body), style = MaterialTheme.typography.bodyMedium)
|
||||
Row(Modifier.padding(top = 4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(containerColor = content, contentColor = container),
|
||||
onClick = {
|
||||
val fine = context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) ==
|
||||
android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
if (fine) askBackground.launch(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
else askForeground.launch(arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_COARSE_LOCATION))
|
||||
},
|
||||
) { Text(stringResource(R.string.home_location_allow)) }
|
||||
androidx.compose.material3.TextButton(
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = content),
|
||||
onClick = { dismissed = true },
|
||||
) { Text(stringResource(R.string.home_location_not_now)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** While the voice models download, how far along they are; nothing otherwise. */
|
||||
@Composable
|
||||
private fun VoiceDownloadCard() {
|
||||
|
||||
@@ -438,6 +438,10 @@
|
||||
<!-- -->
|
||||
<string name="home_debug_build">%1$s · debug build</string>
|
||||
<string name="home_bt_body">Music plays over Bluetooth, but your phone isn\'t connected to the car. Connect it here or from the car\'s Bluetooth menu.</string>
|
||||
<string name="home_location_title">Keep the car\'s map on GPS</string>
|
||||
<string name="home_location_body">With the phone locked, Android stops passing your location to the car\'s map. Choose Allow all the time, and Gearslip keeps it going. It reads your location in the background only while a car is connected.</string>
|
||||
<string name="home_location_allow">Allow location</string>
|
||||
<string name="home_location_not_now">Not now</string>
|
||||
<string name="home_voice_downloading">Downloading voice, %1$d%%</string>
|
||||
|
||||
<!-- Car audio dialogs -->
|
||||
|
||||
Reference in new issue
Block a user