diff --git a/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/FeedsRefreshWorker.kt b/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/FeedsRefreshWorker.kt index 2b9f2878d..c4becc204 100644 --- a/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/FeedsRefreshWorker.kt +++ b/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/FeedsRefreshWorker.kt @@ -22,6 +22,7 @@ import androidx.work.NetworkType import androidx.work.PeriodicWorkRequest import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkerParameters +import dev.sasikanth.rss.reader.refresh.LastUpdatedAt import dev.sasikanth.rss.reader.repository.RssRepository import io.sentry.kotlin.multiplatform.Sentry import java.lang.Exception @@ -30,7 +31,8 @@ import java.time.Duration class FeedsRefreshWorker( context: Context, workerParameters: WorkerParameters, - private val rssRepository: RssRepository + private val rssRepository: RssRepository, + private val lastUpdatedAt: LastUpdatedAt ) : CoroutineWorker(context, workerParameters) { companion object { @@ -53,6 +55,7 @@ class FeedsRefreshWorker( override suspend fun doWork(): Result { return try { rssRepository.updateFeeds() + lastUpdatedAt.refresh() Result.success() } catch (e: Exception) { Sentry.captureException(e) diff --git a/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/ReaderApplication.kt b/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/ReaderApplication.kt index 16f2e25a0..15b9fa2ec 100644 --- a/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/ReaderApplication.kt +++ b/androidApp/src/androidMain/kotlin/dev/sasikanth/rss/reader/ReaderApplication.kt @@ -59,7 +59,12 @@ class ReaderApplication : Application(), Configuration.Provider { workerClassName: String, workerParameters: WorkerParameters ): ListenableWorker { - return FeedsRefreshWorker(appContext, workerParameters, appComponent.rssRepository) + return FeedsRefreshWorker( + context = appContext, + workerParameters = workerParameters, + rssRepository = appComponent.rssRepository, + lastUpdatedAt = appComponent.lastUpdatedAt + ) } } ) diff --git a/iosApp/iosApp/AppDelegate.swift b/iosApp/iosApp/AppDelegate.swift index 221cb6d7c..b2479975a 100644 --- a/iosApp/iosApp/AppDelegate.swift +++ b/iosApp/iosApp/AppDelegate.swift @@ -44,6 +44,11 @@ class AppDelegate: NSObject, UIApplicationDelegate { func refreshFeeds(task: BGAppRefreshTask) { scheduledRefreshFeeds() applicationComponent.rssRepository.updateFeeds { error in + if error != nil { + self.applicationComponent.lastUpdatedAt.refresh { error in + // no-op + } + } task.setTaskCompleted(success: error == nil) } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt index b5ea77a49..3635174d2 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/app/AppPresenter.kt @@ -24,11 +24,14 @@ import com.arkivanov.decompose.router.stack.push import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.instancekeeper.InstanceKeeper import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.arkivanov.essenty.lifecycle.doOnStart import com.arkivanov.essenty.parcelable.Parcelable import com.arkivanov.essenty.parcelable.Parcelize import dev.sasikanth.rss.reader.bookmarks.BookmarksPresenter import dev.sasikanth.rss.reader.di.scopes.ActivityScope import dev.sasikanth.rss.reader.home.HomePresenter +import dev.sasikanth.rss.reader.refresh.LastUpdatedAt +import dev.sasikanth.rss.reader.repository.RssRepository import dev.sasikanth.rss.reader.repository.SettingsRepository import dev.sasikanth.rss.reader.search.SearchPresenter import dev.sasikanth.rss.reader.settings.SettingsPresenter @@ -43,6 +46,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject private typealias HomePresenterFactory = @@ -77,14 +81,18 @@ class AppPresenter( private val homePresenter: HomePresenterFactory, private val searchPresenter: SearchPresentFactory, private val bookmarksPresenter: BookmarkPresenterFactory, - private val settingsPresenter: SettingsPresenterFactory + private val settingsPresenter: SettingsPresenterFactory, + private val lastUpdatedAt: LastUpdatedAt, + private val rssRepository: RssRepository ) : ComponentContext by componentContext { private val presenterInstance = instanceKeeper.getOrCreate { PresenterInstance( dispatchersProvider = dispatchersProvider, - settingsRepository = settingsRepository + settingsRepository = settingsRepository, + lastUpdatedAt = lastUpdatedAt, + rssRepository = rssRepository ) } @@ -99,6 +107,10 @@ class AppPresenter( childFactory = ::createScreen, ) + init { + lifecycle.doOnStart { presenterInstance.refreshFeedsIfExpired() } + } + fun onBackClicked() { navigation.pop() } @@ -129,7 +141,9 @@ class AppPresenter( private class PresenterInstance( dispatchersProvider: DispatchersProvider, - private val settingsRepository: SettingsRepository + settingsRepository: SettingsRepository, + private val lastUpdatedAt: LastUpdatedAt, + private val rssRepository: RssRepository ) : InstanceKeeper.Instance { private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) @@ -148,6 +162,15 @@ class AppPresenter( .launchIn(coroutineScope) } + fun refreshFeedsIfExpired() { + coroutineScope.launch { + if (lastUpdatedAt.hasExpired()) { + rssRepository.updateFeeds() + lastUpdatedAt.refresh() + } + } + } + override fun onDestroy() { coroutineScope.cancel() } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/SharedApplicationComponent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/SharedApplicationComponent.kt index bc84f4bae..28a024d81 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/SharedApplicationComponent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/di/SharedApplicationComponent.kt @@ -19,6 +19,7 @@ import dev.sasikanth.rss.reader.components.image.ImageLoader import dev.sasikanth.rss.reader.di.scopes.AppScope import dev.sasikanth.rss.reader.initializers.Initializer import dev.sasikanth.rss.reader.network.NetworkComponent +import dev.sasikanth.rss.reader.refresh.LastUpdatedAt import dev.sasikanth.rss.reader.sentry.SentryComponent import dev.sasikanth.rss.reader.utils.DefaultDispatchersProvider import dev.sasikanth.rss.reader.utils.DispatchersProvider @@ -31,5 +32,7 @@ abstract class SharedApplicationComponent : abstract val initializers: Set + abstract val lastUpdatedAt: LastUpdatedAt + @Provides @AppScope fun DefaultDispatchersProvider.bind(): DispatchersProvider = this } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/refresh/LastUpdatedAt.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/refresh/LastUpdatedAt.kt new file mode 100644 index 000000000..4ae8a9898 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/refresh/LastUpdatedAt.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Sasikanth Miriyampalli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.sasikanth.rss.reader.refresh + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import dev.sasikanth.rss.reader.di.scopes.AppScope +import kotlin.time.Duration.Companion.minutes +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.datetime.Clock +import kotlinx.datetime.toInstant +import me.tatarka.inject.annotations.Inject + +@Inject +@AppScope +class LastUpdatedAt(private val dataStore: DataStore) { + + companion object { + private val UPDATE_DURATION = 60.minutes + } + + private val lastUpdatedAtKey = stringPreferencesKey("pref_last_updated_at") + + suspend fun refresh() { + dataStore.edit { preferences -> preferences[lastUpdatedAtKey] = Clock.System.now().toString() } + } + + suspend fun hasExpired(): Boolean { + val lastUpdatedAt = fetchLastUpdatedAt() ?: return true + val currentTime = Clock.System.now() + val lastUpdateDuration = currentTime - lastUpdatedAt + + return lastUpdateDuration > UPDATE_DURATION + } + + private suspend fun fetchLastUpdatedAt() = + dataStore.data + .map { preferences -> preferences[lastUpdatedAtKey] ?: return@map null } + .first() + ?.toInstant() +}