From 7f324168464934d711953dc748fbc7c65eb88f4b Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 22 Aug 2026 01:08:22 +0000 Subject: Add a 5am morning-fetch alarm so articles are fresh before you open the app FeedFetchWorker's periodic 60-minute WorkManager job can (and does) get silently deferred for hours by Doze on an idle overnight device. An exact alarm (setExactAndAllowWhileIdle) wakes the device at a fixed 5am to run a fetch, without requesting the much broader "ignore battery optimizations" exemption -- battery optimization stays on otherwise, just this one daily wake-up. Android 12+ requires a one-time user grant for exact alarms (no in-app permission dialog); MainActivity requests it via Settings if not already granted, tracked across rotation via savedInstanceState so a config change doesn't re-trigger the Settings screen. The alarm also gets re-armed on BOOT_COMPLETED, since AlarmManager alarms don't survive a device restart on their own. --- .../terst/feedreader/work/MorningFetchReceiver.kt | 80 ++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 app/src/main/java/org/terst/feedreader/work/MorningFetchReceiver.kt (limited to 'app/src/main/java/org/terst/feedreader/work') diff --git a/app/src/main/java/org/terst/feedreader/work/MorningFetchReceiver.kt b/app/src/main/java/org/terst/feedreader/work/MorningFetchReceiver.kt new file mode 100644 index 0000000..883fb07 --- /dev/null +++ b/app/src/main/java/org/terst/feedreader/work/MorningFetchReceiver.kt @@ -0,0 +1,80 @@ +package org.terst.feedreader.work + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import java.util.Calendar + +private const val TAG = "MorningFetch" + +/** + * Wakes the device at a fixed time every morning to run a feed fetch, so the app has + * fresh articles ready before the user ever opens it -- rather than relying purely on + * FeedFetchWorker's 60-minute WorkManager periodic job, which Doze/battery management + * can (and does) silently defer for hours on an idle overnight device. Uses an exact + * alarm (setExactAndAllowWhileIdle) specifically so this one wake survives Doze without + * requesting the much broader "ignore battery optimizations" exemption -- the user wants + * battery optimization left on otherwise, just this one daily wake-up. + */ +class MorningFetchReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + try { + // A reboot only needs the alarm re-armed (AlarmManager alarms don't survive a + // restart) -- an actual fetch isn't due just because the phone restarted. + if (intent.action != Intent.ACTION_BOOT_COMPLETED) { + FeedFetchWorker.fetchNow(context) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to enqueue morning fetch for action=${intent.action}", e) + } + scheduleNext(context) + } + + companion object { + private const val HOUR = 5 + private const val MINUTE = 0 + private const val ACTION_MORNING_FETCH = "org.terst.feedreader.MORNING_FETCH" + + private fun nextTriggerMillis(): Long { + val now = Calendar.getInstance() + val next = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, HOUR) + set(Calendar.MINUTE, MINUTE) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + if (next.timeInMillis <= now.timeInMillis) { + next.add(Calendar.DAY_OF_MONTH, 1) + } + return next.timeInMillis + } + + /** No-ops quietly if the exact-alarm permission isn't granted yet (Android 12+) -- + * MainActivity is what prompts for that; this just skips scheduling until it's there. + * Also survives any failure from AlarmManager itself (logged, not crashed): this runs + * unconditionally from MainActivity.onCreate/onResume on every single launch, so an + * unguarded exception here would crash the app on every open -- a background wake-up + * alarm failing to schedule isn't worth that; just log it and let the next periodic + * WorkManager fetch or next app open try again. */ + fun scheduleNext(context: Context) { + try { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager ?: return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) { + return + } + val intent = Intent(context, MorningFetchReceiver::class.java).setAction(ACTION_MORNING_FETCH) + val pendingIntent = PendingIntent.getBroadcast( + context, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextTriggerMillis(), pendingIntent) + } catch (e: Exception) { + Log.e(TAG, "Failed to schedule morning fetch alarm", e) + } + } + } +} -- cgit v1.2.3