diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-08-22 22:42:15 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-08-22 22:42:15 +0000 |
| commit | bc891a67802c837d77726f618e68c2ac84ea22d7 (patch) | |
| tree | 15a4810f8f7ed698b91334e554c949179038e3ee /app/src | |
| parent | ef1cec60c91c2481ebad52d2d39da536606a1631 (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')
| -rw-r--r-- | app/src/main/AndroidManifest.xml | 20 | ||||
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/ui/MainActivity.kt | 65 | ||||
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/work/MorningFetchReceiver.kt | 78 |
3 files changed, 160 insertions, 3 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a91bd3a..325a2cf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,13 @@ <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> + <!-- For the 5am morning-fetch alarm (MorningFetchReceiver): lets it wake the device + at an exact time even under Doze, without the much broader "ignore battery + optimizations" exemption (user explicitly wants battery optimization left on + otherwise). Android 12+ requires the user grant this once via Settings; MainActivity + prompts for it if not already granted. --> + <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <application android:label="@string/app_name" @@ -40,5 +47,18 @@ </intent-filter> </activity> + <!-- + Fires the 5am morning fetch (exact alarm, set up by MorningFetchReceiver.scheduleNext, + called from MainActivity). Also re-arms that alarm after a reboot: an exact alarm + set via AlarmManager does NOT survive a device restart on its own. + --> + <receiver + android:name=".work.MorningFetchReceiver" + android:exported="false"> + <intent-filter> + <action android:name="android.intent.action.BOOT_COMPLETED" /> + </intent-filter> + </receiver> + </application> </manifest> 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) + } + } + } +} |
