mirror of
https://github.com/FossifyOrg/Calendar.git
synced 2025-12-23 23:59:23 -05:00
fix: improve event scheduling logic on reboot (#743)
* fix: call `goAsync()` before doing background work in boot completed receiver It's the recommended mechanism. Otherwise, there's a good chance that the system may kill the process as soon as `onReceive()` returns. * fix: remove `canScheduleExactAlarms()` check for Android 13 and above `AlarmManager.canScheduleExactAlarms()` should return `true` for `USE_EXACT_ALARM` as per the source code, but skipping the check shouldn't hurt. It might also help in case OEMs have customized this behavior. See: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java#2850 * fix: reschedule events when time is set or when timezone changes * refactor: delegate startup work to work manager - This receiver was doing too much work and likely timed out when there are many events. Using a dedicated job should prevent such issues. - The `android.permission.FOREGROUND_SERVICE` permission is required prior to Android 12 for expedited work. * fix: reschedule events on app startup when alarms are missing This should help with recovery in case the app was force-stopped or if the alarms were cleared somehow, e.g., by battery optimization or if the OS ate the BOOT_COMPLETED broadcast for some reason. * docs: update changelog Refs: https://github.com/FossifyOrg/Calendar/issues/217
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
android:name="android.permission.USE_FINGERPRINT"
|
||||
tools:node="remove" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<queries>
|
||||
<package android:name="org.fossify.contacts.debug" />
|
||||
<package android:name="org.fossify.contacts" />
|
||||
@@ -39,7 +43,7 @@
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name="org.fossify.commons.FossifyApp"
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="productivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -281,14 +285,16 @@
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.BootCompletedReceiver"
|
||||
android:name=".receivers.RescheduleEventsReceiver"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
|
||||
<action android:name="android.intent.action.TIME_SET" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
@@ -296,6 +302,9 @@
|
||||
android:name=".receivers.AutomaticBackupReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver android:name=".receivers.DummyAlarmReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
||||
14
app/src/main/kotlin/org/fossify/calendar/App.kt
Normal file
14
app/src/main/kotlin/org/fossify/calendar/App.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package org.fossify.calendar
|
||||
|
||||
import org.fossify.calendar.extensions.hasDummyAlarm
|
||||
import org.fossify.calendar.jobs.AppStartupWorker
|
||||
import org.fossify.commons.FossifyApp
|
||||
|
||||
class App : FossifyApp() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (!hasDummyAlarm()) {
|
||||
AppStartupWorker.start(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import org.fossify.calendar.helpers.DEFAULT_START_TIME_NEXT_FULL_HOUR
|
||||
import org.fossify.calendar.helpers.DELETE_ALL_OCCURRENCES
|
||||
import org.fossify.calendar.helpers.DELETE_FUTURE_OCCURRENCES
|
||||
import org.fossify.calendar.helpers.DELETE_SELECTED_OCCURRENCE
|
||||
import org.fossify.calendar.helpers.DUMMY_ALARM_REQUEST_CODE
|
||||
import org.fossify.calendar.helpers.EVENT_ID
|
||||
import org.fossify.calendar.helpers.EVENT_OCCURRENCE_TS
|
||||
import org.fossify.calendar.helpers.EventsHelper
|
||||
@@ -81,6 +82,7 @@ import org.fossify.calendar.models.ListSectionMonth
|
||||
import org.fossify.calendar.models.Task
|
||||
import org.fossify.calendar.receivers.AutomaticBackupReceiver
|
||||
import org.fossify.calendar.receivers.CalDAVSyncReceiver
|
||||
import org.fossify.calendar.receivers.DummyAlarmReceiver
|
||||
import org.fossify.calendar.receivers.NotificationReceiver
|
||||
import org.fossify.calendar.services.MarkCompletedService
|
||||
import org.fossify.calendar.services.SnoozeService
|
||||
@@ -116,6 +118,7 @@ import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
import org.fossify.commons.helpers.isOreoPlus
|
||||
import org.fossify.commons.helpers.isRPlus
|
||||
import org.fossify.commons.helpers.isSPlus
|
||||
import org.fossify.commons.helpers.isTiramisuPlus
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeConstants
|
||||
import org.joda.time.Days
|
||||
@@ -123,6 +126,7 @@ import org.joda.time.LocalDate
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -847,7 +851,7 @@ fun Context.getFirstDayOfWeek(date: DateTime): String {
|
||||
}
|
||||
|
||||
fun Context.getFirstDayOfWeekDt(date: DateTime): DateTime {
|
||||
var today = date.withTimeAtStartOfDay()
|
||||
val today = date.withTimeAtStartOfDay()
|
||||
var currentDate = today
|
||||
if (!config.startWeekWithCurrentDay) {
|
||||
val firstDayOfWeek = config.firstDayOfWeek
|
||||
@@ -997,13 +1001,26 @@ fun Context.addImportIdsToTasks(callback: () -> 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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<AppStartupWorker>()
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user