Compare commits
10 Commits
6aba977823
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c7d3a64c34 | |||
| c824b100b6 | |||
| c56ea58fc5 | |||
| b0072f0fc2 | |||
| 61f3424de5 | |||
| 45ac29e8a7 | |||
| 291752abcd | |||
| 4836618609 | |||
| c8109d9197 | |||
| 3b3d6473ca |
36
CLAUDE.md
36
CLAUDE.md
@@ -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.
|
||||
|
||||
@@ -87,7 +98,7 @@ state in `ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt`. New strings a
|
||||
|
||||
On the desktop:
|
||||
|
||||
JAVA_HOME=~/jdk21 ANDROID_HOME=~/android-sdk \
|
||||
JAVA_HOME=/usr/lib/jvm/java-21-openjdk ANDROID_HOME=~/Android/Sdk \
|
||||
./gradlew :app:app:assembleDefaultRelease
|
||||
|
||||
Result: `app/app/build/outputs/apk/default/release/app-default-release.apk`, applicationId
|
||||
@@ -99,6 +110,21 @@ in the repository root (git-ignored) or the `KEYSTORE_FILE`, `KEYSTORE_PASSWORD`
|
||||
`~/android-keystores/kvaesitso-release.jks`; the phone has its own copy under
|
||||
`~/android-keystores/` in the Termux home.
|
||||
|
||||
The desktop's checkout (jonas@192.168.1.86) is `~/sources/Kvaesitso`. Its SDK is `~/Android/Sdk`
|
||||
(platforms android-35/37.0, build-tools 34/36/37), so `local.properties` there has
|
||||
`sdk.dir=/home/jonas/Android/Sdk`, and `keystore.properties` points at the copy of the release key
|
||||
in `~/android-keystores/`. It has no `~/jdk21`; it uses Arch's `java-21-openjdk`.
|
||||
|
||||
After the APK is built (on either machine), the release is tagged and published from the phone:
|
||||
|
||||
git tag -a v1.40.2-typing.N -m "Kvaesitso 1.40.2 type-to-search and agenda, Nth build"
|
||||
git push origin main v1.40.2-typing.N
|
||||
|
||||
and a Gitea release for that tag is created with the APK attached as
|
||||
`Kvaesitso-v<versionName>-Signed.apk` (the token is in `~/.config/gitea/token`; the API is
|
||||
`POST /api/v1/repos/jonas/Kvaesitso/releases` and then `.../releases/<id>/assets?name=<asset>` with
|
||||
`-F attachment=@<apk>`).
|
||||
|
||||
### Building on the phone itself (Termux)
|
||||
|
||||
The phone builds this project without a desktop. `scripts/ondevice.sh` wraps the loop:
|
||||
|
||||
@@ -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() ?: 2026091005
|
||||
versionName = "1.40.2-typing.6"
|
||||
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091701
|
||||
versionName = "1.40.2-typing.10"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
|
||||
|
||||
@@ -709,6 +709,14 @@ fun ConfigureClockWidgetSheet(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (smartspacer == false) {
|
||||
AnimatedVisibility(visible = parts?.calendar == true) {
|
||||
ConfigureCalendarPart(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,33 +17,47 @@ import de.mm20.launcher2.ui.launcher.widgets.clock.parts.MusicPartProvider
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.PartProvider
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.SmartspacerPartProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class ClockWidgetVM : ViewModel(), KoinComponent {
|
||||
private val settings: ClockWidgetSettings by inject()
|
||||
|
||||
private val partProviders = settings.parts.combine(settings.useSmartspacer) { p, s ->
|
||||
p to s
|
||||
}.map { (parts, smartspacer) ->
|
||||
if (smartspacer && isAtLeastApiLevel(29)) {
|
||||
return@map listOf(SmartspacerPartProvider())
|
||||
}
|
||||
/**
|
||||
* Kept for the lifetime of the ViewModel instead of being rebuilt whenever the clock widget
|
||||
* is subscribed again - which happens on every return to the home screen, because the
|
||||
* launcher stops while it is in the background. Providers cache the data behind their
|
||||
* ranking, so a fresh instance starts at ranking 0 and makes its part disappear until its
|
||||
* query returns; the agenda part would blink on every visit that way.
|
||||
*/
|
||||
private val partProviders = MutableStateFlow<List<PartProvider>>(emptyList())
|
||||
|
||||
val providers = mutableListOf<PartProvider>()
|
||||
if (parts.date) providers += DatePartProvider()
|
||||
if (parts.calendar) providers += CalendarPartProvider()
|
||||
if (parts.music) providers += MusicPartProvider()
|
||||
providers += BatteryPartProvider(parts.battery)
|
||||
if (parts.alarm) providers += AlarmPartProvider()
|
||||
providers
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
settings.parts.combine(settings.useSmartspacer) { p, s -> p to s }
|
||||
.collect { (parts, smartspacer) ->
|
||||
partProviders.value = if (smartspacer && isAtLeastApiLevel(29)) {
|
||||
listOf(SmartspacerPartProvider())
|
||||
} else {
|
||||
buildList {
|
||||
if (parts.date) add(DatePartProvider())
|
||||
if (parts.calendar) add(CalendarPartProvider())
|
||||
if (parts.music) add(MusicPartProvider())
|
||||
add(BatteryPartProvider(parts.battery))
|
||||
if (parts.alarm) add(AlarmPartProvider())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getActiveParts(context: Context): Flow<List<PartProvider>> = channelFlow {
|
||||
partProviders.collectLatest { providers ->
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -122,6 +136,10 @@ class CalendarPartProvider : PartProvider, KoinComponent {
|
||||
from = now,
|
||||
to = endOfDay(now),
|
||||
excludeCalendars = excludedCalendars.toList(),
|
||||
// No debounce: the ranking stays 0 until the first result arrives, and
|
||||
// the query is only re-run on a calendar change or every 15 minutes, so
|
||||
// holding the result back would only delay the part's appearance.
|
||||
debounceMillis = 0,
|
||||
).first()
|
||||
.filter { !it.isTask }
|
||||
.sortedBy { it.startTime ?: it.endTime }
|
||||
@@ -172,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(
|
||||
@@ -212,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.
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
) {
|
||||
|
||||
@@ -22,20 +22,28 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
interface CalendarRepository : SearchableRepository<CalendarEvent> {
|
||||
/**
|
||||
* @param debounceMillis how long each result is held back, so bursts of upstream changes
|
||||
* (plugin or permission updates) cause a single query. [debounce] delays the first result
|
||||
* too, so callers that must show something immediately pass 0.
|
||||
*/
|
||||
fun findMany(
|
||||
from: Long = System.currentTimeMillis(),
|
||||
to: Long = from + 14 * 24 * 60 * 60 * 1000L,
|
||||
excludeCalendars: List<String> = emptyList(),
|
||||
excludeAllDayEvents: Boolean = false,
|
||||
debounceMillis: Long = 500,
|
||||
): Flow<ImmutableList<CalendarEvent>>
|
||||
|
||||
fun getCalendars(providerId: String? = null): Flow<List<CalendarList>>
|
||||
@@ -94,6 +102,7 @@ internal class CalendarRepositoryImpl(
|
||||
to: Long,
|
||||
excludeCalendars: List<String>,
|
||||
excludeAllDayEvents: Boolean,
|
||||
debounceMillis: Long,
|
||||
): Flow<ImmutableList<CalendarEvent>> {
|
||||
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
val hasTasksPermission = permissionsManager.hasPermission(PermissionGroup.Tasks)
|
||||
@@ -112,17 +121,17 @@ internal class CalendarRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
emitAll(
|
||||
queryCalendarEvents(
|
||||
query = null,
|
||||
intervalStart = from,
|
||||
intervalEnd = to,
|
||||
excludeAllDayEvents = excludeAllDayEvents,
|
||||
excludeCalendars = excludeCalendars,
|
||||
providers = providers,
|
||||
allowNetwork = false,
|
||||
).debounce(500)
|
||||
val events = queryCalendarEvents(
|
||||
query = null,
|
||||
intervalStart = from,
|
||||
intervalEnd = to,
|
||||
excludeAllDayEvents = excludeAllDayEvents,
|
||||
excludeCalendars = excludeCalendars,
|
||||
providers = providers,
|
||||
awaitAllProviders = true,
|
||||
allowNetwork = false,
|
||||
)
|
||||
emitAll(if (debounceMillis > 0L) events.debounce(debounceMillis) else events)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,12 +142,17 @@ internal class CalendarRepositoryImpl(
|
||||
excludeAllDayEvents: Boolean = false,
|
||||
excludeCalendars: List<String> = emptyList(),
|
||||
allowNetwork: Boolean = false,
|
||||
awaitAllProviders: Boolean = false,
|
||||
providers: List<CalendarProvider>,
|
||||
): Flow<ImmutableList<CalendarEvent>> = flow {
|
||||
supervisorScope {
|
||||
val result = MutableStateFlow(persistentListOf<CalendarEvent>())
|
||||
// Null until the first provider has reported. Emitting the (empty) accumulator right
|
||||
// away would hand collectors an empty list before any query had run, which callers
|
||||
// that take the first result - like the agenda in the clock widget - cannot tell
|
||||
// apart from "no events".
|
||||
val result = MutableStateFlow<ImmutableList<CalendarEvent>?>(null)
|
||||
|
||||
for (provider in providers) {
|
||||
val jobs = providers.map { provider ->
|
||||
launch {
|
||||
val r = provider.search(
|
||||
query,
|
||||
@@ -152,11 +166,19 @@ internal class CalendarRepositoryImpl(
|
||||
allowNetwork = allowNetwork,
|
||||
)
|
||||
result.update {
|
||||
(it + r).toPersistentList()
|
||||
((it ?: persistentListOf()) + r).toPersistentList()
|
||||
}
|
||||
}
|
||||
}
|
||||
emitAll(result)
|
||||
|
||||
if (awaitAllProviders) {
|
||||
// One-shot callers must not receive a partial list, so wait until every
|
||||
// provider has reported; if there is none, report an empty result.
|
||||
jobs.joinAll()
|
||||
emit(result.value ?: persistentListOf())
|
||||
} else {
|
||||
emitAll(result.filterNotNull())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -79,7 +79,12 @@ internal class WeatherRepositoryImpl(
|
||||
}
|
||||
scope.launch {
|
||||
settings.collectLatest {
|
||||
val provider = WeatherProvider.getInstance(it.provider)
|
||||
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 weatherRequest =
|
||||
PeriodicWorkRequestBuilder<WeatherUpdateWorker>(Duration.ofMillis(provider.getUpdateInterval()))
|
||||
.build()
|
||||
@@ -214,6 +219,17 @@ class WeatherUpdateWorker(
|
||||
val settingsData = settings.first()
|
||||
val provider = WeatherProvider.getInstance(settingsData.provider)
|
||||
|
||||
// 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 updateInterval = provider.getUpdateInterval()
|
||||
val lastUpdate = settingsData.lastUpdate
|
||||
|
||||
@@ -222,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) {
|
||||
@@ -254,7 +275,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) }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user