Files
calendula/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt
Jean-Luc Makiola b3fe98907e
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 6s
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Has been skipped
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Has been skipped
Renovate / renovate (push) Successful in 2m21s
Release v2.19.4 (#259)
Release v2.19.4.

**Fixed**
- A deleted occurrence stays deleted through a series re-timing, and through backup/restore ([#225]).
- "Only this event" saves the edit on calendars that never synced, and on a row that doesn't recur it edits or deletes the event itself instead of filing an exception against nothing.
- A reminder left on the account's default is no longer read as a lead time of Calendula's own.
- The widgets turn the page at midnight; the re-arm backstop for the inexact rollover alarm is back, throttled to once per 15 minutes on calendar changes.
- An event running past midnight can be dragged by either half ([#253]).
- One event the provider refuses no longer costs the rest of the import, and an import cut short reports honestly how far it got.
- EXDATE zone prefixes are read in the bare `<zone>;` form the provider and DAVx5 actually write, not only the iCalendar `TZID=` spelling; a list holding a stamp we can't parse is kept whole rather than re-emitted stripped of its zone; RDATE travels with RRULE when the recurrence set is rewritten.

**Added**
- Belarusian, Hungarian and Slovak, from community translators on Weblate.

**Changed**
- Arabic, Polish and Simplified Chinese are complete; Czech grew, and Russian is now at about half.

Bumps `versionName` to 2.19.4, so merging this triggers the release pipeline.

Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/259
2026-09-02 12:44:40 +02:00

125 lines
5.1 KiB
Kotlin

package de.jeanlucmakiola.calendula
import android.app.Application
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.HiltAndroidApp
import de.jeanlucmakiola.calendula.data.backup.BackupScheduler
import de.jeanlucmakiola.calendula.data.calendar.CalendarVisibilityReconciler
import de.jeanlucmakiola.calendula.data.backup.BackupWorker
import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler
import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesSyncWorker
import de.jeanlucmakiola.calendula.data.reminders.ReminderMaintenanceScheduler
import de.jeanlucmakiola.calendula.data.reminders.ReminderMaintenanceWorker
import de.jeanlucmakiola.calendula.widget.WidgetRolloverScheduler
import de.jeanlucmakiola.floret.crash.CrashConfig
import de.jeanlucmakiola.floret.crash.CrashReporter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
/**
* Application entry point. Registered as android:name=".CalendulaApp"
* in AndroidManifest.xml. Hilt initializes its component graph here.
*/
@HiltAndroidApp
class CalendulaApp : Application() {
override fun onCreate() {
super.onCreate()
// Install first thing so startup crashes are captured too (privacy-
// respecting, on-device; the user submits the report by hand). The
// capture/loop-detection/report machinery lives in floret-kit's
// core-crash; only the app label + issue-tracker URLs are app-specific.
CrashReporter.install(
this,
CrashConfig(
appLabel = getString(R.string.app_name),
newIssueUrl = getString(R.string.report_issue_url),
chooseIssueUrl = getString(R.string.report_issue_choose_url),
issueTitle = getString(R.string.crash_report_issue_title),
),
)
reconcileAutoBackup()
reconcileSpecialDates()
reconcileCalendarVisibility()
startReminderDelivery()
reconcileWidgetRollover()
}
/**
* Re-arm the widgets' midnight rollover from whatever is actually placed
* (#228). Idempotent, and it covers what no broadcast reaches — an alarm
* dropped by a force-stop is armed again the next time the app is opened.
* Off the main thread: a handful of binder calls on every process start.
*/
private fun reconcileWidgetRollover() {
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
WidgetRolloverScheduler.sync(this@CalendulaApp)
}
}
/**
* Bring reminder delivery up with the process (#75): a scan re-arms whatever
* the system dropped and posts what a missed alarm still owes, then the
* provider watch keeps edits re-planned. The daily worker is the backstop.
*/
private fun startReminderDelivery() {
val deps = EntryPointAccessors.fromApplication(
this, ReminderMaintenanceWorker.Deps::class.java,
)
val scanner = deps.reminderScanner()
scanner.startWatchingProvider()
scanner.scanInBackground()
ReminderMaintenanceScheduler.apply(this)
}
/**
* Flush any calendar switch-off not yet written to `Calendars.VISIBLE`,
* including the set inherited from the retired app-local model (#75). A
* no-op in the steady state; `RootScreen` re-runs it after a later grant.
*/
private fun reconcileCalendarVisibility() {
val deps = EntryPointAccessors.fromApplication(
this, CalendarVisibilityReconciler.Deps::class.java,
)
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
deps.calendarVisibilityReconciler().run()
}
}
/**
* Bring the scheduled auto-backup work back in line with the saved settings
* on every launch — re-arms it after a reinstall and, crucially, cancels any
* orphaned work once backup has been turned off.
*/
private fun reconcileAutoBackup() {
val deps = EntryPointAccessors.fromApplication(this, BackupWorker.Deps::class.java)
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
val prefs = deps.settingsPrefs()
BackupScheduler.apply(
context = this@CalendulaApp,
enabled = prefs.autoBackupEnabled.first(),
intervalMinutes = prefs.autoBackupIntervalMinutes.first(),
hasFolder = prefs.autoBackupFolderUri.first() != null,
)
}
}
/**
* Re-arm (or cancel) the daily special-dates reconcile from the saved
* settings, like [reconcileAutoBackup]. The on-open refresh is RootScreen's
* ON_RESUME trigger, so no immediate run is needed here.
*/
private fun reconcileSpecialDates() {
val deps = EntryPointAccessors.fromApplication(this, SpecialDatesSyncWorker.Deps::class.java)
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
SpecialDatesScheduler.apply(
context = this@CalendulaApp,
enabled = deps.settingsPrefs().specialDatesEnabled.first(),
)
}
}
}