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" }