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. --- .../java/org/terst/feedreader/ui/MainActivity.kt | 59 ++++++++++++++++++++++ 1 file changed, 59 insertions(+) (limited to 'app/src/main/java/org/terst/feedreader/ui') diff --git a/app/src/main/java/org/terst/feedreader/ui/MainActivity.kt b/app/src/main/java/org/terst/feedreader/ui/MainActivity.kt index 9add10a..38c04e4 100644 --- a/app/src/main/java/org/terst/feedreader/ui/MainActivity.kt +++ b/app/src/main/java/org/terst/feedreader/ui/MainActivity.kt @@ -1,6 +1,12 @@ package org.terst.feedreader.ui +import android.app.AlarmManager +import android.content.Intent +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.provider.Settings +import android.util.Log import android.webkit.CookieManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -16,9 +22,13 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import org.terst.feedreader.work.FeedFetchWorker +import org.terst.feedreader.work.MorningFetchReceiver import java.net.URLDecoder import java.net.URLEncoder +private const val TAG = "MainActivity" +private const val KEY_REQUESTED_EXACT_ALARM = "requestedExactAlarmPermission" + const val UNREAD_FEED_ID = "__unread__" const val SAVED_FEED_ID = "__saved__" const val OLDER_FEED_ID = "__older__" @@ -32,7 +42,9 @@ fun decodeNavArg(value: String): String = URLDecoder.decode(value, "UTF-8") class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + requestedExactAlarmPermission = savedInstanceState?.getBoolean(KEY_REQUESTED_EXACT_ALARM) ?: false FeedFetchWorker.schedulePeriodic(applicationContext) + maybeScheduleMorningFetch() // Explicit, even though it defaults on -- BrowserScreen relies on cookies set // during a site login surviving into the reader's later extraction requests. CookieManager.getInstance().setAcceptCookie(true) @@ -46,6 +58,53 @@ class MainActivity : ComponentActivity() { } } } + + // Re-checked on resume too, not just onCreate -- returning here from the "Alarms & + // reminders" Settings screen (below) is an onResume, not a fresh onCreate, and the + // alarm should get armed the moment permission is actually granted rather than + // needing a full app relaunch. + override fun onResume() { + super.onResume() + maybeScheduleMorningFetch() + } + + // onCreate's own onStart/onResume fires once automatically as part of normal startup, + // right after onCreate -- before the user has done anything -- so without this guard, + // a not-yet-granted permission would launch the Settings screen twice back to back + // (once from onCreate, immediately again from that auto-triggered onResume). Restored + // from savedInstanceState in onCreate (see below), not left as a plain field that + // silently resets to false on every config change -- a rotation while still ungranted + // used to re-trigger the Settings screen every time the device rotated. + private var requestedExactAlarmPermission = false + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(KEY_REQUESTED_EXACT_ALARM, requestedExactAlarmPermission) + } + + // Runs unconditionally on every onCreate/onResume -- must never let an exception + // escape from here, or a device/OEM quirk around the exact-alarm APIs would crash + // the app on every single launch. A wake-up alarm failing to schedule just isn't + // worth that; worst case it's logged and tries again next launch. + private fun maybeScheduleMorningFetch() { + try { + val alarmManager = getSystemService(AlarmManager::class.java) ?: return + val canScheduleExact = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || alarmManager.canScheduleExactAlarms() + if (canScheduleExact) { + MorningFetchReceiver.scheduleNext(applicationContext) + } else if (!requestedExactAlarmPermission) { + requestedExactAlarmPermission = true + // Android 12+ only: exact alarms require a one-time user grant via Settings, + // there's no in-app permission dialog for this one. Sends the user straight to + // this app's page in the "Alarms & reminders" settings screen. + startActivity( + Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM, Uri.parse("package:$packageName")) + ) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to schedule or request permission for morning fetch", e) + } + } } @Composable -- cgit v1.2.3