4 Commits

Author SHA1 Message Date
c7d3a64c34 Bump version to 1.40.2-typing.10
Some checks are pending
Trigger F-Droid repository rebuild / trigger (release) Waiting to run
2026-09-17 08:56:57 +02:00
c824b100b6 Make the clock agenda's calendars and row count configurable
The agenda part filtered calendars through the search settings and always
collapsed after four rows. It now has its own selection: an exclusion set
(`clockWidgetCalendarPartExcludedCalendars`, empty = all) that is added to the
search settings' exclusions, because the picker only offers calendars those
settings leave enabled.

The row cap is `clockWidgetCalendarPartMaxRows` (1..8, default 4); the agenda
shows that many events and then a "+N more events" row if more are left, so
with the default five events now render as four rows plus "+1 more event"
instead of three plus "+2".

Both are configured in `ConfigureCalendarPart`, shown by the clock sheet while
the Events part is enabled: a slider, a Calendars row that expands into a
colour-tinted checkbox per calendar, and a button to Settings -> Search ->
Calendar (new `ROUTE_SEARCH_CALENDARS`) for calendars the search settings
disable. A missing calendar permission shows a banner with a Grant button.

New setting fields default to the previous behaviour, so no datastore
migration is needed.
2026-09-17 08:56:55 +02:00
c56ea58fc5 Use || instead of comma conditions in the subjectless when
Some checks failed
Trigger F-Droid repository rebuild / trigger (release) Has been cancelled
Build Nightly APK / build (push) Has been cancelled
2026-09-12 15:44:06 +02:00
b0072f0fc2 Key the weather worker's location skip on the provider, not on a setting
typing.8 checked WeatherSettingsData.managedLocation, but that only exists once the
user has explicitly picked "managed location" for the provider, so
weatherProviderSettings stayed empty and the guard never fired: the worker kept
requesting a location and retrying every 30s.

The real signal is the provider's own capability. BreezyWeatherProvider.getWeatherData()
is a hardcoded noop that also manages its own location, so add WeatherProvider.isPushBased
and check that instead.

Also stop treating a managed location as "pull nothing": those providers still have data
to fetch, they just resolve the position themselves, so they now take the
WeatherLocation path instead of the auto-location one.

Bump to 1.40.2-typing.9 (versionCode 2026091202).
2026-09-12 15:43:10 +02:00
13 changed files with 364 additions and 38 deletions

View File

