Stop the weather worker pinning location for providers that manage it
Some checks failed
Trigger F-Droid repository rebuild / trigger (release) Has been cancelled

Breezy Weather resolves its own location and pushes weather updates, but
WeatherUpdateWorker only looked at autoLocation before requesting a location.
BreezyWeatherProvider.getWeatherData() is a noop that always returns null, so
the worker then returned Result.retry() forever and never set lastUpdate -
which meant the update interval check could never short-circuit it either.

Every attempt called getLastKnownLocation(), which registers GPS *and* network
listeners at a one second interval and held them for up to ten minutes, so the
location stack never idled. On the phone this showed up as ~46k delivered fixes,
`*location*` wakelocks held ~99% of the time and Doze never engaging at all.

- expose the selected provider's managedLocation in WeatherSettingsData
- skip and cancel the weather work for providers that manage their own location
- give up on a fix after two minutes instead of ten
- LocationsRepository: don't collect the location flow when location search is
  off or not permitted. As a combineTransform argument it was always collected,
  so every search query registered GPS and network listeners for up to 30s.

Bump to 1.40.2-typing.8 (versionCode 2026091201).
This commit is contained in:
2026-09-12 15:19:10 +02:00
parent 45ac29e8a7
commit 61f3424de5
4 changed files with 99 additions and 45 deletions

View File

@@ -34,8 +34,8 @@ android {
applicationId = "de.mm20.launcher2"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091006
versionName = "1.40.2-typing.7"
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091201
versionName = "1.40.2-typing.8"
signingConfig = signingConfigs.getByName("debug")
}

View File

