diff --git a/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt b/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt index 9d9829127..0972c4ca6 100644 --- a/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt @@ -2,14 +2,17 @@ package to.bitkit.repositories import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import to.bitkit.data.SettingsStore @@ -30,6 +33,7 @@ import to.bitkit.models.widget.HeadlinePreferences import to.bitkit.models.widget.PricePreferences import to.bitkit.models.widget.WeatherPreferences import to.bitkit.utils.Logger +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton @@ -44,9 +48,8 @@ class WidgetsRepo @Inject constructor( private val widgetsStore: WidgetsStore, private val settingsStore: SettingsStore, ) { - // TODO Only refresh in loop widgets displayed in the Home - // TODO Perform a refresh when the preview screen is displayed private val repoScope = CoroutineScope(bgDispatcher + SupervisorJob()) + private val widgetJobs = ConcurrentHashMap() val widgetsDataFlow = widgetsStore.data val showWidgetTitles = settingsStore.data.map { it.showWidgetTitles } @@ -63,7 +66,86 @@ class WidgetsRepo @Inject constructor( val refreshStates: StateFlow> = _refreshStates.asStateFlow() init { - startPeriodicUpdates() + observeWidgetStateChanges() + } + + private fun observeWidgetStateChanges() { + repoScope.launch { + widgetsDataFlow + .map { it.widgets.map { widget -> widget.type }.toSet() } + .distinctUntilChanged() + .collect { enabledWidgetTypes -> + updateWidgetJobs(enabledWidgetTypes) + } + } + } + + private fun updateWidgetJobs(enabledWidgetTypes: Set) { + val widgetTypesWithServices = WidgetType.entries.filter { + it != WidgetType.CALCULATOR + } + + widgetTypesWithServices.forEach { widgetType -> + val isEnabled = widgetType in enabledWidgetTypes + val hasRunningJob = widgetJobs.containsKey(widgetType) && + widgetJobs[widgetType]?.isActive == true + + when { + isEnabled && !hasRunningJob -> startWidgetRefresh(widgetType) + !isEnabled && hasRunningJob -> stopWidgetRefresh(widgetType) + } + } + } + + private fun startWidgetRefresh(widgetType: WidgetType) { + stopWidgetRefresh(widgetType) + + val job = when (widgetType) { + WidgetType.NEWS -> repoScope.launch { + while (isActive) { + updateWidget(newsService) { widgetsStore.updateArticles(it) } + delay(newsService.refreshInterval) + } + } + + WidgetType.FACTS -> repoScope.launch { + while (isActive) { + updateWidget(factsService) { widgetsStore.updateFacts(it) } + delay(factsService.refreshInterval) + } + } + + WidgetType.BLOCK -> repoScope.launch { + while (isActive) { + updateWidget(blocksService) { widgetsStore.updateBlock(it) } + delay(blocksService.refreshInterval) + } + } + + WidgetType.WEATHER -> repoScope.launch { + while (isActive) { + updateWidget(weatherService) { widgetsStore.updateWeather(it) } + delay(weatherService.refreshInterval) + } + } + + WidgetType.PRICE -> repoScope.launch { + while (isActive) { + updateWidget(priceService) { widgetsStore.updatePrice(it) } + delay(priceService.refreshInterval) + } + } + + WidgetType.CALCULATOR -> throw NotImplementedError("Calculator widget doesn't need a service") + } + + widgetJobs[widgetType] = job + } + + private fun stopWidgetRefresh(widgetType: WidgetType) { + widgetJobs[widgetType]?.cancel() + widgetJobs.remove(widgetType) + Logger.verbose("Stopped refresh coroutine for $widgetType", context = TAG) } suspend fun addWidget(type: WidgetType) = withContext(bgDispatcher) { widgetsStore.addWidget(type) } @@ -96,48 +178,10 @@ class WidgetsRepo @Inject constructor( suspend fun fetchAllPeriods() = withContext(bgDispatcher) { priceService.fetchAllPeriods() } - /** - * Start periodic updates for all widgets - */ - private fun startPeriodicUpdates() { - startPeriodicUpdate(newsService) { articles -> - widgetsStore.updateArticles(articles) - } - startPeriodicUpdate(factsService) { facts -> - widgetsStore.updateFacts(facts) - } - startPeriodicUpdate(blocksService) { block -> - widgetsStore.updateBlock(block) - } - startPeriodicUpdate(weatherService) { weather -> - widgetsStore.updateWeather(weather) - } - startPeriodicUpdate(priceService) { price -> - widgetsStore.updatePrice(price) - } - } - - /** - * Generic method to start periodic updates for any widget service - */ - private fun startPeriodicUpdate( - service: WidgetService, - updateStore: suspend (T) -> Unit - ) { - repoScope.launch { - while (true) { - updateWidget(service, updateStore) - delay(service.refreshInterval) - } - } - } - /** - * Update a specific widget type - */ private suspend fun updateWidget( service: WidgetService, - updateStore: suspend (T) -> Unit + updateStore: suspend (T) -> Unit, ) { val widgetType = service.widgetType _refreshStates.update { it + (widgetType to true) } @@ -145,7 +189,7 @@ class WidgetsRepo @Inject constructor( service.fetchData() .onSuccess { data -> updateStore(data) - Logger.verbose("Updated $widgetType widget successfully") + Logger.verbose("Updated $widgetType widget successfully", context = TAG) } .onFailure { e -> Logger.verbose("Failed to update $widgetType widget", e = e, context = TAG) @@ -154,27 +198,6 @@ class WidgetsRepo @Inject constructor( _refreshStates.update { it + (widgetType to false) } } - /** - * Manually refresh all widgets - */ - suspend fun refreshAllWidgets(): Result = runCatching { - updateWidget(newsService) { articles -> - widgetsStore.updateArticles(articles) - } - updateWidget(factsService) { facts -> - widgetsStore.updateFacts(facts) - } - updateWidget(blocksService) { block -> - widgetsStore.updateBlock(block) - } - updateWidget(weatherService) { weather -> - widgetsStore.updateWeather(weather) - } - updateWidget(priceService) { price -> - widgetsStore.updatePrice(price) - } - } - suspend fun refreshEnabledWidgets() = withContext(bgDispatcher) { widgetsDataFlow.first().widgets.forEach { refreshWidget(it.type) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreen.kt index dcba3cb0c..f2cd45de3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksPreviewScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -51,6 +52,10 @@ fun BlocksPreviewScreen( val currentBlock by blocksViewModel.currentBlock.collectAsStateWithLifecycle() val isBlocksWidgetEnabled by blocksViewModel.isBlocksWidgetEnabled.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + blocksViewModel.refreshOnDisplay() + } + BlocksPreviewContent( onBack = onBack, isBlocksWidgetEnabled = isBlocksWidgetEnabled, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksViewModel.kt index 793f43a60..3f3c0b93a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/blocks/BlocksViewModel.kt @@ -122,6 +122,12 @@ class BlocksViewModel @Inject constructor( } } + fun refreshOnDisplay() { + viewModelScope.launch { + widgetsRepo.refreshWidget(WidgetType.BLOCK) + } + } + // MARK: - Private Methods private fun initializeCustomPreferences() { diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreen.kt index 3007611a7..10e2d34db 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsPreviewScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -50,6 +51,10 @@ fun FactsPreviewScreen( val fact by factsViewModel.currentFact.collectAsStateWithLifecycle() val isFactsWidgetEnabled by factsViewModel.isFactsWidgetEnabled.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + factsViewModel.refreshOnDisplay() + } + FactsPreviewContent( onBack = onBack, isFactsWidgetEnabled = isFactsWidgetEnabled, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsViewModel.kt index aa3e89546..dfc9cb881 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/facts/FactsViewModel.kt @@ -89,6 +89,12 @@ class FactsViewModel @Inject constructor( } } + fun refreshOnDisplay() { + viewModelScope.launch { + widgetsRepo.refreshWidget(WidgetType.FACTS) + } + } + // MARK: - Private Methods private fun initializeCustomPreferences() { diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt index 308e7a3ce..29730b48f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesPreviewScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -51,6 +52,10 @@ fun HeadlinesPreviewScreen( val article by headlinesViewModel.currentArticle.collectAsStateWithLifecycle() val isHeadlinesImplemented by headlinesViewModel.isNewsWidgetEnabled.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + headlinesViewModel.refreshOnDisplay() + } + HeadlinesPreviewContent( onBack = onBack, isHeadlinesImplemented = isHeadlinesImplemented, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesViewModel.kt index 5e16550ac..14f389e93 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlinesViewModel.kt @@ -98,6 +98,12 @@ class HeadlinesViewModel @Inject constructor( } } + fun refreshOnDisplay() { + viewModelScope.launch { + widgetsRepo.refreshWidget(WidgetType.NEWS) + } + } + // MARK: - Private Methods private fun initializeCustomPreferences() { diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt index 1d2322900..9ca260bd3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PricePreviewScreen.kt @@ -60,6 +60,10 @@ fun PricePreviewScreen( val isPriceWidgetEnabled by priceViewModel.isPriceWidgetEnabled.collectAsStateWithLifecycle() val isLoading by priceViewModel.isLoading.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + priceViewModel.refreshOnDisplay() + } + LaunchedEffect(Unit) { priceViewModel.priceEffect.collect { effect -> when (effect) { diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt index fbcc7fd83..0c4df4cd6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt @@ -130,6 +130,12 @@ class PriceViewModel @Inject constructor( } } + fun refreshOnDisplay() { + viewModelScope.launch { + widgetsRepo.refreshWidget(WidgetType.PRICE) + } + } + private fun initializeCustomPreferences() { viewModelScope.launch { pricePreferences.collect { preferences -> diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreen.kt index 8f89c5451..3747138fe 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherPreviewScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -53,6 +54,10 @@ fun WeatherPreviewScreen( val weather by weatherViewModel.currentWeather.collectAsStateWithLifecycle() val isWeatherWidgetEnabled by weatherViewModel.isWeatherWidgetEnabled.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + weatherViewModel.refreshOnDisplay() + } + WeatherPreviewContent( onBack = onBack, isWeatherWidgetEnabled = isWeatherWidgetEnabled, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherViewModel.kt index ef649e449..d90bd4a57 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/weather/WeatherViewModel.kt @@ -124,6 +124,12 @@ class WeatherViewModel @Inject constructor( } } + fun refreshOnDisplay() { + viewModelScope.launch { + widgetsRepo.refreshWidget(WidgetType.WEATHER) + } + } + // MARK: - Private Methods private fun initializeCustomPreferences() {