@@ -59,12 +59,21 @@ changes on top of that:
* **A new part shows today's agenda.** `CalendarPartProvider`
(`app/ui/.../widgets/clock/parts/`) is toggled with the *Events* switch
(`clockWidgetCalendarPart`). It lists the events that are still running or upcoming today, plus
today's all-day events, capped at 4 rows — if there are more, the last row is a "+N more events"
hint that opens the calendar app. Tapping a row opens the event
today's all-day events, capped at `clockWidgetCalendarPartMaxRows` rows (1..8, default 4) — if
there are more, the rows are followed by a "+N more events" hint that opens the calendar app.
Tapping a row opens the event
(`CalendarEvent.launch`, i.e. `ACTION_VIEW` on `content://com.android.calendar/events/<id>`,
which KashCal handles). Rows reuse `CalendarEvent.getSummary()` from
`ui/launcher/search/calendar/CalendarItem.kt` (made `internal` for this), so an all-day event
reads "All day" and a timed one reads e.g. "09:00 09:30".
* **The agenda picks its calendars.** `clockWidgetCalendarPartExcludedCalendars` (empty = all)
hides individual calendars; the query's exclusion set is that set plus the search settings'
exclusions, because the picker only ever offers calendars the search settings leave enabled
(`ClockWidgetSettingsScreenVM.calendars` filters them out of `CalendarRepository.getCalendars()`,
and a "Manage calendars" button opens `Settings → Search → Calendar` via the
`ROUTE_SEARCH_CALENDARS` route in `SettingsActivity`). The whole calendar configuration lives in
`ConfigureCalendarPart.kt`, shown by `ConfigureClockWidgetSheet` while the Events part is on —
the agenda deliberately ignores the calendar *widget*'s own `excludedCalendarIds`.
Rankings, in the order the zone prefers them: `CalendarPartProvider` 70 when an event is running or
starts within 30 minutes, else 30 while there are events left today, else 0 (so the date part keeps
@@ -78,8 +87,10 @@ launcher only re-query when they become visible). `PartProvider.setTime()` is ca
by the clock widget; the provider folds that into its ranking instead of re-querying.
The clock settings themselves are in `core/preferences`: `LauncherSettingsData` (new fields
`clockWidgetCalendarPart`, `clockWidgetDynamicZoneSlots`), `ClockWidgetParts` and the
`ClockWidgetSettings` accessors in `core/preferences/.../ui/ClockWidgetSettings.kt`, and the UI
`clockWidgetCalendarPart`, `clockWidgetCalendarPartExcludedCalendars`,
`clockWidgetCalendarPartMaxRows`, `clockWidgetDynamicZoneSlots`), `ClockWidgetParts` and the
`ClockWidgetSettings` accessors in `core/preferences/.../ui/ClockWidgetSettings.kt` (including the
`MinCalendarPartRows`/`MaxCalendarPartRows` bounds), and the UI
state in `ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt`. New strings are English-only in
`core/i18n/src/main/res/values/strings.xml`; translations come from Crowdin upstream.

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() ?: 2026091201
versionName = "1.40.2-typing.8"
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091701
versionName = "1.40.2-typing.10"
signingConfig = signingConfigs.getByName("debug")
}

View File

@@ -709,6 +709,14 @@ fun ConfigureClockWidgetSheet(
}
}
}
if (smartspacer == false) {
AnimatedVisibility(visible = parts?.calendar == true) {
ConfigureCalendarPart(
modifier = Modifier.padding(top = 16.dp),
viewModel = viewModel,
)
}
}
}
}
}

View File

