summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-08-22 22:42:15 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-08-22 22:42:15 +0000
commitbc891a67802c837d77726f618e68c2ac84ea22d7 (patch)
tree15a4810f8f7ed698b91334e554c949179038e3ee /app/src/main/java/org/terst
parentef1cec60c91c2481ebad52d2d39da536606a1631 (diff)
Restore the 5am morning-fetch alarm
Reverts part of the previous commit: the explicit refresh button and fetch-on-launch stay, but the user still wants the background 5am alarm so articles are fresh before the app is opened, not just the periodic hourly WorkManager job.
Diffstat (limited to 'app/src/main/java/org/terst')
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/MainActivity.kt65
-rw-r--r--app/src/main/java/org/terst/feedreader/work/MorningFetchReceiver.kt78
2 files changed, 140 insertions, 3 deletions
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 85bc2b3..dff9858 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,10 +42,12 @@ fun decodeNavArg(value: String): String = URLDecoder.decode(value, "UTF-8")
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- // The only implicit fetch trigger left: a fresh cold launch. No periodic
- // WorkManager job and no background alarm any more -- per user request, refreshing
- // only ever happens here or from an explicit tap on the refresh button.
+ requestedExactAlarmPermission = savedInstanceState?.getBoolean(KEY_REQUESTED_EXACT_ALARM) ?: false
+ // A fresh cold launch always fetches immediately; the 5am alarm below is the only
+ // other trigger left (no periodic WorkManager job any more) -- otherwise refreshing
+ // only happens via an explicit tap on the refresh button.
FeedFetchWorker.fetchNow(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)
@@ -49,6 +61,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
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..c897046
--- /dev/null
+++ b/app/src/main/java/org/terst/feedreader/work/MorningFetchReceiver.kt
@@ -0,0 +1,78 @@
+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. 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 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)
+ }
+ }
+ }
+}