diff --git a/CHANGELOG.md b/CHANGELOG.md
index de59092c6..4f0b718cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,12 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
-
### Changed
- Declined events will no longer trigger notifications ([#732])
### Fixed
- Fixed incorrect widget font size on foldable devices ([#337])
+- Fixed missing or delayed reminders in some cases ([#217])
## [1.6.0] - 2025-08-21
### Added
@@ -117,6 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#148]: https://github.com/FossifyOrg/Calendar/issues/148
[#196]: https://github.com/FossifyOrg/Calendar/issues/196
+[#217]: https://github.com/FossifyOrg/Calendar/issues/217
[#262]: https://github.com/FossifyOrg/Calendar/issues/262
[#337]: https://github.com/FossifyOrg/Calendar/issues/337
[#394]: https://github.com/FossifyOrg/Calendar/issues/394
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6a1f85b9c..fa3cbfb00 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -145,6 +145,7 @@ dependencies {
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.print)
implementation(libs.bundles.room)
+ implementation(libs.androidx.work.runtime.ktx)
ksp(libs.androidx.room.compiler)
detektPlugins(libs.compose.detekt)
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3d2d0ef00..39a3bdfec 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -24,6 +24,10 @@
android:name="android.permission.USE_FINGERPRINT"
tools:node="remove" />
+
+
@@ -39,7 +43,7 @@
android:required="false" />
-
-
+
+
+
+
@@ -296,6 +302,9 @@
android:name=".receivers.AutomaticBackupReceiver"
android:exported="false" />
+
+
Unit) {
fun Context.getAlarmManager() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
-fun Context.setExactAlarm(triggerAtMillis: Long, operation: PendingIntent, type: Int = AlarmManager.RTC_WAKEUP) {
- val alarmManager = getAlarmManager()
+fun Context.setExactAlarm(
+ triggerAtMillis: Long,
+ operation: PendingIntent,
+ type: Int = AlarmManager.RTC_WAKEUP
+) = with(getAlarmManager()) {
try {
- if (isSPlus() && alarmManager.canScheduleExactAlarms() || !isSPlus()) {
- alarmManager.setExactAndAllowWhileIdle(type, triggerAtMillis, operation)
- } else {
- alarmManager.setAndAllowWhileIdle(type, triggerAtMillis, operation)
+ when {
+ // USE_EXACT_ALARM *cannot* be revoked by users on Android 13+
+ isTiramisuPlus() -> setExactAndAllowWhileIdle(type, triggerAtMillis, operation)
+
+ // SCHEDULE_EXACT_ALARM *may* be revoked by users/system on Android 12
+ isSPlus() && canScheduleExactAlarms() -> {
+ setExactAndAllowWhileIdle(type, triggerAtMillis, operation)
+ }
+
+ // No special permissions are needed *before* Android 12
+ !isSPlus() -> setExactAndAllowWhileIdle(type, triggerAtMillis, operation)
+
+ // Fallback to *inexact* alarms for Android 12. This will cause delayed reminders.
+ else -> setAndAllowWhileIdle(type, triggerAtMillis, operation)
}
} catch (e: Exception) {
showErrorToast(e)
@@ -1023,3 +1040,25 @@ fun Context.getWeekNumberWidth(): Int {
0
}
}
+
+/**
+ * Returns true if the dummy alarm is already scheduled.
+ */
+fun Context.hasDummyAlarm(): Boolean {
+ return PendingIntent.getBroadcast(
+ this, DUMMY_ALARM_REQUEST_CODE,
+ Intent(this, DummyAlarmReceiver::class.java),
+ PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
+ ) != null
+}
+
+fun Context.scheduleDummyAlarm() {
+ setExactAlarm(
+ triggerAtMillis = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1),
+ operation = PendingIntent.getBroadcast(
+ this, DUMMY_ALARM_REQUEST_CODE,
+ Intent(this, DummyAlarmReceiver::class.java),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+}
diff --git a/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt
index 763244518..2214d2898 100644
--- a/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt
+++ b/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt
@@ -13,6 +13,7 @@ const val ROW_COUNT = 6
const val COLUMN_COUNT = 7
const val SCHEDULE_CALDAV_REQUEST_CODE = 10000
const val AUTOMATIC_BACKUP_REQUEST_CODE = 10001
+const val DUMMY_ALARM_REQUEST_CODE = 10002
const val FETCH_INTERVAL = 3 * MONTH_SECONDS
const val MAX_SEARCH_YEAR =
2051218800L // 2035, limit search results for events repeating indefinitely
diff --git a/app/src/main/kotlin/org/fossify/calendar/jobs/AppStartupWorker.kt b/app/src/main/kotlin/org/fossify/calendar/jobs/AppStartupWorker.kt
new file mode 100644
index 000000000..867c033f7
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/calendar/jobs/AppStartupWorker.kt
@@ -0,0 +1,76 @@
+package org.fossify.calendar.jobs
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import androidx.core.app.NotificationCompat
+import androidx.work.CoroutineWorker
+import androidx.work.ExistingWorkPolicy
+import androidx.work.ForegroundInfo
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.OutOfQuotaPolicy
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import org.fossify.calendar.R
+import org.fossify.calendar.extensions.checkAndBackupEventsOnBoot
+import org.fossify.calendar.extensions.recheckCalDAVCalendars
+import org.fossify.calendar.extensions.scheduleAllEvents
+import org.fossify.calendar.extensions.scheduleDummyAlarm
+import org.fossify.calendar.extensions.scheduleNextAutomaticBackup
+import org.fossify.commons.extensions.notificationManager
+
+/**
+ * Does everything that needs to be done on device boot, app updates, etc.
+ */
+class AppStartupWorker(
+ context: Context,
+ params: WorkerParameters,
+) : CoroutineWorker(context, params) {
+
+ override suspend fun doWork(): Result = with(applicationContext) {
+ try {
+ scheduleAllEvents()
+ scheduleDummyAlarm()
+ scheduleNextAutomaticBackup()
+ checkAndBackupEventsOnBoot()
+ } catch (_: Throwable) {
+ return Result.retry()
+ }
+
+ recheckCalDAVCalendars(true) {}
+ Result.success()
+ }
+
+ // expedited work on before Android 12 requires a foreground service
+ override suspend fun getForegroundInfo(): ForegroundInfo {
+ val channelId = "app_startup_worker"
+ val channelName = applicationContext.getString(R.string.app_name)
+ NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_MIN).apply {
+ setSound(null, null)
+ applicationContext.notificationManager.createNotificationChannel(this)
+ }
+
+ val notification = NotificationCompat.Builder(applicationContext, channelId)
+ .setSmallIcon(R.drawable.ic_calendar_vector)
+ .setContentTitle(applicationContext.getString(R.string.app_name))
+ .setOngoing(true)
+ .build()
+ return ForegroundInfo(42, notification)
+ }
+
+ companion object {
+ fun start(
+ context: Context,
+ replaceExistingWork: Boolean = false
+ ) {
+ WorkManager.getInstance(context)
+ .enqueueUniqueWork(
+ "initialize_fossify_calendar",
+ if (replaceExistingWork) ExistingWorkPolicy.REPLACE else ExistingWorkPolicy.KEEP,
+ OneTimeWorkRequestBuilder()
+ .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+ .build()
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/fossify/calendar/receivers/BootCompletedReceiver.kt b/app/src/main/kotlin/org/fossify/calendar/receivers/BootCompletedReceiver.kt
deleted file mode 100644
index 39581e4cd..000000000
--- a/app/src/main/kotlin/org/fossify/calendar/receivers/BootCompletedReceiver.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.fossify.calendar.receivers
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import org.fossify.calendar.extensions.*
-import org.fossify.commons.helpers.ensureBackgroundThread
-
-class BootCompletedReceiver : BroadcastReceiver() {
-
- override fun onReceive(context: Context, intent: Intent) {
- ensureBackgroundThread {
- context.apply {
- scheduleAllEvents()
- notifyRunningEvents()
- recheckCalDAVCalendars(true) {}
- scheduleNextAutomaticBackup()
- checkAndBackupEventsOnBoot()
- }
- }
- }
-}
diff --git a/app/src/main/kotlin/org/fossify/calendar/receivers/DummyAlarmReceiver.kt b/app/src/main/kotlin/org/fossify/calendar/receivers/DummyAlarmReceiver.kt
new file mode 100644
index 000000000..07016881a
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/calendar/receivers/DummyAlarmReceiver.kt
@@ -0,0 +1,12 @@
+package org.fossify.calendar.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import org.fossify.calendar.extensions.scheduleDummyAlarm
+
+class DummyAlarmReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent?) {
+ context.scheduleDummyAlarm()
+ }
+}
diff --git a/app/src/main/kotlin/org/fossify/calendar/receivers/RescheduleEventsReceiver.kt b/app/src/main/kotlin/org/fossify/calendar/receivers/RescheduleEventsReceiver.kt
new file mode 100644
index 000000000..77bcc41c0
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/calendar/receivers/RescheduleEventsReceiver.kt
@@ -0,0 +1,35 @@
+package org.fossify.calendar.receivers
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import org.fossify.calendar.extensions.notifyRunningEvents
+import org.fossify.calendar.jobs.AppStartupWorker
+import org.fossify.commons.helpers.ensureBackgroundThread
+
+class RescheduleEventsReceiver : BroadcastReceiver() {
+
+ @SuppressLint("UnsafeProtectedBroadcastReceiver")
+ override fun onReceive(context: Context, intent: Intent) {
+ val action = intent.action ?: return
+ AppStartupWorker.start(
+ context = context,
+ replaceExistingWork = action == Intent.ACTION_TIME_CHANGED
+ || action == Intent.ACTION_TIMEZONE_CHANGED
+ )
+
+ val shouldNotifyRunningEvents = action != Intent.ACTION_TIME_CHANGED
+ && action != Intent.ACTION_TIMEZONE_CHANGED
+ && action != Intent.ACTION_MY_PACKAGE_REPLACED
+
+ if (shouldNotifyRunningEvents) {
+ val result = goAsync()
+ ensureBackgroundThread {
+ context.notifyRunningEvents()
+ result.finish()
+ }
+ }
+ }
+}
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d48bba848..7624659cf 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -11,6 +11,7 @@ multidex = "2.0.1"
print = "1.1.0"
constraintlayout = "2.2.1"
swiperefreshlayout = "1.1.0"
+work = "2.9.1"
#Room
room = "2.7.2"
#Fossify
@@ -29,6 +30,7 @@ androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayo
androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
androidx-print = { module = "androidx.print:print", version.ref = "print" }
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
+androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }
#Room
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }