summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/work
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/work')
-rw-r--r--app/src/main/java/org/terst/feedreader/work/FeedFetchWorker.kt52
-rw-r--r--app/src/main/java/org/terst/feedreader/work/MorningFetchReceiver.kt80
2 files changed, 15 insertions, 117 deletions
diff --git a/app/src/main/java/org/terst/feedreader/work/FeedFetchWorker.kt b/app/src/main/java/org/terst/feedreader/work/FeedFetchWorker.kt
index 9c2962f..ecd186b 100644
--- a/app/src/main/java/org/terst/feedreader/work/FeedFetchWorker.kt
+++ b/app/src/main/java/org/terst/feedreader/work/FeedFetchWorker.kt
@@ -2,9 +2,6 @@ package org.terst.feedreader.work
import android.content.Context
import androidx.work.CoroutineWorker
-import androidx.work.ExistingPeriodicWorkPolicy
-import androidx.work.NetworkType
-import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
@@ -25,26 +22,26 @@ import org.terst.feedreader.data.await
import okhttp3.Request
import java.security.MessageDigest
import java.util.concurrent.atomic.AtomicInteger
-import java.util.concurrent.TimeUnit
/**
- * Fetches every subscribed feed on a schedule, parses new items, and
- * inserts them (existing articles are left untouched -- see ArticleDao.insertNew's
- * IGNORE conflict strategy). Uses conditional GET (ETag / Last-Modified) so
- * an unchanged feed costs a 304 instead of a full re-download and re-parse.
+ * Fetches every subscribed feed, parses new items, and inserts them (existing
+ * articles are left untouched -- see ArticleDao.insertNew's IGNORE conflict
+ * strategy). Uses conditional GET (ETag / Last-Modified) so an unchanged feed
+ * costs a 304 instead of a full re-download and re-parse. Only ever triggered by
+ * fetchNow -- a fresh app launch or an explicit tap on the refresh button, never
+ * a background schedule.
*/
class FeedFetchWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
private val client = Net.client
override suspend fun doWork(): Result {
- // WorkManager's "unique work name" dedup does NOT prevent this: schedulePeriodic
- // and fetchNow use two different unique-work names (periodic and one-time requests
- // can't share one), so an hourly periodic run and a pull-to-refresh/morning-alarm
- // fetch firing at the same moment both start doWork() concurrently -- every feed
- // gets fetched twice, and both instances race writes to the same feed rows'
- // etag/lastModified/lastFetchedAt. This mutex is process-local, which is exactly
- // right here: WorkManager runs this worker in the app's own process, never a
+ // fetchNow uses ExistingWorkPolicy.KEEP against a single unique work name, so
+ // WorkManager itself already dedups a rapid double-tap of the refresh button or an
+ // overlapping cold-launch fetch. This mutex covers the remaining case: a manual
+ // fetchNow firing while doWork() from a still-running instance hasn't finished yet
+ // (e.g. a very slow feed run overlapping the next launch). Process-local, which is
+ // exactly right here: WorkManager runs this worker in the app's own process, never a
// separate one, so a single in-memory lock covers every real overlap case.
if (!fetchMutex.tryLock()) {
return Result.success()
@@ -159,11 +156,8 @@ class FeedFetchWorker(context: Context, params: WorkerParameters) : CoroutineWor
}
companion object {
- private const val UNIQUE_WORK_NAME = "feed_fetch_periodic"
-
- // Common tag on every enqueue (periodic AND one-off) so ArticleListScreen can
- // observe "is a fetch happening right now" via a single getWorkInfosByTagFlow
- // query, regardless of which of the two triggered it.
+ // Tagged so ArticleListScreen can observe "is a fetch happening right now" via a
+ // single getWorkInfosByTagFlow query.
const val FETCH_WORK_TAG = "feed_fetch"
const val KEY_CURRENT = "current"
const val KEY_TOTAL = "total"
@@ -177,23 +171,7 @@ class FeedFetchWorker(context: Context, params: WorkerParameters) : CoroutineWor
private val fetchMutex = Mutex()
- fun schedulePeriodic(context: Context) {
- val request = PeriodicWorkRequestBuilder<FeedFetchWorker>(60, TimeUnit.MINUTES)
- .setConstraints(
- androidx.work.Constraints.Builder()
- .setRequiredNetworkType(NetworkType.CONNECTED)
- .build()
- )
- .addTag(FETCH_WORK_TAG)
- .build()
- WorkManager.getInstance(context).enqueueUniquePeriodicWork(
- UNIQUE_WORK_NAME,
- ExistingPeriodicWorkPolicy.KEEP,
- request
- )
- }
-
- /** One-off immediate fetch, e.g. a manual pull-to-refresh. */
+ /** One-off immediate fetch: a fresh app launch or an explicit refresh-button tap. */
fun fetchNow(context: Context) {
val request = androidx.work.OneTimeWorkRequestBuilder<FeedFetchWorker>()
.addTag(FETCH_WORK_TAG)
diff --git a/app/src/main/java/org/terst/feedreader/work/MorningFetchReceiver.kt b/app/src/main/java/org/terst/feedreader/work/MorningFetchReceiver.kt
deleted file mode 100644
index 883fb07..0000000
--- a/app/src/main/java/org/terst/feedreader/work/MorningFetchReceiver.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-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)
- }
- }
- }
-}