@@ -0,0 +1,181 @@
package de.mm20.launcher2.ui.launcher.widgets.clock
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.ui.MaxCalendarPartRows
import de.mm20.launcher2.preferences.ui.MinCalendarPartRows
import de.mm20.launcher2.themes.colors.atTone
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.CheckboxPreference
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.SliderPreference
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.settings.SettingsActivity
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreenVM
import org.koin.compose.koinInject
/**
* Settings for the agenda in the clock widget's dynamic zone: how many events are shown
* before they collapse into a "+N more events" row, and which calendars they come from.
*/
@Composable
internal fun ConfigureCalendarPart(
viewModel: ClockWidgetSettingsScreenVM,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val activity = LocalLifecycleOwner.current as? AppCompatActivity
val permissionsManager: PermissionsManager = koinInject()
val calendars by viewModel.calendars.collectAsStateWithLifecycle()
val excludedCalendars by viewModel.calendarPartExcludedCalendars.collectAsStateWithLifecycle()
val maxRows by viewModel.calendarPartMaxRows.collectAsStateWithLifecycle()
val hasPermission by viewModel.hasCalendarPermission.collectAsStateWithLifecycle()
var showCalendars by remember { mutableStateOf(false) }
val shownCalendars = calendars.count { it.id !in excludedCalendars }
Column(modifier = modifier) {
if (!hasPermission) {
MissingPermissionBanner(
modifier = Modifier.padding(bottom = 8.dp),
text = stringResource(R.string.missing_permission_calendar_widget_settings),
onClick = {
activity?.let {
permissionsManager.requestPermission(it, PermissionGroup.Calendar)
}
},
)
}
OutlinedCard(
modifier = Modifier.fillMaxWidth(),
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
SliderPreference(
title = stringResource(R.string.preference_clockwidget_calendar_part_events),
icon = R.drawable.event_24px,
value = maxRows,
min = MinCalendarPartRows,
max = MaxCalendarPartRows,
step = 1,
onValueChanged = {
viewModel.setCalendarPartMaxRows(it)
}
)
HorizontalDivider()
Preference(
title = stringResource(R.string.preference_clockwidget_calendar_part_calendars),
summary = when {
calendars.isEmpty() -> stringResource(R.string.preference_clockwidget_calendar_part_no_calendars)
shownCalendars == calendars.size -> stringResource(R.string.preference_clockwidget_calendar_part_all_calendars)
else -> stringResource(
R.string.preference_clockwidget_calendar_part_calendars_summary,
shownCalendars,
calendars.size,
)
},
icon = R.drawable.calendar_today_24px,
onClick = { showCalendars = !showCalendars },
enabled = calendars.isNotEmpty(),
controls = {
Icon(
painter = painterResource(
if (showCalendars) R.drawable.keyboard_arrow_up_24px
else R.drawable.keyboard_arrow_down_24px
),
contentDescription = null,
)
},
)
AnimatedVisibility(
visible = showCalendars && calendars.isNotEmpty(),
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
HorizontalDivider()
for (calendar in calendars) {
CheckboxPreference(
title = calendar.name,
summary = calendar.owner,
iconPadding = false,
value = calendar.id !in excludedCalendars,
onValueChanged = {
viewModel.setCalendarExcluded(calendar.id, !it)
},
checkboxColors = CheckboxDefaults.colors(
checkedColor = if (calendar.color == 0) MaterialTheme.colorScheme.primary
else Color(
calendar.color.atTone(if (LocalDarkTheme.current) 80 else 40)
),
checkmarkColor = if (calendar.color == 0) MaterialTheme.colorScheme.onPrimary
else Color(
calendar.color.atTone(if (LocalDarkTheme.current) 20 else 100)
)
)
)
}
}
}
}
}
TextButton(
modifier = Modifier
.padding(top = 8.dp)
.align(Alignment.CenterHorizontally),
contentPadding = ButtonDefaults.TextButtonWithIconContentPadding,
onClick = {
context.startActivity(
Intent(context, SettingsActivity::class.java).apply {
putExtra(
SettingsActivity.EXTRA_ROUTE,
SettingsActivity.ROUTE_SEARCH_CALENDARS,
)
}
)
},
) {
Text(stringResource(R.string.preference_clockwidget_calendar_part_manage_calendars))
Icon(
modifier = Modifier
.padding(start = ButtonDefaults.IconSpacing)
.requiredSize(ButtonDefaults.IconSize),
painter = painterResource(R.drawable.open_in_new_20px),
contentDescription = null,
)
}
}
}

View File

