diff options
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) + } + } + } +} |