@@ -38,6 +38,13 @@ data class WeatherSettingsData(
val lastLocation: LatLon? = null,
val lastUpdate: Long = 0L,
val providerSettings: Map<String, ProviderSettings> = emptyMap(),
/**
* Whether the selected provider resolves its own location (e.g. Breezy Weather, which is
* configured with a [WeatherLocation.Managed]). The launcher must then not request a location
* for it.
*/
val managedLocation: Boolean = false,
)
class WeatherSettings internal constructor(
@@ -53,6 +60,7 @@ class WeatherSettings internal constructor(
lastLocation = it.weatherLastLocation,
lastUpdate = it.weatherLastUpdate,
providerSettings = it.weatherProviderSettings,
managedLocation = it.weatherProviderSettings[it.weatherProvider]?.managedLocation == true,
)
}.distinctUntilChanged()
) {

View File

@@ -12,11 +12,15 @@ import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.timeout
@@ -32,7 +36,7 @@ internal class LocationsRepository(
private val permissionsManager: PermissionsManager,
) : SearchableRepository<Location> {
@OptIn(FlowPreview::class)
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
override fun search(
query: String,
allowNetwork: Boolean
@@ -40,50 +44,71 @@ internal class LocationsRepository(
if (query.isBlank() || query.length <= 1) {
return flowOf(persistentListOf())
}
return combineTransform(
poseProvider
.getLocation(minTimeMs = 2000, minDistanceM = 50.0f)
// 1st location: lastCachedLocation of poseProvider, if available
// 2nd location: LocationManager.getLastKnownLocation(), if available and better than lastCachedLocation
// 3rd location: live location from LocationManager.requestLocationUpdates() that is better than any of the previous
.take(3)
// only request locations for 30 seconds
.timeout(30.seconds),
return combine(
permissionsManager.hasPermission(PermissionGroup.Location),
settings.data
) { userLocation, hasPermission, settingsData ->
emit(persistentListOf())
if (!hasPermission || settingsData.providers.isEmpty()) {
return@combineTransform
}
val providers = settingsData.providers.map {
when (it) {
"openstreetmaps" -> OsmLocationProvider(context, settings)
else -> PluginLocationProvider(context, it)
}
}
supervisorScope {
val result = MutableStateFlow(persistentListOf<Location>())
for (provider in providers) {
launch {
val r = provider.search(
query,
userLocation,
allowNetwork,
settingsData.searchRadius,
settingsData.hideUncategorized
)
result.update {
(it + r).toPersistentList()
}
}
}
emitAll(result)
) { hasPermission, settingsData ->
!hasPermission || settingsData.providers.isEmpty()
}.distinctUntilChanged().flatMapLatest { unusable ->
if (unusable) {
// Bail out before building the location flow below. As an argument to
// combineTransform it is always collected, which registered GPS and network
// listeners for every search query even when location search was off or not
// permitted.
flowOf(persistentListOf())
} else {
locationSearchFlow(query, allowNetwork)
}
}
}
@OptIn(FlowPreview::class)
private fun locationSearchFlow(
query: String,
allowNetwork: Boolean,
): Flow<ImmutableList<Location>> = combineTransform(
poseProvider
.getLocation(minTimeMs = 2000, minDistanceM = 50.0f)
// 1st location: lastCachedLocation of poseProvider, if available
// 2nd location: LocationManager.getLastKnownLocation(), if available and better than lastCachedLocation
// 3rd location: live location from LocationManager.requestLocationUpdates() that is better than any of the previous
.take(3)
// only request locations for 30 seconds
.timeout(30.seconds),
permissionsManager.hasPermission(PermissionGroup.Location),
settings.data
) { userLocation, hasPermission, settingsData ->
emit(persistentListOf())
if (!hasPermission || settingsData.providers.isEmpty()) {
return@combineTransform
}
val providers = settingsData.providers.map {
when (it) {
"openstreetmaps" -> OsmLocationProvider(context, settings)
else -> PluginLocationProvider(context, it)
}
}
supervisorScope {
val result = MutableStateFlow(persistentListOf<Location>())
for (provider in providers) {
launch {
val r = provider.search(
query,
userLocation,
allowNetwork,
settingsData.searchRadius,
settingsData.hideUncategorized
)
result.update {
(it + r).toPersistentList()
}
}
}
emitAll(result)
}
}
}

View File

@@ -79,6 +79,12 @@ internal class WeatherRepositoryImpl(
}
scope.launch {
settings.collectLatest {
if (it.managedLocation) {
// The provider resolves its own location and pushes weather updates, so there
// is nothing to poll for.
WorkManager.getInstance(context).cancelUniqueWork("weather")
return@collectLatest
}
val provider = WeatherProvider.getInstance(it.provider)
val weatherRequest =
PeriodicWorkRequestBuilder<WeatherUpdateWorker>(Duration.ofMillis(provider.getUpdateInterval()))
@@ -212,6 +218,18 @@ class WeatherUpdateWorker(
override suspend fun doWork(): Result {
Log.d("WeatherUpdateWorker", "Requesting weather data")
val settingsData = settings.first()
// A provider with a managed location resolves its position itself and pushes weather
// updates instead of being polled (Breezy Weather does both). There is nothing to pull,
// and its getWeatherData() always returns null - which used to make this worker request a
// location (holding GPS and network listeners for up to ten minutes at a time) and then
// return Result.retry() forever, without ever setting lastUpdate, so the update interval
// check below could never short-circuit it either.
if (settingsData.managedLocation) {
Log.d("WeatherUpdateWorker", "Provider manages its own location, nothing to pull")
return Result.success()
}
val provider = WeatherProvider.getInstance(settingsData.provider)
val updateInterval = provider.getUpdateInterval()
@@ -254,7 +272,10 @@ class WeatherUpdateWorker(
@OptIn(FlowPreview::class)
private suspend fun getLastKnownLocation(): LatLon? = locationProvider.getLocation(skipCache = true)
.timeout(10.minutes)
// Weather does not need a metre-accurate fix; giving up after two minutes instead of ten
// keeps the GPS and network listeners (and the wakelocks they hold) short. The caller
// falls back to the last known location.
.timeout(2.minutes)
.firstOrNull()
.or { locationProvider.lastCachedLocation }
?.let { LatLon(it.latitude, it.longitude) }