@@ -39,6 +39,7 @@ import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.TimeFormat
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.locals.LocalTimeFormat
@@ -65,10 +66,10 @@ import java.time.OffsetDateTime
import java.util.Date
/**
* Highest number of rows the agenda shows. If there are more events than this, the last
* row is a "+N more events" hint.
* Number of rows the agenda shows before it collapses into a "+N more events" row, until the
* setting has been read.
*/
private const val MaxRows = 4
private const val DefaultMaxRows = 4
/** Re-query interval, so the agenda survives calendar changes that don't notify. */
private const val RefreshInterval = 15 * 60 * 1000L
@@ -96,13 +97,17 @@ class CalendarPartProvider : PartProvider, KoinComponent {
private val calendarRepository: CalendarRepository by inject()
private val permissionsManager: PermissionsManager by inject()
private val searchSettings: CalendarSearchSettings by inject()
private val clockWidgetSettings: ClockWidgetSettings by inject()
private val agenda = MutableStateFlow<List<CalendarEvent>>(emptyList())
private val maxRows = MutableStateFlow(DefaultMaxRows)
override fun getRanking(context: Context): Flow<Int> = channelFlow {
// Load the agenda in the background; the ranking itself only depends on the
// already known agenda, so it can be emitted right away.
launch { observeAgenda(context) }
launch { clockWidgetSettings.calendarPartMaxRows.collect { maxRows.value = it } }
agenda.map { ranking(it) }
.distinctUntilChanged()
.collect { send(it) }
@@ -112,8 +117,17 @@ class CalendarPartProvider : PartProvider, KoinComponent {
combine(
dayChanges(context),
permissionsManager.hasPermission(PermissionGroup.Calendar),
clockWidgetSettings.calendarPartExcludedCalendars,
searchSettings.excludedCalendars,
) { now, hasPermission, excludedCalendars -> Triple(now, hasPermission, excludedCalendars) }
) { now, hasPermission, hiddenInWidget, excludedInSearch ->
AgendaInputs(
now = now,
hasCalendarPermission = hasPermission,
// Calendars the user disabled in the search settings cannot be picked in the
// widget either, so the two sets just add up.
excludedCalendars = if (hasPermission) hiddenInWidget + excludedInSearch else emptySet(),
)
}
.collectLatest { (now, hasPermission, excludedCalendars) ->
agenda.value = if (!hasPermission) {
emptyList()
@@ -176,13 +190,15 @@ class CalendarPartProvider : PartProvider, KoinComponent {
@Composable
override fun Component(compactLayout: Boolean) {
val events by agenda.collectAsState()
val maxRows by this.maxRows.collectAsState()
val context = LocalContext.current
if (events.isEmpty()) return
val timeFormat = LocalTimeFormat.current
val contentColor = LocalContentColor.current
val visibleCount = if (events.size > MaxRows) MaxRows - 1 else events.size
// The configured number of events, then a "+N more events" row if there are more.
val visibleCount = minOf(events.size, maxRows)
val hiddenCount = events.size - visibleCount
Column(
@@ -216,6 +232,13 @@ class CalendarPartProvider : PartProvider, KoinComponent {
}
}
/** The three inputs the agenda query depends on. */
private data class AgendaInputs(
val now: Long,
val hasCalendarPermission: Boolean,
val excludedCalendars: Set<String>,
)
/**
* The time of an event, or "all-day" for all-day events. Never a date.
*/

View File

@@ -422,6 +422,7 @@ class SettingsActivity : BaseActivity() {
ROUTE_MEDIA_INTEGRATION -> MediaIntegrationSettingsRoute
ROUTE_SEARCH_ACTIONS -> SearchActionsSettingsRoute
ROUTE_HIDDEN_ITEMS -> HiddenItemsSettingsRoute
ROUTE_SEARCH_CALENDARS -> CalendarSearchSettingsRoute
ROUTE_CRASH_REPORT if (intent.hasExtra(EXTRA_CRASH_REPORT_PATH)) -> {
CrashReportRoute(intent.getStringExtra(EXTRA_CRASH_REPORT_PATH)!!)
}
@@ -435,6 +436,7 @@ class SettingsActivity : BaseActivity() {
const val ROUTE_MEDIA_INTEGRATION = "settings/integrations/media"
const val ROUTE_SEARCH_ACTIONS = "settings/search/searchactions"
const val ROUTE_HIDDEN_ITEMS = "settings/search/hiddenitems"
const val ROUTE_SEARCH_CALENDARS = "settings/search/calendar"
const val ROUTE_CRASH_REPORT = "settings/debug/crashreport"
const val EXTRA_CRASH_REPORT_PATH = "crash_report_path"
}

View File

@@ -2,11 +2,16 @@ package de.mm20.launcher2.ui.settings.clockwidget
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.calendar.providers.CalendarList
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.BatteryStatusVisibility
import de.mm20.launcher2.preferences.ClockWidgetAlignment
import de.mm20.launcher2.preferences.ClockWidgetColors
import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.preferences.TimeFormat
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import de.mm20.launcher2.preferences.ui.UiSettings
import kotlinx.coroutines.flow.SharingStarted
@@ -19,6 +24,9 @@ import org.koin.core.component.inject
class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
private val settings: ClockWidgetSettings by inject()
private val uiSettings: UiSettings by inject()
private val calendarRepository: CalendarRepository by inject()
private val calendarSearchSettings: CalendarSearchSettings by inject()
private val permissionsManager: PermissionsManager by inject()
val compact = settings.compact
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
@@ -106,6 +114,40 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
settings.setCalendarPart(calendarPart)
}
/**
* Number of event rows in the agenda before it collapses into a "+N more events" row.
*/
val calendarPartMaxRows = settings.calendarPartMaxRows
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 4)
fun setCalendarPartMaxRows(rows: Int) {
settings.setCalendarPartMaxRows(rows)
}
val calendarPartExcludedCalendars = settings.calendarPartExcludedCalendars
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptySet<String>())
fun setCalendarExcluded(calendarId: String, excluded: Boolean) {
settings.setCalendarExcluded(calendarId, excluded)
}
/**
* Calendars the agenda can show, i.e. every calendar the search settings make available.
* Calendars disabled there are missing here on purpose.
*/
val calendars = combine(
calendarRepository.getCalendars(),
calendarSearchSettings.excludedCalendars,
) { calendars, excluded ->
calendars
.filter { it.id !in excluded }
.sortedWith(compareBy({ it.owner ?: "" }, { it.name }))
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList<CalendarList>())
/** Whether the agenda can show anything at all. */
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), true)
val slots = settings.slots
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)

View File

@@ -615,6 +615,12 @@
<string name="preference_clockwidget_alarm_part_summary">Show alarms that will ring within the next 24 hours</string>
<string name="preference_clockwidget_calendar_part">Events</string>
<string name="preference_clockwidget_calendar_part_summary">Show today\'s agenda</string>
<string name="preference_clockwidget_calendar_part_events">Visible events</string>
<string name="preference_clockwidget_calendar_part_calendars">Calendars</string>
<string name="preference_clockwidget_calendar_part_all_calendars">All calendars</string>
<string name="preference_clockwidget_calendar_part_calendars_summary">%1$d of %2$d calendars</string>
<string name="preference_clockwidget_calendar_part_no_calendars">No calendars found</string>
<string name="preference_clockwidget_calendar_part_manage_calendars">Manage calendars</string>
<string name="clockwidget_calendar_part_all_day">all-day</string>
<plurals name="clockwidget_calendar_part_more_events">
<item quantity="one">+%d more event</item>

View File

@@ -59,6 +59,8 @@ data class LauncherSettingsData internal constructor(
val clockWidgetMusicPart: Boolean = true,
val clockWidgetDatePart: Boolean = true,
val clockWidgetCalendarPart: Boolean = false,
val clockWidgetCalendarPartExcludedCalendars: Set<String> = emptySet(),
val clockWidgetCalendarPartMaxRows: Int = 4,
val clockWidgetDynamicZoneSlots: Int = 1,
val clockWidgetFillHeight: Boolean = false,
val clockWidgetAlignment: ClockWidgetAlignment = ClockWidgetAlignment.Bottom,

View File

@@ -11,6 +11,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/** Bounds for the number of event rows the agenda shows before collapsing. */
const val MinCalendarPartRows = 1
const val MaxCalendarPartRows = 8
data class ClockWidgetParts(
val date: Boolean,
val music: Boolean = false,
@@ -61,6 +65,39 @@ class ClockWidgetSettings internal constructor(
}
}
/**
* Calendars the agenda does not show. Empty means "all calendars".
*/
val calendarPartExcludedCalendars
get() = launcherDataStore.data.map { it.clockWidgetCalendarPartExcludedCalendars }
.distinctUntilChanged()
fun setCalendarExcluded(calendarId: String, excluded: Boolean) {
launcherDataStore.update {
it.copy(
clockWidgetCalendarPartExcludedCalendars =
if (excluded) it.clockWidgetCalendarPartExcludedCalendars + calendarId
else it.clockWidgetCalendarPartExcludedCalendars - calendarId
)
}
}
/**
* Number of event rows in the agenda before it collapses into a "+N more events" row.
*/
val calendarPartMaxRows
get() = launcherDataStore.data
.map { it.clockWidgetCalendarPartMaxRows.coerceIn(MinCalendarPartRows, MaxCalendarPartRows) }
.distinctUntilChanged()
fun setCalendarPartMaxRows(rows: Int) {
launcherDataStore.update {
it.copy(
clockWidgetCalendarPartMaxRows = rows.coerceIn(MinCalendarPartRows, MaxCalendarPartRows)
)
}
}
fun setDatePart(datePart: Boolean) {
launcherDataStore.update {
it.copy(clockWidgetDatePart = datePart)

View File

@@ -11,6 +11,14 @@ interface WeatherProvider {
return 1000 * 60 * 60L
}
/**
* Whether this provider pushes weather updates on its own instead of being polled, and
* resolves its location itself. [WeatherUpdateWorker] then has nothing to pull and must not
* request a location for it.
*/
val isPushBased: Boolean
get() = false
suspend fun getWeatherData(location: WeatherLocation): List<Forecast>?
suspend fun getWeatherData(lat: Double, lon: Double): List<Forecast>?
suspend fun findLocation(query: String): List<WeatherLocation>

View File

@@ -79,13 +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.
val provider = WeatherProvider.getInstance(it.provider)
if (provider.isPushBased) {
// The provider sends its updates itself, 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()))
.build()
@@ -218,20 +217,19 @@ class WeatherUpdateWorker(
override suspend fun doWork(): Result {
Log.d("WeatherUpdateWorker", "Requesting weather data")
val settingsData = settings.first()
val provider = WeatherProvider.getInstance(settingsData.provider)
// 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")
// A push-based provider sends its updates on its own and resolves its own location
// (Breezy Weather does both), so there is nothing to pull here. Its getWeatherData()
// always returns null, which used to make this worker request a location - registering
// GPS and network listeners and holding them for up to ten minutes - and then return
// Result.retry() forever, without ever setting lastUpdate, so the interval check below
// could never short-circuit it either.
if (provider.isPushBased) {
Log.d("WeatherUpdateWorker", "Provider pushes updates, nothing to pull")
return Result.success()
}
val provider = WeatherProvider.getInstance(settingsData.provider)
val updateInterval = provider.getUpdateInterval()
val lastUpdate = settingsData.lastUpdate
@@ -240,21 +238,26 @@ class WeatherUpdateWorker(
return Result.failure()
}
val weatherData = if (settingsData.autoLocation) {
val latLon = getLastKnownLocation() ?: settingsData.lastLocation
if (latLon == null) {
Log.e("WeatherUpdateWorker", "Could not get location")
return Result.failure()
val weatherData = when {
// A managed location means the provider resolves the position itself, so requesting
// one here would be wasted work that holds the location stack open.
settingsData.managedLocation || !settingsData.autoLocation -> {
val location = settings.location.first()
if (location == null) {
Log.e("WeatherUpdateWorker", "Location not set")
return Result.failure()
}
provider.getWeatherData(location)
}
settings.setLastLocation(latLon)
provider.getWeatherData(latLon.lat, latLon.lon)
} else {
val location = settings.location.first()
if (location == null) {
Log.e("WeatherUpdateWorker", "Location not set")
return Result.failure()
else -> {
val latLon = getLastKnownLocation() ?: settingsData.lastLocation
if (latLon == null) {
Log.e("WeatherUpdateWorker", "Could not get location")
return Result.failure()
}
settings.setLastLocation(latLon)
provider.getWeatherData(latLon.lat, latLon.lon)
}
provider.getWeatherData(location)
}
return if (weatherData == null) {

View File

@@ -20,6 +20,9 @@ class BreezyWeatherProvider(
) : WeatherProvider, KoinComponent {
private val database: AppDatabase by inject()
// Breezy sends its weather data to us (see pushWeatherData) and manages its own location.
override val isPushBased: Boolean = true
override suspend fun getWeatherData(location: WeatherLocation): List<Forecast>? {
// Noop implementation, because Breezy weather is handled in a special way
return null