diff options
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/work')
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/work/FeedFetchWorker.kt | 123 |
1 files changed, 123 insertions, 0 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 new file mode 100644 index 0000000..560b3d8 --- /dev/null +++ b/app/src/main/java/org/terst/feedreader/work/FeedFetchWorker.kt @@ -0,0 +1,123 @@ +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 org.terst.feedreader.data.AppDatabase +import org.terst.feedreader.data.ArticleEntity +import org.terst.feedreader.data.FeedEntity +import org.terst.feedreader.data.FeedParser +import org.terst.feedreader.data.Net +import okhttp3.Request +import java.security.MessageDigest +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. + */ +class FeedFetchWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { + + private val client = Net.client + + override suspend fun doWork(): Result { + val db = AppDatabase.get(applicationContext) + val feeds = db.feedDao().getAll() + + for (feed in feeds) { + fetchOne(feed, db) + } + return Result.success() + } + + private suspend fun fetchOne(feed: FeedEntity, db: AppDatabase) { + val requestBuilder = Request.Builder().url(feed.id) + feed.etag?.let { requestBuilder.header("If-None-Match", it) } + feed.lastModified?.let { requestBuilder.header("If-Modified-Since", it) } + + try { + client.newCall(requestBuilder.build()).execute().use { response -> + val now = System.currentTimeMillis() + if (response.code == 304) { + db.feedDao().update(feed.copy(lastFetchedAt = now, lastError = null)) + return + } + if (!response.isSuccessful) { + db.feedDao().update(feed.copy(lastFetchedAt = now, lastError = "HTTP ${response.code}")) + return + } + + val body = response.body ?: return + val parsed = FeedParser.parse(body.byteStream()) + + val articles = parsed.items.map { item -> + val id = item.guid ?: sha256(item.link) + ArticleEntity( + id = id, + feedId = feed.id, + title = item.title, + link = item.link, + author = item.author, + publishedAt = item.publishedAt, + feedContentHtml = item.contentHtml, + createdAt = now + ) + } + db.articleDao().insertNew(articles) + + db.feedDao().update( + feed.copy( + title = parsed.title.ifBlank { feed.title }, + siteUrl = parsed.siteUrl ?: feed.siteUrl, + lastFetchedAt = now, + etag = response.header("ETag") ?: feed.etag, + lastModified = response.header("Last-Modified") ?: feed.lastModified, + lastError = null + ) + ) + } + } catch (e: Exception) { + db.feedDao().update(feed.copy(lastFetchedAt = System.currentTimeMillis(), lastError = e.message ?: "Fetch failed")) + } + } + + private fun sha256(input: String): String { + val digest = MessageDigest.getInstance("SHA-256").digest(input.toByteArray()) + return digest.joinToString("") { "%02x".format(it) } + } + + companion object { + private const val UNIQUE_WORK_NAME = "feed_fetch_periodic" + + fun schedulePeriodic(context: Context) { + val request = PeriodicWorkRequestBuilder<FeedFetchWorker>(60, TimeUnit.MINUTES) + .setConstraints( + androidx.work.Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .build() + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + UNIQUE_WORK_NAME, + ExistingPeriodicWorkPolicy.KEEP, + request + ) + } + + /** One-off immediate fetch, e.g. a manual pull-to-refresh. */ + fun fetchNow(context: Context) { + val request = androidx.work.OneTimeWorkRequestBuilder<FeedFetchWorker>().build() + WorkManager.getInstance(context).enqueueUniqueWork( + "feed_fetch_now", + androidx.work.ExistingWorkPolicy.KEEP, + request + ) + } + } +} |
