From 760e0cdecd6f539d2ec6f37265d762e7fb5c1c2b Mon Sep 17 00:00:00 2001 From: qaziabubakar-vd Date: Wed, 20 Nov 2024 14:32:30 +0500 Subject: [PATCH 01/16] fix tests --- .../configuration/ConfigurationRegistry.kt | 55 +++++++++++++------ .../fhircore/engine/sync/CustomSyncState.kt | 27 +++++++++ .../fhircore/engine/sync/CustomSyncWorker.kt | 2 + .../fhircore/engine/sync/CustomWorkerState.kt | 29 ++++++++++ .../quest/ui/main/AppMainViewModel.kt | 46 +++++++++++++--- 5 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncState.kt create mode 100644 android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index 50579cce387..b4f9d77f8a2 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -59,6 +59,8 @@ import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource import org.smartregister.fhircore.engine.di.NetworkModule import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction +import org.smartregister.fhircore.engine.sync.CustomSyncState +import org.smartregister.fhircore.engine.sync.CustomWorkerState import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.KnowledgeManagerUtil import org.smartregister.fhircore.engine.util.SharedPreferenceKey @@ -529,29 +531,46 @@ constructor( suspend fun fetchResources( gatewayModeHeaderValue: String? = null, url: String, + enableCustomSyncWorkerLogs: Boolean = false ) { - val resultBundle = - runCatching { - if (gatewayModeHeaderValue.isNullOrEmpty()) { - fhirResourceDataSource.getResource(url) - } else { - fhirResourceDataSource.getResourceWithGatewayModeHeader(gatewayModeHeaderValue, url) - } - } - .onFailure { throwable -> - Timber.e("Error occurred while retrieving resource via URL $url", throwable) + var currentPage = 0 + var totalProcessedResources = 0 + + var nextPageUrl: String? = url + while (!nextPageUrl.isNullOrEmpty()) { + val currentUrl = nextPageUrl // Create an immutable reference for this iteration + + currentPage++ + + Timber.d("Fetching page $currentPage with URL: $currentUrl") + if (enableCustomSyncWorkerLogs) CustomWorkerState.postState(CustomSyncState.InProgress) + + val resultBundle = runCatching { + if (gatewayModeHeaderValue.isNullOrEmpty()) { + fhirResourceDataSource.getResource(currentUrl) + } else { + fhirResourceDataSource.getResourceWithGatewayModeHeader( + gatewayModeHeaderValue, + currentUrl + ) } - .getOrThrow() + }.onFailure { throwable -> + Timber.e("Error occurred while retrieving resource via URL $currentUrl", throwable) + if (enableCustomSyncWorkerLogs) CustomWorkerState.postState(CustomSyncState.Failed(throwable.localizedMessage)) + }.getOrThrow() - val nextPageUrl = resultBundle.getLink(PAGINATION_NEXT)?.url + val fetchedResources = resultBundle.entry.size + totalProcessedResources += fetchedResources - processResultBundleEntries(resultBundle.entry) + Timber.d("Page $currentPage fetched $fetchedResources resources. Total processed: $totalProcessedResources") - if (!nextPageUrl.isNullOrEmpty()) { - fetchResources( - gatewayModeHeaderValue = gatewayModeHeaderValue, - url = nextPageUrl, - ) + // Check for the next page URL + nextPageUrl = resultBundle.getLink(PAGINATION_NEXT)?.url + + if (nextPageUrl.isNullOrEmpty()) { + Timber.d("No more pages to fetch. Fetching completed.") + if (enableCustomSyncWorkerLogs) CustomWorkerState.postState(CustomSyncState.Success) + } } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncState.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncState.kt new file mode 100644 index 00000000000..7fa681a8dee --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncState.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.engine.sync + +sealed class CustomSyncState { + object InProgress : CustomSyncState() + + object Success : CustomSyncState() + + data class Failed(val error: String? = null) : CustomSyncState() + + object Idle : CustomSyncState() +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt index 00c66ddce49..00e138b51d6 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import androidx.work.workDataOf import com.google.android.fhir.sync.concatParams import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -56,6 +57,7 @@ constructor( fetchResources( gatewayModeHeaderValue = ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE, url = url, + enableCustomSyncWorkerLogs = true ) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt new file mode 100644 index 00000000000..ded9fc76999 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.engine.sync + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +object CustomWorkerState { + private val _workerState = MutableStateFlow(CustomSyncState.Idle) + val workerState: StateFlow = _workerState + + fun postState(state: CustomSyncState) { + _workerState.value = state + } +} \ No newline at end of file diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt index ed921891e89..19eb5110bd1 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt @@ -34,14 +34,10 @@ import com.google.android.fhir.FhirEngine import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.fhir.sync.SyncJobStatus import dagger.hilt.android.lifecycle.HiltViewModel -import java.text.SimpleDateFormat -import java.time.OffsetDateTime -import java.util.Date -import java.util.Locale -import java.util.TimeZone -import javax.inject.Inject -import kotlin.time.Duration import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -58,7 +54,9 @@ import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger import org.smartregister.fhircore.engine.data.local.register.RegisterRepository import org.smartregister.fhircore.engine.domain.model.LauncherType import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction +import org.smartregister.fhircore.engine.sync.CustomSyncState import org.smartregister.fhircore.engine.sync.CustomSyncWorker +import org.smartregister.fhircore.engine.sync.CustomWorkerState import org.smartregister.fhircore.engine.sync.SyncBroadcaster import org.smartregister.fhircore.engine.task.FhirCarePlanGenerator import org.smartregister.fhircore.engine.task.FhirCompleteCarePlanWorker @@ -88,6 +86,14 @@ import org.smartregister.fhircore.quest.ui.shared.models.AppDrawerUIState import org.smartregister.fhircore.quest.ui.shared.models.QuestionnaireSubmission import org.smartregister.fhircore.quest.util.extensions.handleClickEvent import org.smartregister.fhircore.quest.util.extensions.schedulePeriodically +import timber.log.Timber +import java.text.SimpleDateFormat +import java.time.OffsetDateTime +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import javax.inject.Inject +import kotlin.time.Duration @HiltViewModel class AppMainViewModel @@ -133,6 +139,32 @@ constructor( configurationRegistry.retrieveConfigurations(ConfigType.MeasureReport) } + private val _customSyncState = MutableStateFlow(CustomSyncState.Idle) + val customSyncState: StateFlow = _customSyncState.asStateFlow() + + init { + observeWorkerSyncState() + } + + private fun observeWorkerSyncState() { + viewModelScope.launch { + CustomWorkerState.workerState.collect { syncState -> + when (syncState) { + CustomSyncState.InProgress -> { + Timber.d("Custom Sync Worker InProgress") + } + CustomSyncState.Failed() -> { + Timber.d("Custom Sync Worker Failed") + } + CustomSyncState.Success -> { + Timber.d("Custom Sync Worker Finished") + } + else -> {} + } + } + } + } + fun retrieveAppMainUiState(refreshAll: Boolean = true) { if (refreshAll) { appMainUiState.value = From 080443c5dbaccd00e5b18e14b62419e816447c84 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Tue, 10 Dec 2024 08:02:56 +0300 Subject: [PATCH 02/16] [Enahncement] Monitor custom sync status. Signed-off-by: Lentumunai-Mark --- .../configuration/ConfigurationRegistry.kt | 67 ++++----- .../fhircore/engine/sync/CustomSyncWorker.kt | 3 +- .../fhircore/engine/sync/CustomWorkerState.kt | 12 +- .../ui/geowidget/GeoWidgetLauncherFragment.kt | 12 +- .../ui/geowidget/GeoWidgetLauncherScreen.kt | 3 + .../quest/ui/main/AppMainViewModel.kt | 35 +++-- .../quest/ui/register/RegisterScreen.kt | 3 + .../ui/shared/components/SyncStatusView.kt | 138 ++++++++++-------- 8 files changed, 150 insertions(+), 123 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index b4f9d77f8a2..b7413f7e8ac 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -531,47 +531,48 @@ constructor( suspend fun fetchResources( gatewayModeHeaderValue: String? = null, url: String, - enableCustomSyncWorkerLogs: Boolean = false + enableCustomSyncWorkerLogs: Boolean = false, ) { - var currentPage = 0 - var totalProcessedResources = 0 - - var nextPageUrl: String? = url - while (!nextPageUrl.isNullOrEmpty()) { - val currentUrl = nextPageUrl // Create an immutable reference for this iteration - - currentPage++ - - Timber.d("Fetching page $currentPage with URL: $currentUrl") - if (enableCustomSyncWorkerLogs) CustomWorkerState.postState(CustomSyncState.InProgress) + runCatching { + if (enableCustomSyncWorkerLogs) { + Timber.d("Posting state: InProgress") + CustomWorkerState.postState(CustomSyncState.InProgress) + } - val resultBundle = runCatching { + Timber.d("Fetching page with URL: $url") if (gatewayModeHeaderValue.isNullOrEmpty()) { - fhirResourceDataSource.getResource(currentUrl) + fhirResourceDataSource.getResource(url) } else { - fhirResourceDataSource.getResourceWithGatewayModeHeader( - gatewayModeHeaderValue, - currentUrl - ) + fhirResourceDataSource.getResourceWithGatewayModeHeader(gatewayModeHeaderValue, url) } - }.onFailure { throwable -> - Timber.e("Error occurred while retrieving resource via URL $currentUrl", throwable) - if (enableCustomSyncWorkerLogs) CustomWorkerState.postState(CustomSyncState.Failed(throwable.localizedMessage)) - }.getOrThrow() - - val fetchedResources = resultBundle.entry.size - totalProcessedResources += fetchedResources - - Timber.d("Page $currentPage fetched $fetchedResources resources. Total processed: $totalProcessedResources") + } + .onFailure { throwable -> + Timber.e("Error occurred while retrieving resource via URL $url", throwable) + if (enableCustomSyncWorkerLogs) { + Timber.d("Posting state: Failed") + CustomWorkerState.postState(CustomSyncState.Failed(throwable.localizedMessage)) + } + return // Exit on failure + } + .onSuccess { resultBundle -> + val nextPageUrl = resultBundle.getLink(PAGINATION_NEXT)?.url - // Check for the next page URL - nextPageUrl = resultBundle.getLink(PAGINATION_NEXT)?.url + processResultBundleEntries(resultBundle.entry) - if (nextPageUrl.isNullOrEmpty()) { - Timber.d("No more pages to fetch. Fetching completed.") - if (enableCustomSyncWorkerLogs) CustomWorkerState.postState(CustomSyncState.Success) + if (!nextPageUrl.isNullOrEmpty()) { + fetchResources( + gatewayModeHeaderValue = gatewayModeHeaderValue, + url = nextPageUrl, + enableCustomSyncWorkerLogs = enableCustomSyncWorkerLogs, + ) + } else { + Timber.d("No more pages to fetch. Fetching completed.") + if (enableCustomSyncWorkerLogs) { + Timber.d("Posting state: Success") + CustomWorkerState.postState(CustomSyncState.Success) + } + } } - } } private suspend fun processResultBundleEntries( diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt index 00e138b51d6..6f2ab0d0a15 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt @@ -20,7 +20,6 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import androidx.work.workDataOf import com.google.android.fhir.sync.concatParams import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -57,7 +56,7 @@ constructor( fetchResources( gatewayModeHeaderValue = ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE, url = url, - enableCustomSyncWorkerLogs = true + enableCustomSyncWorkerLogs = true, ) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt index ded9fc76999..7248a9d9ee5 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt @@ -20,10 +20,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow object CustomWorkerState { - private val _workerState = MutableStateFlow(CustomSyncState.Idle) - val workerState: StateFlow = _workerState + private val _workerState = MutableStateFlow(CustomSyncState.Idle) + val workerState: StateFlow = _workerState - fun postState(state: CustomSyncState) { - _workerState.value = state - } -} \ No newline at end of file + fun postState(state: CustomSyncState) { + _workerState.value = state + } +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt index 2eab074a82e..8a71a7bf2c1 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView @@ -119,6 +120,7 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { val scaffoldState = rememberScaffoldState() val uiState: AppMainUiState = appMainViewModel.appMainUiState.value val appDrawerUIState = appMainViewModel.appDrawerUiState.value + val customSyncState = appMainViewModel.customSyncState.collectAsState().value val openDrawer: (Boolean) -> Unit = { open: Boolean -> coroutineScope.launch { if (open) scaffoldState.drawerState.open() else scaffoldState.drawerState.close() @@ -192,6 +194,7 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { appDrawerUIState = appDrawerUIState, clearMapLiveData = geoWidgetLauncherViewModel.clearMapLiveData, geoJsonFeatures = geoWidgetLauncherViewModel.geoJsonFeatures, + customSyncState = customSyncState, launchQuestionnaire = geoWidgetLauncherViewModel::launchQuestionnaire, decodeImage = geoWidgetLauncherViewModel::getImageBitmap, onAppMainEvent = appMainViewModel::onEvent, @@ -222,12 +225,11 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { ) } } - is CurrentSyncJobStatus.Succeeded, - is CurrentSyncJobStatus.Failed, -> { + is CurrentSyncJobStatus.Succeeded -> { + appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) + } + is CurrentSyncJobStatus.Failed -> { appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) - if (syncJobStatus is CurrentSyncJobStatus.Succeeded) { - geoWidgetLauncherViewModel.onEvent(GeoWidgetEvent.ClearMap) - } geoWidgetLauncherViewModel.onEvent( GeoWidgetEvent.RetrieveFeatures( geoWidgetConfig = geoWidgetConfiguration, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt index 86dc61b6bcb..497439586b9 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt @@ -36,6 +36,7 @@ import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation +import org.smartregister.fhircore.engine.sync.CustomSyncState import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.geowidget.screens.GeoWidgetFragment @@ -62,6 +63,7 @@ fun GeoWidgetLauncherScreen( appDrawerUIState: AppDrawerUIState, clearMapLiveData: MutableLiveData, geoJsonFeatures: MutableLiveData>, + customSyncState: CustomSyncState = CustomSyncState.Idle, launchQuestionnaire: (QuestionnaireConfig, GeoJsonFeature, Context) -> Unit, decodeImage: ((String) -> Bitmap?)?, onAppMainEvent: (AppMainEvent) -> Unit, @@ -114,6 +116,7 @@ fun GeoWidgetLauncherScreen( appDrawerUIState = appDrawerUIState, onAppMainEvent = onAppMainEvent, openDrawer = openDrawer, + customSyncState = customSyncState, ) }, ) { innerPadding -> diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt index 19eb5110bd1..c1e37add9fa 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt @@ -34,6 +34,13 @@ import com.google.android.fhir.FhirEngine import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.fhir.sync.SyncJobStatus import dagger.hilt.android.lifecycle.HiltViewModel +import java.text.SimpleDateFormat +import java.time.OffsetDateTime +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import javax.inject.Inject +import kotlin.time.Duration import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -87,13 +94,6 @@ import org.smartregister.fhircore.quest.ui.shared.models.QuestionnaireSubmission import org.smartregister.fhircore.quest.util.extensions.handleClickEvent import org.smartregister.fhircore.quest.util.extensions.schedulePeriodically import timber.log.Timber -import java.text.SimpleDateFormat -import java.time.OffsetDateTime -import java.util.Date -import java.util.Locale -import java.util.TimeZone -import javax.inject.Inject -import kotlin.time.Duration @HiltViewModel class AppMainViewModel @@ -150,15 +150,18 @@ constructor( viewModelScope.launch { CustomWorkerState.workerState.collect { syncState -> when (syncState) { - CustomSyncState.InProgress -> { - Timber.d("Custom Sync Worker InProgress") - } - CustomSyncState.Failed() -> { - Timber.d("Custom Sync Worker Failed") - } - CustomSyncState.Success -> { - Timber.d("Custom Sync Worker Finished") - } + CustomSyncState.InProgress -> { + Timber.d("Custom Sync Worker InProgress") + _customSyncState.value = CustomSyncState.InProgress + } + CustomSyncState.Failed() -> { + Timber.d("Custom Sync Worker Failed") + _customSyncState.value = CustomSyncState.Failed("") + } + CustomSyncState.Success -> { + _customSyncState.value = CustomSyncState.Success + Timber.d("Custom Sync Worker Finished") + } else -> {} } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt index 4db108fa4f7..186ef9ccad2 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt @@ -61,6 +61,7 @@ import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig import org.smartregister.fhircore.engine.domain.model.ResourceConfig import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation +import org.smartregister.fhircore.engine.sync.CustomSyncState import org.smartregister.fhircore.engine.ui.components.register.LoaderDialog import org.smartregister.fhircore.engine.ui.components.register.RegisterHeader import org.smartregister.fhircore.engine.ui.theme.AppTheme @@ -100,6 +101,7 @@ fun RegisterScreen( currentPage: MutableState, pagingItems: LazyPagingItems, navController: NavController, + customSyncState: CustomSyncState = CustomSyncState.Idle, toolBarHomeNavigation: ToolBarHomeNavigation = ToolBarHomeNavigation.OPEN_DRAWER, decodeImage: ((String) -> Bitmap?)?, ) { @@ -166,6 +168,7 @@ fun RegisterScreen( appDrawerUIState = appDrawerUIState, onAppMainEvent = onAppMainEvent, openDrawer = openDrawer, + customSyncState = customSyncState, ) }, ) { innerPadding -> diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt index 423323e660a..69894be9b88 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt @@ -63,9 +63,9 @@ import java.time.OffsetDateTime import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.smartregister.fhircore.engine.sync.CustomSyncState import org.smartregister.fhircore.engine.ui.theme.AppTheme import org.smartregister.fhircore.engine.ui.theme.DangerColor -import org.smartregister.fhircore.engine.ui.theme.DefaultColor import org.smartregister.fhircore.engine.ui.theme.SubtitleTextColor import org.smartregister.fhircore.engine.ui.theme.SuccessColor import org.smartregister.fhircore.engine.ui.theme.SyncBarBackgroundColor @@ -73,6 +73,7 @@ import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundEx import org.smartregister.fhircore.quest.ui.main.AppMainEvent import org.smartregister.fhircore.quest.ui.shared.models.AppDrawerUIState import org.smartregister.fhircore.quest.util.extensions.conditional +import timber.log.Timber const val TRANSPARENCY = 0.2f const val SYNC_PROGRESS_INDICATOR_TEST_TAG = "syncProgressIndicatorTestTag" @@ -81,13 +82,18 @@ const val SYNC_PROGRESS_INDICATOR_TEST_TAG = "syncProgressIndicatorTestTag" fun SyncBottomBar( isFirstTimeSync: Boolean, appDrawerUIState: AppDrawerUIState, + customSyncState: CustomSyncState, onAppMainEvent: (AppMainEvent) -> Unit, openDrawer: (Boolean) -> Unit, ) { val coroutineScope = rememberCoroutineScope() val currentSyncJobStatus = appDrawerUIState.currentSyncJobStatus val hideSyncCompleteStatus = remember { mutableStateOf(false) } - if (currentSyncJobStatus is CurrentSyncJobStatus.Succeeded) { + + if ( + currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && + customSyncState is CustomSyncState.Success + ) { LaunchedEffect(Unit) { coroutineScope.launch { delay(7.seconds) @@ -95,13 +101,17 @@ fun SyncBottomBar( } } } + val syncBackgroundColor = - when (currentSyncJobStatus) { - is CurrentSyncJobStatus.Failed -> DangerColor.copy(alpha = 0.2f) - is CurrentSyncJobStatus.Succeeded -> SuccessColor.copy(alpha = 0.2f) - is CurrentSyncJobStatus.Running -> SyncBarBackgroundColor + when { + currentSyncJobStatus is CurrentSyncJobStatus.Failed || + customSyncState is CustomSyncState.Failed -> DangerColor.copy(alpha = 0.2f) + currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && + customSyncState is CustomSyncState.Success -> SuccessColor.copy(alpha = 0.2f) + currentSyncJobStatus is CurrentSyncJobStatus.Running -> SyncBarBackgroundColor else -> Color.Transparent } + var syncNotificationBarExpanded by remember { mutableStateOf(true) } val bottomRadius = if (!hideSyncCompleteStatus.value || currentSyncJobStatus is CurrentSyncJobStatus.Running) { @@ -115,12 +125,16 @@ fun SyncBottomBar( if (currentSyncJobStatus is CurrentSyncJobStatus.Running) 114.dp else 80.dp else -> 60.dp } + if ( !isFirstTimeSync && currentSyncJobStatus != null && (currentSyncJobStatus is CurrentSyncJobStatus.Running || - currentSyncJobStatus is CurrentSyncJobStatus.Failed || - (!hideSyncCompleteStatus.value && currentSyncJobStatus is CurrentSyncJobStatus.Succeeded)) + (currentSyncJobStatus is CurrentSyncJobStatus.Failed || + customSyncState is CustomSyncState.Failed) || + (!hideSyncCompleteStatus.value && + currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && + customSyncState is CustomSyncState.Success)) ) { Box( modifier = @@ -154,9 +168,11 @@ fun SyncBottomBar( }, contentDescription = null, tint = - when (currentSyncJobStatus) { - is CurrentSyncJobStatus.Failed -> DangerColor - is CurrentSyncJobStatus.Succeeded -> SuccessColor + when { + currentSyncJobStatus is CurrentSyncJobStatus.Failed || + customSyncState is CustomSyncState.Failed -> DangerColor + currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && + customSyncState is CustomSyncState.Success -> SuccessColor else -> Color.White }, modifier = Modifier.size(16.dp), @@ -168,34 +184,39 @@ fun SyncBottomBar( contentAlignment = Alignment.Center, ) { val context = LocalContext.current - when (currentSyncJobStatus) { - is CurrentSyncJobStatus.Running -> { + when { + currentSyncJobStatus is CurrentSyncJobStatus.Running -> { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, progressPercentage = appDrawerUIState.percentageProgress, + customSyncState = customSyncState, onCancel = { onAppMainEvent(AppMainEvent.CancelSyncData(context)) }, ) SideEffect { hideSyncCompleteStatus.value = false } } - is CurrentSyncJobStatus.Failed -> { + currentSyncJobStatus is CurrentSyncJobStatus.Failed || + customSyncState is CustomSyncState.Failed -> { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, + customSyncState = customSyncState, onRetry = { openDrawer(false) onAppMainEvent(AppMainEvent.SyncData(context)) }, ) } - is CurrentSyncJobStatus.Succeeded -> { + currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && + customSyncState is CustomSyncState.Success -> { if (!hideSyncCompleteStatus.value) { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, + customSyncState = customSyncState, ) } } @@ -212,15 +233,29 @@ fun SyncBottomBar( fun SyncStatusView( isSyncUpload: Boolean?, currentSyncJobStatus: CurrentSyncJobStatus?, + customSyncState: CustomSyncState = CustomSyncState.Idle, progressPercentage: Int? = null, minimized: Boolean = false, onRetry: () -> Unit = {}, onCancel: () -> Unit = {}, ) { val height = - if (minimized) { - 36.dp - } else if (currentSyncJobStatus is CurrentSyncJobStatus.Running) 88.dp else 56.dp + when { + minimized -> 36.dp + currentSyncJobStatus is CurrentSyncJobStatus.Running -> 88.dp + else -> 56.dp + } + + val isSucceeded = + currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && + customSyncState is CustomSyncState.Success + val isFailed = + currentSyncJobStatus is CurrentSyncJobStatus.Failed || customSyncState is CustomSyncState.Failed + + Timber.d("current sync status evaluates to : $isSucceeded") + Timber.d("current custom Sync state is $customSyncState") + Timber.d("current fhir sync state is $currentSyncJobStatus") + Row( modifier = Modifier.height(height) @@ -231,35 +266,27 @@ fun SyncStatusView( .conditional(minimized, { padding(vertical = 4.dp) }, { padding(vertical = 16.dp) }), verticalAlignment = Alignment.CenterVertically, ) { - if ( - (currentSyncJobStatus is CurrentSyncJobStatus.Failed || - currentSyncJobStatus is CurrentSyncJobStatus.Succeeded) - ) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) { + if (isSucceeded || isFailed) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f), + ) { if (!minimized) { Icon( - imageVector = - if (currentSyncJobStatus is CurrentSyncJobStatus.Succeeded) { - Icons.Default.CheckCircle - } else { - Icons.Default.Error - }, + imageVector = if (isSucceeded) Icons.Default.CheckCircle else Icons.Default.Error, contentDescription = null, - tint = - when (currentSyncJobStatus) { - is CurrentSyncJobStatus.Failed -> DangerColor - is CurrentSyncJobStatus.Succeeded -> SuccessColor - else -> DefaultColor - }, + tint = if (isFailed) DangerColor else SuccessColor, ) } SyncStatusTitle( text = - if (currentSyncJobStatus is CurrentSyncJobStatus.Succeeded) { - stringResource(org.smartregister.fhircore.engine.R.string.sync_complete) - } else { - stringResource(org.smartregister.fhircore.engine.R.string.sync_error) - }, + stringResource( + if (isSucceeded) { + org.smartregister.fhircore.engine.R.string.sync_complete + } else { + org.smartregister.fhircore.engine.R.string.sync_error + }, + ), minimized = minimized, startPadding = if (minimized) 0 else 16, ) @@ -304,28 +331,17 @@ fun SyncStatusView( } } } - - if ( - (currentSyncJobStatus is CurrentSyncJobStatus.Failed || - currentSyncJobStatus is CurrentSyncJobStatus.Running) && !minimized - ) { + if (isFailed && !minimized) { Text( - text = - stringResource( - if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { - org.smartregister.fhircore.engine.R.string.retry - } else { - org.smartregister.fhircore.engine.R.string.cancel - }, - ), - modifier = - Modifier.padding(start = 16.dp).clickable { - if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { - onRetry() - } else { - onCancel() - } - }, + text = stringResource(org.smartregister.fhircore.engine.R.string.retry), + modifier = Modifier.padding(start = 16.dp).clickable { onRetry() }, + color = MaterialTheme.colors.primary, + fontWeight = FontWeight.SemiBold, + ) + } else if (currentSyncJobStatus is CurrentSyncJobStatus.Running && !minimized) { + Text( + text = stringResource(org.smartregister.fhircore.engine.R.string.cancel), + modifier = Modifier.padding(start = 16.dp).clickable { onCancel() }, color = MaterialTheme.colors.primary, fontWeight = FontWeight.SemiBold, ) From 94f9daf0f6cc684b1814c948184c949f21712421 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Tue, 10 Dec 2024 08:14:59 +0300 Subject: [PATCH 03/16] Fix failing tests. Signed-off-by: Lentumunai-Mark --- .../quest/integration/ui/register/RegisterScreenTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt index 4f06616a7f3..4c879a2042a 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt @@ -60,6 +60,7 @@ import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger import org.smartregister.fhircore.engine.domain.model.ActionConfig import org.smartregister.fhircore.engine.domain.model.Language import org.smartregister.fhircore.engine.domain.model.ResourceData +import org.smartregister.fhircore.engine.sync.CustomSyncState import org.smartregister.fhircore.quest.integration.Faker import org.smartregister.fhircore.quest.ui.main.appMainUiStateOf import org.smartregister.fhircore.quest.ui.register.FAB_BUTTON_REGISTER_TEST_TAG @@ -610,6 +611,7 @@ class RegisterScreenTest { currentSyncJobStatus = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), ), onAppMainEvent = {}, + customSyncState = CustomSyncState.Success, searchQuery = searchText, currentPage = currentPage, pagingItems = pagingItems, From 4646b49fd3ec9f2c54ef678810ab41fb4a1708f6 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Fri, 13 Dec 2024 13:31:10 +0300 Subject: [PATCH 04/16] Harmonize sync status files. Signed-off-by: Lentumunai-Mark --- .../ui/shared/components/SyncStatusView.kt | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt index bb0f570c423..6475b2443e8 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt @@ -354,48 +354,49 @@ fun SyncStatusView( fontWeight = FontWeight.SemiBold, ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = 16.dp), - ) { - if (currentSyncJobStatus is CurrentSyncJobStatus.Running) { - LineSpinFadeLoaderProgressIndicator( - color = Color.White, - lineLength = 8f, - innerRadius = 12f, - ) - } - if ( - (currentSyncJobStatus is CurrentSyncJobStatus.Failed || - currentSyncJobStatus is CurrentSyncJobStatus.Running) && !minimized + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 16.dp), ) { - Text( - text = - stringResource( - if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { - org.smartregister.fhircore.engine.R.string.retry - } else { - org.smartregister.fhircore.engine.R.string.cancel + if (currentSyncJobStatus is CurrentSyncJobStatus.Running) { + LineSpinFadeLoaderProgressIndicator( + color = Color.White, + lineLength = 8f, + innerRadius = 12f, + ) + } + if ( + (currentSyncJobStatus is CurrentSyncJobStatus.Failed || + currentSyncJobStatus is CurrentSyncJobStatus.Running) && !minimized + ) { + Text( + text = + stringResource( + if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { + org.smartregister.fhircore.engine.R.string.retry + } else { + org.smartregister.fhircore.engine.R.string.cancel + }, + ), + modifier = + Modifier.padding(start = 16.dp).clickable { + if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { + onRetry() + } else { + onCancel() + } }, - ), - modifier = - Modifier.padding(start = 16.dp).clickable { - if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { - onRetry() - } else { - onCancel() - } - }, - color = MaterialTheme.colors.primary, - fontWeight = FontWeight.SemiBold, - ) + color = MaterialTheme.colors.primary, + fontWeight = FontWeight.SemiBold, + ) + } } } } } @Composable -private fun SyncStatusTitle( +fun SyncStatusTitle( text: String, color: Color = Color.Unspecified, minimized: Boolean, From d66652c388eeb2b6a5e00603b210e970c9ca2a85 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Fri, 13 Dec 2024 17:06:47 +0300 Subject: [PATCH 05/16] Chain custom worker to fhir sync worker and clean up sync view. Signed-off-by: Lentumunai-Mark --- .../fhircore/engine/sync/SyncBroadcaster.kt | 50 ++++++------ .../quest/ui/main/AppMainViewModel.kt | 7 -- .../ui/shared/components/SyncStatusView.kt | 81 ++++++++----------- 3 files changed, 58 insertions(+), 80 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt index 59c9413d934..0556eef2d0e 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt @@ -17,19 +17,16 @@ package org.smartregister.fhircore.engine.sync import android.content.Context -import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.google.android.fhir.FhirEngine -import com.google.android.fhir.sync.BackoffCriteria import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.fhir.sync.LastSyncJobStatus import com.google.android.fhir.sync.PeriodicSyncConfiguration import com.google.android.fhir.sync.PeriodicSyncJobStatus import com.google.android.fhir.sync.RepeatInterval -import com.google.android.fhir.sync.RetryConfiguration import com.google.android.fhir.sync.Sync import com.google.android.fhir.sync.SyncJobStatus import com.google.android.fhir.sync.download.ResourceParamsBasedDownloadWorkManager @@ -80,11 +77,6 @@ constructor( .setConstraints( Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), ) - .setBackoffCriteria( - BackoffPolicy.LINEAR, - 10, - TimeUnit.SECONDS, - ) .build(), ) } @@ -103,16 +95,6 @@ constructor( syncConstraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), repeat = RepeatInterval(interval = interval, timeUnit = TimeUnit.MINUTES), - retryConfiguration = - RetryConfiguration( - backoffCriteria = - BackoffCriteria( - backoffDelay = 10, - timeUnit = TimeUnit.SECONDS, - backoffPolicy = BackoffPolicy.EXPONENTIAL, - ), - maxRetries = 3, - ), ), ) .handlePeriodicSyncJobStatus(this) @@ -121,14 +103,34 @@ constructor( private fun Flow.handlePeriodicSyncJobStatus( coroutineScope: CoroutineScope, ) { - this.onEach { + this.onEach { status -> syncListenerManager.onSyncListeners.forEach { onSyncListener -> - onSyncListener.onSync( - if (it.lastSyncJobStatus as? LastSyncJobStatus.Succeeded != null) { - CurrentSyncJobStatus.Succeeded((it.lastSyncJobStatus as LastSyncJobStatus).timestamp) + Timber.d("fhir sync worker...") + + // Check if lastSyncJobStatus is not null and is of type Succeeded + val syncStatus = + if (status.lastSyncJobStatus as? LastSyncJobStatus.Succeeded != null) { + CurrentSyncJobStatus.Succeeded( + (status.lastSyncJobStatus as LastSyncJobStatus.Succeeded).timestamp, + ) } else { - it.currentSyncJobStatus - }, + status.currentSyncJobStatus + } + + onSyncListener.onSync(syncStatus) + } + + if ( + status.currentSyncJobStatus is CurrentSyncJobStatus.Succeeded || + status.lastSyncJobStatus is LastSyncJobStatus.Succeeded + ) { + Timber.d("Periodic sync succeeded. Triggering CustomSyncWorker...") + workManager.enqueue( + OneTimeWorkRequestBuilder() + .setConstraints( + Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), + ) + .build(), ) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt index fd6c4cb2aaf..e6ab47b9bdd 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt @@ -63,7 +63,6 @@ import org.smartregister.fhircore.engine.data.local.register.RegisterRepository import org.smartregister.fhircore.engine.domain.model.LauncherType import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction import org.smartregister.fhircore.engine.sync.CustomSyncState -import org.smartregister.fhircore.engine.sync.CustomSyncWorker import org.smartregister.fhircore.engine.sync.CustomWorkerState import org.smartregister.fhircore.engine.sync.SyncBroadcaster import org.smartregister.fhircore.engine.task.FhirCarePlanGenerator @@ -503,12 +502,6 @@ constructor( initialDelay = INITIAL_DELAY, ) - schedulePeriodically( - workId = CustomSyncWorker.WORK_ID, - repeatInterval = applicationConfiguration.syncInterval, - initialDelay = 0, - ) - measureReportConfigurations.forEach { measureReportConfig -> measureReportConfig.scheduledGenerationDuration?.let { scheduledGenerationDuration -> schedulePeriodically( diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt index 6475b2443e8..56d83bf4fef 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt @@ -339,57 +339,39 @@ fun SyncStatusView( } } } - if (isFailed && !minimized) { - Text( - text = stringResource(org.smartregister.fhircore.engine.R.string.retry), - modifier = Modifier.padding(start = 16.dp).clickable { onRetry() }, - color = MaterialTheme.colors.primary, - fontWeight = FontWeight.SemiBold, - ) - } else if (currentSyncJobStatus is CurrentSyncJobStatus.Running && !minimized) { - Text( - text = stringResource(org.smartregister.fhircore.engine.R.string.cancel), - modifier = Modifier.padding(start = 16.dp).clickable { onCancel() }, - color = MaterialTheme.colors.primary, - fontWeight = FontWeight.SemiBold, - ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = 16.dp), - ) { - if (currentSyncJobStatus is CurrentSyncJobStatus.Running) { - LineSpinFadeLoaderProgressIndicator( - color = Color.White, - lineLength = 8f, - innerRadius = 12f, - ) - } - if ( - (currentSyncJobStatus is CurrentSyncJobStatus.Failed || - currentSyncJobStatus is CurrentSyncJobStatus.Running) && !minimized - ) { - Text( - text = - stringResource( - if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { - org.smartregister.fhircore.engine.R.string.retry - } else { - org.smartregister.fhircore.engine.R.string.cancel - }, - ), - modifier = - Modifier.padding(start = 16.dp).clickable { - if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { - onRetry() - } else { - onCancel() - } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 16.dp), + ) { + if (currentSyncJobStatus is CurrentSyncJobStatus.Running) { + LineSpinFadeLoaderProgressIndicator( + color = Color.White, + lineLength = 8f, + innerRadius = 12f, + ) + } + if ((isFailed || currentSyncJobStatus is CurrentSyncJobStatus.Running) && !minimized) { + Text( + text = + stringResource( + if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { + org.smartregister.fhircore.engine.R.string.retry + } else { + org.smartregister.fhircore.engine.R.string.cancel }, - color = MaterialTheme.colors.primary, - fontWeight = FontWeight.SemiBold, - ) - } + ), + modifier = + Modifier.padding(start = 16.dp).clickable { + if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { + onRetry() + } else { + onCancel() + } + }, + color = MaterialTheme.colors.primary, + fontWeight = FontWeight.SemiBold, + ) } } } @@ -419,6 +401,7 @@ fun SyncStatusSucceededPreview() { SyncStatusView( isSyncUpload = false, currentSyncJobStatus = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), + customSyncState = CustomSyncState.Success, ) } } From 98dad1809dd5b6752cc2e7ab04ed07566e8c25e7 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Mon, 16 Dec 2024 10:26:07 +0300 Subject: [PATCH 06/16] run cutom sync once the last fhir sync job completes. Signed-off-by: Lentumunai-Mark --- .../smartregister/fhircore/engine/sync/SyncBroadcaster.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt index 0556eef2d0e..4b965583839 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt @@ -120,10 +120,7 @@ constructor( onSyncListener.onSync(syncStatus) } - if ( - status.currentSyncJobStatus is CurrentSyncJobStatus.Succeeded || - status.lastSyncJobStatus is LastSyncJobStatus.Succeeded - ) { + if (status.lastSyncJobStatus is LastSyncJobStatus.Succeeded) { Timber.d("Periodic sync succeeded. Triggering CustomSyncWorker...") workManager.enqueue( OneTimeWorkRequestBuilder() From 091fd8e25ec22cf183c0ff9fa1695fa163eaad82 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Mon, 16 Dec 2024 16:04:16 +0300 Subject: [PATCH 07/16] add custom sync state on the register fragment. Signed-off-by: Lentumunai-Mark --- .../fhircore/quest/ui/register/RegisterFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt index 9e6aff8fb8d..3a4c959064d 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt @@ -102,6 +102,7 @@ class RegisterFragment : Fragment(), OnSyncListener { val scope = rememberCoroutineScope() val scaffoldState = rememberScaffoldState() val uiState: AppMainUiState = appMainViewModel.appMainUiState.value + val customSyncState = appMainViewModel.customSyncState.collectAsState().value val openDrawer: (Boolean) -> Unit = { open: Boolean -> scope.launch { if (open) scaffoldState.drawerState.open() else scaffoldState.drawerState.close() @@ -172,6 +173,7 @@ class RegisterFragment : Fragment(), OnSyncListener { registerUiState = registerViewModel.registerUiState.value, registerUiCountState = registerViewModel.registerUiCountState.value, appDrawerUIState = appMainViewModel.appDrawerUiState.value, + customSyncState = customSyncState, onAppMainEvent = { appMainViewModel.onEvent(it) }, searchQuery = searchViewModel.searchQuery, currentPage = registerViewModel.currentPage, From 936094e6d7b554944ce1a96c765d7e0590a84fe2 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Fri, 10 Jan 2025 12:00:25 +0300 Subject: [PATCH 08/16] Track fetching of cutom resources. Signed-off-by: Lentumunai-Mark --- .../configuration/ConfigurationRegistry.kt | 125 ++++++++++++++---- .../fhircore/engine/sync/CustomSyncState.kt | 27 ---- .../fhircore/engine/sync/CustomSyncWorker.kt | 56 ++++++-- .../fhircore/engine/sync/CustomWorkerState.kt | 29 ---- .../fhircore/engine/sync/SyncBroadcaster.kt | 47 +++---- .../ui/register/RegisterScreenTest.kt | 2 - .../ui/geowidget/GeoWidgetLauncherFragment.kt | 59 ++++++--- .../ui/geowidget/GeoWidgetLauncherScreen.kt | 3 - .../quest/ui/main/AppMainViewModel.kt | 42 +----- .../quest/ui/register/RegisterFragment.kt | 62 ++++++--- .../quest/ui/register/RegisterScreen.kt | 3 - .../ui/shared/components/SyncStatusView.kt | 115 +++++++--------- android/quest/src/main/res/values/strings.xml | 1 + 13 files changed, 303 insertions(+), 268 deletions(-) delete mode 100644 android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncState.kt delete mode 100644 android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index 4c868edf1c7..e49a49bceeb 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -24,16 +24,22 @@ import com.google.android.fhir.FhirEngine import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.get import com.google.android.fhir.knowledge.KnowledgeManager +import com.google.android.fhir.sync.CurrentSyncJobStatus +import com.google.android.fhir.sync.SyncJobStatus +import com.google.android.fhir.sync.SyncOperation import com.google.android.fhir.sync.download.ResourceSearchParams import dagger.hilt.android.qualifiers.ApplicationContext import java.io.FileNotFoundException import java.io.InputStreamReader import java.net.UnknownHostException +import java.time.OffsetDateTime import java.util.Locale import java.util.PropertyResourceBundle import java.util.ResourceBundle import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import okhttp3.RequestBody.Companion.toRequestBody @@ -57,8 +63,6 @@ import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource import org.smartregister.fhircore.engine.di.NetworkModule import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction -import org.smartregister.fhircore.engine.sync.CustomSyncState -import org.smartregister.fhircore.engine.sync.CustomWorkerState import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.KnowledgeManagerUtil import org.smartregister.fhircore.engine.util.SharedPreferenceKey @@ -100,12 +104,33 @@ constructor( val localizationHelper: LocalizationHelper by lazy { LocalizationHelper(this) } private val supportedFileExtensions = listOf("json", "properties") private var _isNonProxy = BuildConfig.IS_NON_PROXY_APK + private val _syncState = MutableSharedFlow() + val syncState: SharedFlow = _syncState + + suspend fun setSyncState(state: CurrentSyncJobStatus) = _syncState.emit(state) /** * Retrieve configuration for the provided [ConfigType]. The JSON retrieved from [configsJsonMap] * can be directly converted to a FHIR resource or hard coded custom model. The filtering assumes * you are passing data across screens, then later using it in DataQueries and to retrieve - * registerConfiguration. It is necessary to check that [paramsMap] is empty to confirm that the + * registerConfiguration. It is necessary to // private val _syncState = + * MutableSharedFlow() // val customSyncState: SharedFlow = + * _syncState // // private suspend fun setSyncState(state: SyncJobStatus) = + * _syncState.emit(state) + * + * // init { // observeWorkerSyncState() // } + * + * // private fun observeWorkerSyncState() { // viewModelScope.launch { // + * CustomWorkerState.workerState.collect { syncState -> // when (syncState) { // is + * CustomSyncState.InProgress -> { // Timber.d("Custom Sync Worker InProgress") // // val total = + * syncState.total // val completed = syncState.completed // val syncOperation = + * syncState.syncOperation // // // Set the current sync state with the extracted values // + * setSyncState( // SyncJobStatus.InProgress( // syncOperation = syncOperation, // total = total, + * // completed = completed, // ), // ) // } // is CustomSyncState.Failed -> { // Timber.d("Custom + * Sync Worker Failed") // setSyncState(SyncJobStatus.Failed(emptyList())) // } // + * CustomSyncState.Success -> { // Timber.d("Custom Sync Worker Succeeded") // + * setSyncState(SyncJobStatus.Succeeded()) // } // CustomSyncState.Idle -> { // Timber.d("Custom + * Sync Worker Idle") // } // } // } // } // }check that [paramsMap] is empty to confirm that the * params used in the DataQuery are passed when retrieving the configurations. * * @throws NoSuchElementException when the [configsJsonMap] doesn't contain a value for the @@ -525,17 +550,32 @@ constructor( return resultBundle } - suspend fun fetchResources( + suspend fun fetchCustomResources( gatewayModeHeaderValue: String? = null, url: String, - enableCustomSyncWorkerLogs: Boolean = false, + totalCustomRecords: Int = 0, + completedRecords: Int = 0, ) { - runCatching { - if (enableCustomSyncWorkerLogs) { - Timber.d("Posting state: InProgress") - CustomWorkerState.postState(CustomSyncState.InProgress) - } + if (completedRecords == 0) { + Timber.d("Setting state: Started") + setSyncState( + CurrentSyncJobStatus.Running( + SyncJobStatus.Started(), + ), + ) + } + runCatching { + Timber.d("Setting state: Running") + setSyncState( + CurrentSyncJobStatus.Running( + SyncJobStatus.InProgress( + syncOperation = SyncOperation.DOWNLOAD, + total = totalCustomRecords, + completed = completedRecords, + ), + ), + ) Timber.d("Fetching page with URL: $url") if (gatewayModeHeaderValue.isNullOrEmpty()) { fhirResourceDataSource.getResource(url) @@ -545,33 +585,72 @@ constructor( } .onFailure { throwable -> Timber.e("Error occurred while retrieving resource via URL $url", throwable) - if (enableCustomSyncWorkerLogs) { - Timber.d("Posting state: Failed") - CustomWorkerState.postState(CustomSyncState.Failed(throwable.localizedMessage)) - } - return // Exit on failure + Timber.d("Setting state: Failed") + setSyncState( + CurrentSyncJobStatus.Failed(OffsetDateTime.now()), + ) + return } .onSuccess { resultBundle -> - val nextPageUrl = resultBundle.getLink(PAGINATION_NEXT)?.url - processResultBundleEntries(resultBundle.entry) + val newCompletedRecords = completedRecords + resultBundle.entry.size + + Timber.d("Updating state: Running") + setSyncState( + CurrentSyncJobStatus.Running( + SyncJobStatus.InProgress( + syncOperation = SyncOperation.DOWNLOAD, + total = totalCustomRecords, + completed = newCompletedRecords, + ), + ), + ) + + val nextPageUrl = resultBundle.getLink(PAGINATION_NEXT)?.url if (!nextPageUrl.isNullOrEmpty()) { - fetchResources( + fetchCustomResources( gatewayModeHeaderValue = gatewayModeHeaderValue, url = nextPageUrl, - enableCustomSyncWorkerLogs = enableCustomSyncWorkerLogs, + totalCustomRecords = totalCustomRecords, + completedRecords = newCompletedRecords, // Pass the new value ) } else { Timber.d("No more pages to fetch. Fetching completed.") - if (enableCustomSyncWorkerLogs) { - Timber.d("Posting state: Success") - CustomWorkerState.postState(CustomSyncState.Success) - } + Timber.d("Setting state: Succeeded") + setSyncState( + CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), + ) } } } + private suspend fun fetchResources( + gatewayModeHeaderValue: String? = null, + url: String, + ) { + val resultBundle = + runCatching { + if (gatewayModeHeaderValue.isNullOrEmpty()) { + fhirResourceDataSource.getResource(url) + } else { + fhirResourceDataSource.getResourceWithGatewayModeHeader(gatewayModeHeaderValue, url) + } + } + .onFailure { throwable -> + Timber.e("Error occurred while retrieving resource via URL $url", throwable) + } + .getOrThrow() + val nextPageUrl = resultBundle.getLink(PAGINATION_NEXT)?.url + processResultBundleEntries(resultBundle.entry) + if (!nextPageUrl.isNullOrEmpty()) { + fetchResources( + gatewayModeHeaderValue = gatewayModeHeaderValue, + url = nextPageUrl, + ) + } + } + private suspend fun processResultBundleEntries( resultBundleEntries: List, ) { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncState.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncState.kt deleted file mode 100644 index 7fa681a8dee..00000000000 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncState.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2021-2024 Ona Systems, Inc - * - * 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 org.smartregister.fhircore.engine.sync - -sealed class CustomSyncState { - object InProgress : CustomSyncState() - - object Success : CustomSyncState() - - data class Failed(val error: String? = null) : CustomSyncState() - - object Idle : CustomSyncState() -} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt index 9572286f0b8..07467843bd7 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt @@ -20,10 +20,12 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.fhir.sync.concatParams import dagger.assisted.Assisted import dagger.assisted.AssistedInject import java.net.UnknownHostException +import java.time.OffsetDateTime import kotlinx.coroutines.withContext import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource @@ -42,23 +44,34 @@ constructor( val dispatcherProvider: DispatcherProvider, val fhirResourceDataSource: FhirResourceDataSource, ) : CoroutineWorker(appContext, workerParams) { + override suspend fun doWork(): Result { return withContext(dispatcherProvider.io()) { try { with(configurationRegistry) { val (resourceSearchParams, _) = loadResourceSearchParams() Timber.i("Custom resource sync parameters $resourceSearchParams") - resourceSearchParams - .asIterable() - .filter { it.value.isNotEmpty() } - .map { "${it.key}?${it.value.concatParams()}" } - .forEach { url -> - fetchResources( - gatewayModeHeaderValue = ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE, - url = url, - enableCustomSyncWorkerLogs = true, - ) - } + + // Process resource URLs + val resourceUrls = + resourceSearchParams + .asIterable() + .filter { it.value.isNotEmpty() } + .map { "${it.key}?${it.value.concatParams()}" } + + // Fetch summary count first + + val summaryCount = fetchSummaryCount(resourceUrls).values.sumOf { it ?: 0 } + Timber.d("Fetched summary count: $summaryCount") + + // Fetch resources + resourceUrls.forEach { url -> + fetchCustomResources( + gatewayModeHeaderValue = ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE, + url = url, + totalCustomRecords = summaryCount, + ) + } } Result.success() } catch (httpException: HttpException) { @@ -73,11 +86,32 @@ constructor( Result.failure() } catch (exception: Exception) { Timber.e(exception) + configurationRegistry.setSyncState( + CurrentSyncJobStatus.Failed(OffsetDateTime.now()), + ) Result.failure() } } } + /** Fetch summary counts for the provided URLs with summary=count query added. */ + private suspend fun fetchSummaryCount(resourceUrls: List): Map = + resourceUrls + .associate { url -> + // Modify URL to include 'summary=count' + val summaryUrl = "$url&summary=count" + val total: Int? = + runCatching { + // Explicitly fetch resource and ensure result can be cast to Bundle + fhirResourceDataSource.getResource(summaryUrl) + } + .onFailure { Timber.e(it, "Failed to fetch summary for $summaryUrl") } + .getOrNull() + ?.total + summaryUrl to total + } + .also { summaries -> Timber.i("Summary fetch results: $summaries") } + companion object { const val WORK_ID = "CustomResourceSyncWorker" } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt deleted file mode 100644 index 7248a9d9ee5..00000000000 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomWorkerState.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2021-2024 Ona Systems, Inc - * - * 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 org.smartregister.fhircore.engine.sync - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - -object CustomWorkerState { - private val _workerState = MutableStateFlow(CustomSyncState.Idle) - val workerState: StateFlow = _workerState - - fun postState(state: CustomSyncState) { - _workerState.value = state - } -} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt index 4b965583839..59c9413d934 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt @@ -17,16 +17,19 @@ package org.smartregister.fhircore.engine.sync import android.content.Context +import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.google.android.fhir.FhirEngine +import com.google.android.fhir.sync.BackoffCriteria import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.fhir.sync.LastSyncJobStatus import com.google.android.fhir.sync.PeriodicSyncConfiguration import com.google.android.fhir.sync.PeriodicSyncJobStatus import com.google.android.fhir.sync.RepeatInterval +import com.google.android.fhir.sync.RetryConfiguration import com.google.android.fhir.sync.Sync import com.google.android.fhir.sync.SyncJobStatus import com.google.android.fhir.sync.download.ResourceParamsBasedDownloadWorkManager @@ -77,6 +80,11 @@ constructor( .setConstraints( Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), ) + .setBackoffCriteria( + BackoffPolicy.LINEAR, + 10, + TimeUnit.SECONDS, + ) .build(), ) } @@ -95,6 +103,16 @@ constructor( syncConstraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), repeat = RepeatInterval(interval = interval, timeUnit = TimeUnit.MINUTES), + retryConfiguration = + RetryConfiguration( + backoffCriteria = + BackoffCriteria( + backoffDelay = 10, + timeUnit = TimeUnit.SECONDS, + backoffPolicy = BackoffPolicy.EXPONENTIAL, + ), + maxRetries = 3, + ), ), ) .handlePeriodicSyncJobStatus(this) @@ -103,31 +121,14 @@ constructor( private fun Flow.handlePeriodicSyncJobStatus( coroutineScope: CoroutineScope, ) { - this.onEach { status -> + this.onEach { syncListenerManager.onSyncListeners.forEach { onSyncListener -> - Timber.d("fhir sync worker...") - - // Check if lastSyncJobStatus is not null and is of type Succeeded - val syncStatus = - if (status.lastSyncJobStatus as? LastSyncJobStatus.Succeeded != null) { - CurrentSyncJobStatus.Succeeded( - (status.lastSyncJobStatus as LastSyncJobStatus.Succeeded).timestamp, - ) + onSyncListener.onSync( + if (it.lastSyncJobStatus as? LastSyncJobStatus.Succeeded != null) { + CurrentSyncJobStatus.Succeeded((it.lastSyncJobStatus as LastSyncJobStatus).timestamp) } else { - status.currentSyncJobStatus - } - - onSyncListener.onSync(syncStatus) - } - - if (status.lastSyncJobStatus is LastSyncJobStatus.Succeeded) { - Timber.d("Periodic sync succeeded. Triggering CustomSyncWorker...") - workManager.enqueue( - OneTimeWorkRequestBuilder() - .setConstraints( - Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), - ) - .build(), + it.currentSyncJobStatus + }, ) } } diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt index 4c879a2042a..4f06616a7f3 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt @@ -60,7 +60,6 @@ import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger import org.smartregister.fhircore.engine.domain.model.ActionConfig import org.smartregister.fhircore.engine.domain.model.Language import org.smartregister.fhircore.engine.domain.model.ResourceData -import org.smartregister.fhircore.engine.sync.CustomSyncState import org.smartregister.fhircore.quest.integration.Faker import org.smartregister.fhircore.quest.ui.main.appMainUiStateOf import org.smartregister.fhircore.quest.ui.register.FAB_BUTTON_REGISTER_TEST_TAG @@ -611,7 +610,6 @@ class RegisterScreenTest { currentSyncJobStatus = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), ), onAppMainEvent = {}, - customSyncState = CustomSyncState.Success, searchQuery = searchText, currentPage = currentPage, pagingItems = pagingItems, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt index 6b7371fd40a..61205e606b7 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView @@ -52,6 +51,7 @@ import org.hl7.fhir.r4.model.ResourceType import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration +import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager import org.smartregister.fhircore.engine.ui.base.AlertDialogButton @@ -120,7 +120,6 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { val scaffoldState = rememberScaffoldState() val uiState: AppMainUiState = appMainViewModel.appMainUiState.value val appDrawerUIState = appMainViewModel.appDrawerUiState.value - val customSyncState = appMainViewModel.customSyncState.collectAsState().value val openDrawer: (Boolean) -> Unit = { open: Boolean -> coroutineScope.launch { if (open) scaffoldState.drawerState.open() else scaffoldState.drawerState.close() @@ -194,7 +193,6 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { appDrawerUIState = appDrawerUIState, clearMapLiveData = geoWidgetLauncherViewModel.clearMapLiveData, geoJsonFeatures = geoWidgetLauncherViewModel.geoJsonFeatures, - customSyncState = customSyncState, launchQuestionnaire = geoWidgetLauncherViewModel::launchQuestionnaire, decodeImage = geoWidgetLauncherViewModel::getImageBitmap, onAppMainEvent = appMainViewModel::onEvent, @@ -213,9 +211,21 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { } override fun onSync(syncJobStatus: CurrentSyncJobStatus) { + onSync(syncJobStatus, isCustomSync = false) + } + + private fun onSync(syncJobStatus: CurrentSyncJobStatus, isCustomSync: Boolean) { when (syncJobStatus) { is CurrentSyncJobStatus.Running -> { - if (syncJobStatus.inProgressSyncJob is SyncJobStatus.InProgress) { + if (syncJobStatus.inProgressSyncJob is SyncJobStatus.Started) { + lifecycleScope.launch { + if (isCustomSync) { + geoWidgetLauncherViewModel.emitSnackBarState( + SnackBarMessageConfig(message = getString(R.string.syncing_custom_resources_toast)), + ) + } + } + } else { val inProgressSyncJob = syncJobStatus.inProgressSyncJob as SyncJobStatus.InProgress val isSyncUpload = inProgressSyncJob.syncOperation == SyncOperation.UPLOAD val progressPercentage = appMainViewModel.calculatePercentageProgress(inProgressSyncJob) @@ -246,26 +256,33 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { - eventBus.events - .getFor(MainNavigationScreen.GeoWidgetLauncher.eventId(navArgs.geoWidgetId)) - .onEach { appEvent -> - when (appEvent) { - is AppEvent.RefreshData, - is AppEvent.OnSubmitQuestionnaire, -> { - appMainViewModel.countRegisterData() - geoWidgetLauncherViewModel.run { - onEvent(GeoWidgetEvent.ClearMap) - onEvent( - GeoWidgetEvent.RetrieveFeatures( - geoWidgetConfig = geoWidgetConfiguration, - searchQuery = searchViewModel.searchQuery.value, - ), - ) + launch { + configurationRegistry.syncState + .onEach { syncJobStatus -> onSync(syncJobStatus, true) } + .launchIn(this) + } + launch { + eventBus.events + .getFor(MainNavigationScreen.GeoWidgetLauncher.eventId(navArgs.geoWidgetId)) + .onEach { appEvent -> + when (appEvent) { + is AppEvent.RefreshData, + is AppEvent.OnSubmitQuestionnaire, -> { + appMainViewModel.countRegisterData() + geoWidgetLauncherViewModel.run { + onEvent(GeoWidgetEvent.ClearMap) + onEvent( + GeoWidgetEvent.RetrieveFeatures( + geoWidgetConfig = geoWidgetConfiguration, + searchQuery = searchViewModel.searchQuery.value, + ), + ) + } } } } - } - .launchIn(lifecycleScope) + .launchIn(this) + } } } geoWidgetLauncherViewModel.noLocationFoundDialog.observe(viewLifecycleOwner) { show -> diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt index 744cb38e0d5..516a7c460cb 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt @@ -44,7 +44,6 @@ import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation -import org.smartregister.fhircore.engine.sync.CustomSyncState import org.smartregister.fhircore.engine.ui.components.register.LoaderDialog import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.geowidget.model.GeoJsonFeature @@ -72,7 +71,6 @@ fun GeoWidgetLauncherScreen( appDrawerUIState: AppDrawerUIState, clearMapLiveData: MutableLiveData, geoJsonFeatures: MutableLiveData>, - customSyncState: CustomSyncState = CustomSyncState.Idle, launchQuestionnaire: (QuestionnaireConfig, GeoJsonFeature, Context) -> Unit, decodeImage: ((String) -> Bitmap?)?, onAppMainEvent: (AppMainEvent) -> Unit, @@ -128,7 +126,6 @@ fun GeoWidgetLauncherScreen( appDrawerUIState = appDrawerUIState, onAppMainEvent = onAppMainEvent, openDrawer = openDrawer, - customSyncState = customSyncState, ) }, ) { innerPadding -> diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt index e6ab47b9bdd..1ea75e601f1 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt @@ -43,9 +43,6 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.time.Duration import kotlinx.coroutines.async -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -62,8 +59,7 @@ import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger import org.smartregister.fhircore.engine.data.local.register.RegisterRepository import org.smartregister.fhircore.engine.domain.model.LauncherType import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction -import org.smartregister.fhircore.engine.sync.CustomSyncState -import org.smartregister.fhircore.engine.sync.CustomWorkerState +import org.smartregister.fhircore.engine.sync.CustomSyncWorker import org.smartregister.fhircore.engine.sync.SyncBroadcaster import org.smartregister.fhircore.engine.task.FhirCarePlanGenerator import org.smartregister.fhircore.engine.task.FhirCompleteCarePlanWorker @@ -93,7 +89,6 @@ import org.smartregister.fhircore.quest.ui.shared.models.AppDrawerUIState import org.smartregister.fhircore.quest.ui.shared.models.QuestionnaireSubmission import org.smartregister.fhircore.quest.util.extensions.handleClickEvent import org.smartregister.fhircore.quest.util.extensions.schedulePeriodically -import timber.log.Timber @HiltViewModel class AppMainViewModel @@ -139,35 +134,6 @@ constructor( configurationRegistry.retrieveConfigurations(ConfigType.MeasureReport) } - private val _customSyncState = MutableStateFlow(CustomSyncState.Idle) - val customSyncState: StateFlow = _customSyncState.asStateFlow() - - init { - observeWorkerSyncState() - } - - private fun observeWorkerSyncState() { - viewModelScope.launch { - CustomWorkerState.workerState.collect { syncState -> - when (syncState) { - CustomSyncState.InProgress -> { - Timber.d("Custom Sync Worker InProgress") - _customSyncState.value = CustomSyncState.InProgress - } - CustomSyncState.Failed() -> { - Timber.d("Custom Sync Worker Failed") - _customSyncState.value = CustomSyncState.Failed("") - } - CustomSyncState.Success -> { - _customSyncState.value = CustomSyncState.Success - Timber.d("Custom Sync Worker Finished") - } - else -> {} - } - } - } - } - fun retrieveAppMainUiState(refreshAll: Boolean = true) { if (refreshAll) { appMainUiState.value = @@ -502,6 +468,12 @@ constructor( initialDelay = INITIAL_DELAY, ) + schedulePeriodically( + workId = CustomSyncWorker.WORK_ID, + repeatInterval = applicationConfiguration.syncInterval, + initialDelay = 0, + ) + measureReportConfigurations.forEach { measureReportConfig -> measureReportConfig.scheduledGenerationDuration?.let { scheduledGenerationDuration -> schedulePeriodically( diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt index 3a4c959064d..e4ca10838bb 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt @@ -52,9 +52,12 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry +import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager import org.smartregister.fhircore.engine.ui.theme.AppTheme +import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.event.AppEvent import org.smartregister.fhircore.quest.event.EventBus import org.smartregister.fhircore.quest.navigation.MainNavigationScreen @@ -76,6 +79,8 @@ class RegisterFragment : Fragment(), OnSyncListener { @Inject lateinit var syncListenerManager: SyncListenerManager + @Inject lateinit var configurationRegistry: ConfigurationRegistry + @Inject lateinit var eventBus: EventBus private val registerFragmentArgs by navArgs() private val registerViewModel by viewModels() @@ -102,7 +107,6 @@ class RegisterFragment : Fragment(), OnSyncListener { val scope = rememberCoroutineScope() val scaffoldState = rememberScaffoldState() val uiState: AppMainUiState = appMainViewModel.appMainUiState.value - val customSyncState = appMainViewModel.customSyncState.collectAsState().value val openDrawer: (Boolean) -> Unit = { open: Boolean -> scope.launch { if (open) scaffoldState.drawerState.open() else scaffoldState.drawerState.close() @@ -173,7 +177,6 @@ class RegisterFragment : Fragment(), OnSyncListener { registerUiState = registerViewModel.registerUiState.value, registerUiCountState = registerViewModel.registerUiCountState.value, appDrawerUIState = appMainViewModel.appDrawerUiState.value, - customSyncState = customSyncState, onAppMainEvent = { appMainViewModel.onEvent(it) }, searchQuery = searchViewModel.searchQuery, currentPage = registerViewModel.currentPage, @@ -195,19 +198,29 @@ class RegisterFragment : Fragment(), OnSyncListener { } override fun onSync(syncJobStatus: CurrentSyncJobStatus) { + onSync(syncJobStatus, isCustomSync = false) + } + + private fun onSync(syncJobStatus: CurrentSyncJobStatus, isCustomSync: Boolean) { when (syncJobStatus) { is CurrentSyncJobStatus.Running -> { - if (syncJobStatus.inProgressSyncJob is SyncJobStatus.InProgress) { + if (syncJobStatus.inProgressSyncJob is SyncJobStatus.Started) { + lifecycleScope.launch { + if (isCustomSync) { + registerViewModel.emitSnackBarState( + SnackBarMessageConfig(message = getString(R.string.syncing_custom_resources_toast)), + ) + } + } + } else { val inProgressSyncJob = syncJobStatus.inProgressSyncJob as SyncJobStatus.InProgress val isSyncUpload = inProgressSyncJob.syncOperation == SyncOperation.UPLOAD val progressPercentage = appMainViewModel.calculatePercentageProgress(inProgressSyncJob) - lifecycleScope.launch { - appMainViewModel.updateAppDrawerUIState( - isSyncUpload = isSyncUpload, - currentSyncJobStatus = syncJobStatus, - percentageProgress = progressPercentage, - ) - } + appMainViewModel.updateAppDrawerUIState( + isSyncUpload = isSyncUpload, + currentSyncJobStatus = syncJobStatus, + percentageProgress = progressPercentage, + ) } } is CurrentSyncJobStatus.Succeeded -> { @@ -244,20 +257,27 @@ class RegisterFragment : Fragment(), OnSyncListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + configurationRegistry.syncState + .onEach { syncJobStatus -> onSync(syncJobStatus, true) } + .launchIn(this) + } // Each register should have unique eventId - eventBus.events - .getFor(MainNavigationScreen.Home.eventId(registerFragmentArgs.registerId)) - .onEach { appEvent -> - when (appEvent) { - is AppEvent.OnSubmitQuestionnaire -> - handleQuestionnaireSubmission(appEvent.questionnaireSubmission) - is AppEvent.RefreshData -> { - appMainViewModel.countRegisterData() - refreshRegisterData() + launch { + eventBus.events + .getFor(MainNavigationScreen.Home.eventId(registerFragmentArgs.registerId)) + .onEach { appEvent -> + when (appEvent) { + is AppEvent.OnSubmitQuestionnaire -> + handleQuestionnaireSubmission(appEvent.questionnaireSubmission) + is AppEvent.RefreshData -> { + appMainViewModel.countRegisterData() + refreshRegisterData() + } } } - } - .launchIn(lifecycleScope) + .launchIn(this) + } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt index 186ef9ccad2..4db108fa4f7 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt @@ -61,7 +61,6 @@ import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig import org.smartregister.fhircore.engine.domain.model.ResourceConfig import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation -import org.smartregister.fhircore.engine.sync.CustomSyncState import org.smartregister.fhircore.engine.ui.components.register.LoaderDialog import org.smartregister.fhircore.engine.ui.components.register.RegisterHeader import org.smartregister.fhircore.engine.ui.theme.AppTheme @@ -101,7 +100,6 @@ fun RegisterScreen( currentPage: MutableState, pagingItems: LazyPagingItems, navController: NavController, - customSyncState: CustomSyncState = CustomSyncState.Idle, toolBarHomeNavigation: ToolBarHomeNavigation = ToolBarHomeNavigation.OPEN_DRAWER, decodeImage: ((String) -> Bitmap?)?, ) { @@ -168,7 +166,6 @@ fun RegisterScreen( appDrawerUIState = appDrawerUIState, onAppMainEvent = onAppMainEvent, openDrawer = openDrawer, - customSyncState = customSyncState, ) }, ) { innerPadding -> diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt index 56d83bf4fef..cb371dfb207 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt @@ -64,10 +64,10 @@ import java.time.OffsetDateTime import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.smartregister.fhircore.engine.sync.CustomSyncState import org.smartregister.fhircore.engine.ui.components.LineSpinFadeLoaderProgressIndicator import org.smartregister.fhircore.engine.ui.theme.AppTheme import org.smartregister.fhircore.engine.ui.theme.DangerColor +import org.smartregister.fhircore.engine.ui.theme.DefaultColor import org.smartregister.fhircore.engine.ui.theme.SubtitleTextColor import org.smartregister.fhircore.engine.ui.theme.SuccessColor import org.smartregister.fhircore.engine.ui.theme.SyncBarBackgroundColor @@ -75,7 +75,6 @@ import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundEx import org.smartregister.fhircore.quest.ui.main.AppMainEvent import org.smartregister.fhircore.quest.ui.shared.models.AppDrawerUIState import org.smartregister.fhircore.quest.util.extensions.conditional -import timber.log.Timber const val TRANSPARENCY = 0.2f const val SYNC_PROGRESS_INDICATOR_TEST_TAG = "syncProgressIndicatorTestTag" @@ -84,18 +83,13 @@ const val SYNC_PROGRESS_INDICATOR_TEST_TAG = "syncProgressIndicatorTestTag" fun SyncBottomBar( isFirstTimeSync: Boolean, appDrawerUIState: AppDrawerUIState, - customSyncState: CustomSyncState, onAppMainEvent: (AppMainEvent) -> Unit, openDrawer: (Boolean) -> Unit, ) { val coroutineScope = rememberCoroutineScope() val currentSyncJobStatus = appDrawerUIState.currentSyncJobStatus val hideSyncCompleteStatus = remember { mutableStateOf(false) } - - if ( - currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && - customSyncState is CustomSyncState.Success - ) { + if (currentSyncJobStatus is CurrentSyncJobStatus.Succeeded) { LaunchedEffect(Unit) { coroutineScope.launch { delay(7.seconds) @@ -103,17 +97,13 @@ fun SyncBottomBar( } } } - val syncBackgroundColor = - when { - currentSyncJobStatus is CurrentSyncJobStatus.Failed || - customSyncState is CustomSyncState.Failed -> DangerColor.copy(alpha = 0.2f) - currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && - customSyncState is CustomSyncState.Success -> SuccessColor.copy(alpha = 0.2f) - currentSyncJobStatus is CurrentSyncJobStatus.Running -> SyncBarBackgroundColor + when (currentSyncJobStatus) { + is CurrentSyncJobStatus.Failed -> DangerColor.copy(alpha = 0.2f) + is CurrentSyncJobStatus.Succeeded -> SuccessColor.copy(alpha = 0.2f) + is CurrentSyncJobStatus.Running -> SyncBarBackgroundColor else -> Color.Transparent } - var syncNotificationBarExpanded by remember { mutableStateOf(true) } val bottomRadius = if (!hideSyncCompleteStatus.value || currentSyncJobStatus is CurrentSyncJobStatus.Running) { @@ -127,16 +117,12 @@ fun SyncBottomBar( if (currentSyncJobStatus is CurrentSyncJobStatus.Running) 114.dp else 80.dp else -> 60.dp } - if ( !isFirstTimeSync && currentSyncJobStatus != null && (currentSyncJobStatus is CurrentSyncJobStatus.Running || - (currentSyncJobStatus is CurrentSyncJobStatus.Failed || - customSyncState is CustomSyncState.Failed) || - (!hideSyncCompleteStatus.value && - currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && - customSyncState is CustomSyncState.Success)) + currentSyncJobStatus is CurrentSyncJobStatus.Failed || + (!hideSyncCompleteStatus.value && currentSyncJobStatus is CurrentSyncJobStatus.Succeeded)) ) { Box( modifier = @@ -170,11 +156,9 @@ fun SyncBottomBar( }, contentDescription = null, tint = - when { - currentSyncJobStatus is CurrentSyncJobStatus.Failed || - customSyncState is CustomSyncState.Failed -> DangerColor - currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && - customSyncState is CustomSyncState.Success -> SuccessColor + when (currentSyncJobStatus) { + is CurrentSyncJobStatus.Failed -> DangerColor + is CurrentSyncJobStatus.Succeeded -> SuccessColor else -> Color.White }, modifier = Modifier.size(16.dp), @@ -186,39 +170,34 @@ fun SyncBottomBar( contentAlignment = Alignment.Center, ) { val context = LocalContext.current - when { - currentSyncJobStatus is CurrentSyncJobStatus.Running -> { + when (currentSyncJobStatus) { + is CurrentSyncJobStatus.Running -> { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, progressPercentage = appDrawerUIState.percentageProgress, - customSyncState = customSyncState, onCancel = { onAppMainEvent(AppMainEvent.CancelSyncData(context)) }, ) SideEffect { hideSyncCompleteStatus.value = false } } - currentSyncJobStatus is CurrentSyncJobStatus.Failed || - customSyncState is CustomSyncState.Failed -> { + is CurrentSyncJobStatus.Failed -> { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, - customSyncState = customSyncState, onRetry = { openDrawer(false) onAppMainEvent(AppMainEvent.SyncData(context)) }, ) } - currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && - customSyncState is CustomSyncState.Success -> { + is CurrentSyncJobStatus.Succeeded -> { if (!hideSyncCompleteStatus.value) { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, - customSyncState = customSyncState, ) } } @@ -235,29 +214,15 @@ fun SyncBottomBar( fun SyncStatusView( isSyncUpload: Boolean?, currentSyncJobStatus: CurrentSyncJobStatus?, - customSyncState: CustomSyncState = CustomSyncState.Idle, progressPercentage: Int? = null, minimized: Boolean = false, onRetry: () -> Unit = {}, onCancel: () -> Unit = {}, ) { val height = - when { - minimized -> 36.dp - currentSyncJobStatus is CurrentSyncJobStatus.Running -> 88.dp - else -> 56.dp - } - - val isSucceeded = - currentSyncJobStatus is CurrentSyncJobStatus.Succeeded && - customSyncState is CustomSyncState.Success - val isFailed = - currentSyncJobStatus is CurrentSyncJobStatus.Failed || customSyncState is CustomSyncState.Failed - - Timber.d("current sync status evaluates to : $isSucceeded") - Timber.d("current custom Sync state is $customSyncState") - Timber.d("current fhir sync state is $currentSyncJobStatus") - + if (minimized) { + 36.dp + } else if (currentSyncJobStatus is CurrentSyncJobStatus.Running) 88.dp else 56.dp Row( modifier = Modifier.height(height) @@ -268,27 +233,35 @@ fun SyncStatusView( .conditional(minimized, { padding(vertical = 4.dp) }, { padding(vertical = 16.dp) }), verticalAlignment = Alignment.CenterVertically, ) { - if (isSucceeded || isFailed) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f), - ) { + if ( + (currentSyncJobStatus is CurrentSyncJobStatus.Failed || + currentSyncJobStatus is CurrentSyncJobStatus.Succeeded) + ) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) { if (!minimized) { Icon( - imageVector = if (isSucceeded) Icons.Default.CheckCircle else Icons.Default.Error, + imageVector = + if (currentSyncJobStatus is CurrentSyncJobStatus.Succeeded) { + Icons.Default.CheckCircle + } else { + Icons.Default.Error + }, contentDescription = null, - tint = if (isFailed) DangerColor else SuccessColor, + tint = + when (currentSyncJobStatus) { + is CurrentSyncJobStatus.Failed -> DangerColor + is CurrentSyncJobStatus.Succeeded -> SuccessColor + else -> DefaultColor + }, ) } SyncStatusTitle( text = - stringResource( - if (isSucceeded) { - org.smartregister.fhircore.engine.R.string.sync_complete - } else { - org.smartregister.fhircore.engine.R.string.sync_error - }, - ), + if (currentSyncJobStatus is CurrentSyncJobStatus.Succeeded) { + stringResource(org.smartregister.fhircore.engine.R.string.sync_complete) + } else { + stringResource(org.smartregister.fhircore.engine.R.string.sync_error) + }, minimized = minimized, startPadding = if (minimized) 0 else 16, ) @@ -351,7 +324,10 @@ fun SyncStatusView( innerRadius = 12f, ) } - if ((isFailed || currentSyncJobStatus is CurrentSyncJobStatus.Running) && !minimized) { + if ( + (currentSyncJobStatus is CurrentSyncJobStatus.Failed || + currentSyncJobStatus is CurrentSyncJobStatus.Running) && !minimized + ) { Text( text = stringResource( @@ -378,7 +354,7 @@ fun SyncStatusView( } @Composable -fun SyncStatusTitle( +private fun SyncStatusTitle( text: String, color: Color = Color.Unspecified, minimized: Boolean, @@ -401,7 +377,6 @@ fun SyncStatusSucceededPreview() { SyncStatusView( isSyncUpload = false, currentSyncJobStatus = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), - customSyncState = CustomSyncState.Success, ) } } diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml index 4e1f1588a9c..a39950895a5 100644 --- a/android/quest/src/main/res/values/strings.xml +++ b/android/quest/src/main/res/values/strings.xml @@ -144,4 +144,5 @@ Error rendering profile Are you sure you want to submit? You are about to submit + Syncing custom Resources... From 26ad9515fe5cc40d4f380657d7b018c3d906b640 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Fri, 10 Jan 2025 12:11:37 +0300 Subject: [PATCH 09/16] Remove commented lines of code Signed-off-by: Lentumunai-Mark --- .../configuration/ConfigurationRegistry.kt | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index d295645e95c..771dcf43c02 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -114,24 +114,7 @@ constructor( * Retrieve configuration for the provided [ConfigType]. The JSON retrieved from [configsJsonMap] * can be directly converted to a FHIR resource or hard coded custom model. The filtering assumes * you are passing data across screens, then later using it in DataQueries and to retrieve - * registerConfiguration. It is necessary to // private val _syncState = - * MutableSharedFlow() // val customSyncState: SharedFlow = - * _syncState // // private suspend fun setSyncState(state: SyncJobStatus) = - * _syncState.emit(state) - * - * // init { // observeWorkerSyncState() // } - * - * // private fun observeWorkerSyncState() { // viewModelScope.launch { // - * CustomWorkerState.workerState.collect { syncState -> // when (syncState) { // is - * CustomSyncState.InProgress -> { // Timber.d("Custom Sync Worker InProgress") // // val total = - * syncState.total // val completed = syncState.completed // val syncOperation = - * syncState.syncOperation // // // Set the current sync state with the extracted values // - * setSyncState( // SyncJobStatus.InProgress( // syncOperation = syncOperation, // total = total, - * // completed = completed, // ), // ) // } // is CustomSyncState.Failed -> { // Timber.d("Custom - * Sync Worker Failed") // setSyncState(SyncJobStatus.Failed(emptyList())) // } // - * CustomSyncState.Success -> { // Timber.d("Custom Sync Worker Succeeded") // - * setSyncState(SyncJobStatus.Succeeded()) // } // CustomSyncState.Idle -> { // Timber.d("Custom - * Sync Worker Idle") // } // } // } // } // }check that [paramsMap] is empty to confirm that the + * registerConfiguration. It is necessary to check that [paramsMap] is empty to confirm that the * params used in the DataQuery are passed when retrieving the configurations. * * @throws NoSuchElementException when the [configsJsonMap] doesn't contain a value for the From 38f982e361b33e5174f46d00f24cc9f555f3f7e5 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Wed, 29 Jan 2025 10:21:39 +0300 Subject: [PATCH 10/16] Resolve spotless check failing. Signed-off-by: Lentumunai-Mark --- .../fhircore/engine/configuration/ConfigurationRegistry.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index fb796d3e613..c253a02038a 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -26,9 +26,9 @@ import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.get import com.google.android.fhir.knowledge.KnowledgeManager import com.google.android.fhir.sync.CurrentSyncJobStatus +import com.google.android.fhir.sync.SyncDataParams.LAST_UPDATED_KEY import com.google.android.fhir.sync.SyncJobStatus import com.google.android.fhir.sync.SyncOperation -import com.google.android.fhir.sync.SyncDataParams.LAST_UPDATED_KEY import com.google.android.fhir.sync.download.ResourceSearchParams import dagger.hilt.android.qualifiers.ApplicationContext import java.io.FileNotFoundException From ff775e2ce709e102fea551041e8c71cf2578ce27 Mon Sep 17 00:00:00 2001 From: Elly Kitoto Date: Wed, 29 Jan 2025 17:09:38 +0300 Subject: [PATCH 11/16] Refactor custom sync Signed-off-by: Elly Kitoto --- .../configuration/ConfigurationRegistry.kt | 93 +-------- .../fhircore/engine/sync/AppSyncWorker.kt | 2 + .../engine/sync/CustomResourceSyncService.kt | 159 +++++++++++++++ .../fhircore/engine/sync/CustomSyncWorker.kt | 118 ----------- .../fhircore/engine/sync/SyncBroadcaster.kt | 14 -- .../engine/sync/SyncListenerManager.kt | 5 + .../engine/util/SharedPreferencesHelper.kt | 13 -- .../engine/sync/CustomSyncWorkerTest.kt | 184 ------------------ .../ui/geowidget/GeoWidgetLauncherFragment.kt | 20 +- .../quest/ui/main/AppMainViewModel.kt | 17 +- .../quest/ui/register/RegisterFragment.kt | 21 +- android/quest/src/main/res/values/strings.xml | 1 - 12 files changed, 175 insertions(+), 472 deletions(-) create mode 100644 android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomResourceSyncService.kt delete mode 100644 android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt delete mode 100644 android/engine/src/test/java/org/smartregister/fhircore/engine/sync/CustomSyncWorkerTest.kt diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index fb796d3e613..9a095ccb7ee 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -25,23 +25,18 @@ import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.get import com.google.android.fhir.knowledge.KnowledgeManager -import com.google.android.fhir.sync.CurrentSyncJobStatus -import com.google.android.fhir.sync.SyncJobStatus -import com.google.android.fhir.sync.SyncOperation +import com.google.android.fhir.sync.ParamMap import com.google.android.fhir.sync.SyncDataParams.LAST_UPDATED_KEY import com.google.android.fhir.sync.download.ResourceSearchParams import dagger.hilt.android.qualifiers.ApplicationContext import java.io.FileNotFoundException import java.io.InputStreamReader import java.net.UnknownHostException -import java.time.OffsetDateTime import java.util.Locale import java.util.PropertyResourceBundle import java.util.ResourceBundle import javax.inject.Inject import javax.inject.Singleton -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import okhttp3.RequestBody.Companion.toRequestBody @@ -61,7 +56,6 @@ import org.hl7.fhir.r4.model.SearchParameter import org.jetbrains.annotations.VisibleForTesting import org.json.JSONObject import org.smartregister.fhircore.engine.BuildConfig -import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource import org.smartregister.fhircore.engine.di.NetworkModule @@ -108,10 +102,6 @@ constructor( val localizationHelper: LocalizationHelper by lazy { LocalizationHelper(this) } private val supportedFileExtensions = listOf("json", "properties") private var _isNonProxy = BuildConfig.IS_NON_PROXY_APK - private val _syncState = MutableSharedFlow() - val syncState: SharedFlow = _syncState - - suspend fun setSyncState(state: CurrentSyncJobStatus) = _syncState.emit(state) /** * Retrieve configuration for the provided [ConfigType]. The JSON retrieved from [configsJsonMap] @@ -575,81 +565,6 @@ constructor( return resultBundle } - suspend fun fetchCustomResources( - gatewayModeHeaderValue: String? = null, - url: String, - totalCustomRecords: Int = 0, - completedRecords: Int = 0, - ) { - if (completedRecords == 0) { - Timber.d("Setting state: Started") - setSyncState( - CurrentSyncJobStatus.Running( - SyncJobStatus.Started(), - ), - ) - } - - runCatching { - Timber.d("Setting state: Running") - setSyncState( - CurrentSyncJobStatus.Running( - SyncJobStatus.InProgress( - syncOperation = SyncOperation.DOWNLOAD, - total = totalCustomRecords, - completed = completedRecords, - ), - ), - ) - Timber.d("Fetching page with URL: $url") - if (gatewayModeHeaderValue.isNullOrEmpty()) { - fhirResourceDataSource.getResource(url) - } else { - fhirResourceDataSource.getResourceWithGatewayModeHeader(gatewayModeHeaderValue, url) - } - } - .onFailure { throwable -> - Timber.e("Error occurred while retrieving resource via URL $url", throwable) - Timber.d("Setting state: Failed") - setSyncState( - CurrentSyncJobStatus.Failed(OffsetDateTime.now()), - ) - return - } - .onSuccess { resultBundle -> - processResultBundleEntries(resultBundle.entry) - val newCompletedRecords = completedRecords + resultBundle.entry.size - - Timber.d("Updating state: Running") - setSyncState( - CurrentSyncJobStatus.Running( - SyncJobStatus.InProgress( - syncOperation = SyncOperation.DOWNLOAD, - total = totalCustomRecords, - completed = newCompletedRecords, - ), - ), - ) - - val nextPageUrl = resultBundle.getLink(PAGINATION_NEXT)?.url - - if (!nextPageUrl.isNullOrEmpty()) { - fetchCustomResources( - gatewayModeHeaderValue = gatewayModeHeaderValue, - url = nextPageUrl, - totalCustomRecords = totalCustomRecords, - completedRecords = newCompletedRecords, // Pass the new value - ) - } else { - Timber.d("No more pages to fetch. Fetching completed.") - Timber.d("Setting state: Succeeded") - setSyncState( - CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), - ) - } - } - } - private suspend fun fetchResources( gatewayModeHeaderValue: String? = null, url: String, @@ -676,7 +591,7 @@ constructor( } } - private suspend fun processResultBundleEntries( + suspend fun processResultBundleEntries( resultBundleEntries: List, ) { resultBundleEntries.forEach { bundleEntryComponent -> @@ -866,10 +781,8 @@ constructor( } } - suspend fun loadResourceSearchParams(): - Pair>, ResourceSearchParams> { + suspend fun loadResourceSearchParams(): Pair, ResourceSearchParams> { val syncConfig = retrieveResourceConfiguration(ConfigType.Sync) - val appConfig = retrieveConfiguration(ConfigType.Application) val customResourceSearchParams = mutableMapOf>() val fhirResourceSearchParams = mutableMapOf>() val organizationResourceTag = diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt index 80d79883204..f725f09f4e2 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt @@ -57,6 +57,7 @@ constructor( private val openSrpFhirEngine: FhirEngine, private val appTimeStampContext: AppTimeStampContext, private val configService: ConfigService, + private val customResourceSyncService: CustomResourceSyncService, ) : FhirSyncWorker(appContext, workerParams), OnSyncListener { private val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -76,6 +77,7 @@ constructor( override suspend fun doWork(): Result { saveSyncStartTimestamp() setForeground(getForegroundInfo()) + customResourceSyncService.runCustomResourceSync() return super.doWork() } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomResourceSyncService.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomResourceSyncService.kt new file mode 100644 index 00000000000..7db761c4bad --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomResourceSyncService.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.engine.sync + +import com.google.android.fhir.sync.CurrentSyncJobStatus +import com.google.android.fhir.sync.SyncJobStatus +import com.google.android.fhir.sync.SyncOperation +import com.google.android.fhir.sync.concatParams +import java.net.UnknownHostException +import java.time.OffsetDateTime +import javax.inject.Inject +import javax.inject.Singleton +import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry +import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry.Companion.PAGINATION_NEXT +import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource +import org.smartregister.fhircore.engine.util.DispatcherProvider +import retrofit2.HttpException +import retrofit2.Response +import timber.log.Timber + +@Singleton +class CustomResourceSyncService +@Inject +constructor( + val configurationRegistry: ConfigurationRegistry, + val dispatcherProvider: DispatcherProvider, + val fhirResourceDataSource: FhirResourceDataSource, + val syncListenerManager: SyncListenerManager, +) { + suspend fun runCustomResourceSync() { + try { + with(configurationRegistry) { + val (resourceSearchParams, _) = loadResourceSearchParams() + if (resourceSearchParams.isEmpty()) return + + // Process resource URLs + val resourceUrls = + resourceSearchParams + .asIterable() + .filter { it.value.isNotEmpty() } + .map { "${it.key}?${it.value.concatParams()}" } + + // Fetch summary count first + val summaryCount = fetchSummaryCount(resourceUrls).values.sumOf { it ?: 0 } + Timber.d("Fetched summary count: $summaryCount") + + // Fetch resources + resourceUrls.forEach { url -> + fetchCustomResources( + gatewayModeHeaderValue = ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE, + url = url, + totalCounts = summaryCount, + ) + } + } + } catch (httpException: HttpException) { + Timber.e(httpException) + val response: Response<*>? = httpException.response() + if (response != null && (400..503).contains(response.code())) { + Timber.e("HTTP exception ${response.code()} -> ${response.errorBody()}") + } + } catch (unknownHostException: UnknownHostException) { + Timber.e(unknownHostException) + } catch (exception: Exception) { + Timber.e(exception) + syncListenerManager.emitSyncStatus(CurrentSyncJobStatus.Failed(OffsetDateTime.now())) + } + } + + private suspend fun fetchCustomResources( + gatewayModeHeaderValue: String? = null, + url: String, + totalCounts: Int = 0, + completedRecords: Int = 0, + ) { + runCatching { + Timber.d("Setting state: Running") + syncListenerManager.emitSyncStatus( + CurrentSyncJobStatus.Running( + SyncJobStatus.InProgress( + syncOperation = SyncOperation.DOWNLOAD, + total = totalCounts, + completed = completedRecords, + ), + ), + ) + Timber.d("Fetching page with URL: $url") + if (gatewayModeHeaderValue.isNullOrEmpty()) { + fhirResourceDataSource.getResource(url) + } else { + fhirResourceDataSource.getResourceWithGatewayModeHeader(gatewayModeHeaderValue, url) + } + } + .onFailure { throwable -> + Timber.e("Error occurred while retrieving resource via URL $url", throwable) + syncListenerManager.emitSyncStatus(CurrentSyncJobStatus.Failed(OffsetDateTime.now())) + return + } + .onSuccess { resultBundle -> + configurationRegistry.processResultBundleEntries(resultBundle.entry) + val newCompletedRecords = completedRecords + resultBundle.entry.size + syncListenerManager.emitSyncStatus( + CurrentSyncJobStatus.Running( + SyncJobStatus.InProgress( + syncOperation = SyncOperation.DOWNLOAD, + total = totalCounts, + completed = newCompletedRecords, + ), + ), + ) + + val nextPageUrl = resultBundle.getLink(PAGINATION_NEXT)?.url + + if (!nextPageUrl.isNullOrEmpty()) { + fetchCustomResources( + gatewayModeHeaderValue = gatewayModeHeaderValue, + url = nextPageUrl, + totalCounts = totalCounts, + completedRecords = newCompletedRecords, + ) + } else { + Timber.d("Fetch complete. Emitting SyncStatus.Succeeded.") + syncListenerManager.emitSyncStatus(CurrentSyncJobStatus.Succeeded(OffsetDateTime.now())) + } + } + } + + /** Fetch summary counts for the provided [resourceUrls] */ + private suspend fun fetchSummaryCount(resourceUrls: List): Map = + resourceUrls + .associate { url -> + val summaryUrl = "$url&summary=count" + val total: Int? = + runCatching { fhirResourceDataSource.getResource(summaryUrl) } + .onFailure { Timber.e(it, "Failed to fetch summary for $summaryUrl") } + .getOrNull() + ?.total + summaryUrl to total + } + .also { summaries -> Timber.i("Summary fetch results: $summaries") } + + companion object { + const val WORK_ID = "CustomResourceSyncWorker" + } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt deleted file mode 100644 index 07467843bd7..00000000000 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2021-2024 Ona Systems, Inc - * - * 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 org.smartregister.fhircore.engine.sync - -import android.content.Context -import androidx.hilt.work.HiltWorker -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -import com.google.android.fhir.sync.CurrentSyncJobStatus -import com.google.android.fhir.sync.concatParams -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import java.net.UnknownHostException -import java.time.OffsetDateTime -import kotlinx.coroutines.withContext -import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry -import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource -import org.smartregister.fhircore.engine.util.DispatcherProvider -import retrofit2.HttpException -import retrofit2.Response -import timber.log.Timber - -@HiltWorker -class CustomSyncWorker -@AssistedInject -constructor( - @Assisted appContext: Context, - @Assisted workerParams: WorkerParameters, - val configurationRegistry: ConfigurationRegistry, - val dispatcherProvider: DispatcherProvider, - val fhirResourceDataSource: FhirResourceDataSource, -) : CoroutineWorker(appContext, workerParams) { - - override suspend fun doWork(): Result { - return withContext(dispatcherProvider.io()) { - try { - with(configurationRegistry) { - val (resourceSearchParams, _) = loadResourceSearchParams() - Timber.i("Custom resource sync parameters $resourceSearchParams") - - // Process resource URLs - val resourceUrls = - resourceSearchParams - .asIterable() - .filter { it.value.isNotEmpty() } - .map { "${it.key}?${it.value.concatParams()}" } - - // Fetch summary count first - - val summaryCount = fetchSummaryCount(resourceUrls).values.sumOf { it ?: 0 } - Timber.d("Fetched summary count: $summaryCount") - - // Fetch resources - resourceUrls.forEach { url -> - fetchCustomResources( - gatewayModeHeaderValue = ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE, - url = url, - totalCustomRecords = summaryCount, - ) - } - } - Result.success() - } catch (httpException: HttpException) { - Timber.e(httpException) - val response: Response<*>? = httpException.response() - if (response != null && (400..503).contains(response.code())) { - Timber.e("HTTP exception ${response.code()} -> ${response.errorBody()}") - } - Result.failure() - } catch (unknownHostException: UnknownHostException) { - Timber.e(unknownHostException) - Result.failure() - } catch (exception: Exception) { - Timber.e(exception) - configurationRegistry.setSyncState( - CurrentSyncJobStatus.Failed(OffsetDateTime.now()), - ) - Result.failure() - } - } - } - - /** Fetch summary counts for the provided URLs with summary=count query added. */ - private suspend fun fetchSummaryCount(resourceUrls: List): Map = - resourceUrls - .associate { url -> - // Modify URL to include 'summary=count' - val summaryUrl = "$url&summary=count" - val total: Int? = - runCatching { - // Explicitly fetch resource and ensure result can be cast to Bundle - fhirResourceDataSource.getResource(summaryUrl) - } - .onFailure { Timber.e(it, "Failed to fetch summary for $summaryUrl") } - .getOrNull() - ?.total - summaryUrl to total - } - .also { summaries -> Timber.i("Summary fetch results: $summaries") } - - companion object { - const val WORK_ID = "CustomResourceSyncWorker" - } -} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt index 59c9413d934..b5db77630d0 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt @@ -20,7 +20,6 @@ import android.content.Context import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.google.android.fhir.FhirEngine import com.google.android.fhir.sync.BackoffCriteria @@ -74,19 +73,6 @@ constructor( suspend fun runOneTimeSync(): Unit = coroutineScope { Timber.i("Running one time sync...") Sync.oneTimeSync(context).handleOneTimeSyncJobStatus(this) - - workManager.enqueue( - OneTimeWorkRequestBuilder() - .setConstraints( - Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), - ) - .setBackoffCriteria( - BackoffPolicy.LINEAR, - 10, - TimeUnit.SECONDS, - ) - .build(), - ) } /** diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncListenerManager.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncListenerManager.kt index 9015ef7a14e..ea9512b58ff 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncListenerManager.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncListenerManager.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner +import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.fhir.sync.SyncJobStatus import com.google.android.fhir.sync.download.ResourceSearchParams import dagger.hilt.android.qualifiers.ApplicationContext @@ -105,4 +106,8 @@ constructor( Timber.i("FHIR resource sync parameters $resourceSearchParams") return resourceSearchParams } + + fun emitSyncStatus(currentSyncJobStatus: CurrentSyncJobStatus) { + _onSyncListeners.forEach { it.get()?.onSync(currentSyncJobStatus) } + } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt index 627308c2f72..0160f166772 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferencesHelper.kt @@ -18,7 +18,6 @@ package org.smartregister.fhircore.engine.util import android.content.Context import android.content.SharedPreferences -import android.content.SharedPreferences.OnSharedPreferenceChangeListener import com.google.gson.Gson import com.google.gson.JsonIOException import dagger.hilt.android.qualifiers.ApplicationContext @@ -107,18 +106,6 @@ constructor(@ApplicationContext val context: Context, val gson: Gson) { prefs.edit()?.clear()?.apply() } - fun registerSharedPreferencesListener( - onSharedPreferenceChangeListener: OnSharedPreferenceChangeListener, - ) { - prefs.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) - } - - fun unregisterSharedPreferencesListener( - onSharedPreferenceChangeListener: OnSharedPreferenceChangeListener, - ) { - prefs.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) - } - fun retrieveApplicationId() = read(SharedPreferenceKey.APP_ID.name, null) companion object { diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/sync/CustomSyncWorkerTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/sync/CustomSyncWorkerTest.kt deleted file mode 100644 index 9368f37fac6..00000000000 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/sync/CustomSyncWorkerTest.kt +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2021-2024 Ona Systems, Inc - * - * 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 org.smartregister.fhircore.engine.sync - -import android.content.Context -import android.util.Log -import androidx.compose.ui.state.ToggleableState -import androidx.test.core.app.ApplicationProvider -import androidx.work.Configuration -import androidx.work.ListenableWorker -import androidx.work.WorkerFactory -import androidx.work.WorkerParameters -import androidx.work.testing.SynchronousExecutor -import androidx.work.testing.TestListenableWorkerBuilder -import androidx.work.testing.WorkManagerTestInitHelper -import com.google.gson.Gson -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import io.mockk.mockk -import io.mockk.spyk -import java.util.UUID -import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runTest -import org.hl7.fhir.r4.model.ResourceType -import org.junit.Assert -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.smartregister.fhircore.engine.app.fakes.Faker -import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry -import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource -import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceService -import org.smartregister.fhircore.engine.datastore.syncLocationIdsProtoStore -import org.smartregister.fhircore.engine.domain.model.SyncLocationState -import org.smartregister.fhircore.engine.robolectric.RobolectricTest -import org.smartregister.fhircore.engine.rule.CoroutineTestRule -import org.smartregister.fhircore.engine.util.DispatcherProvider -import org.smartregister.fhircore.engine.util.SharedPreferencesHelper - -@HiltAndroidTest -class CustomSyncWorkerTest : RobolectricTest() { - - @kotlinx.coroutines.ExperimentalCoroutinesApi - private val resourceService: FhirResourceService = mockk() - - @OptIn(ExperimentalCoroutinesApi::class) - private var fhirResourceDataSource: FhirResourceDataSource = - spyk(FhirResourceDataSource(resourceService)) - - @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 1) val coroutineTestRule = CoroutineTestRule() - - private lateinit var sharedPreferencesHelper: SharedPreferencesHelper - - @Inject lateinit var dispatcherProvider: DispatcherProvider - - private lateinit var configurationRegistry: ConfigurationRegistry - private lateinit var customSyncWorker: CustomSyncWorker - - @Before - @kotlinx.coroutines.ExperimentalCoroutinesApi - fun setUp() { - hiltRule.inject() - initializeWorkManager() - } - - @Test - fun `should create sync worker with expected properties`() { - sharedPreferencesHelper = - SharedPreferencesHelper(ApplicationProvider.getApplicationContext(), Gson()) - configurationRegistry = Faker.buildTestConfigurationRegistry(sharedPreferencesHelper) - - customSyncWorker = - TestListenableWorkerBuilder( - ApplicationProvider.getApplicationContext(), - ) - .setWorkerFactory(CustomSyncWorkerFactory()) - .build() - - val expected = runBlocking { customSyncWorker.doWork() } - - Assert.assertEquals(ListenableWorker.Result.success(), expected) - } - - @Test - fun `should create sync worker with organization`() = runTest { - sharedPreferencesHelper = - SharedPreferencesHelper(ApplicationProvider.getApplicationContext(), Gson()) - - val organizationId1 = "organization-id1" - val organizationId2 = "organization-id2" - sharedPreferencesHelper.write( - ResourceType.Organization.name, - listOf(organizationId1, organizationId2), - ) - val locationId = UUID.randomUUID().toString() - sharedPreferencesHelper.context.syncLocationIdsProtoStore.updateData { - mapOf( - locationId to SyncLocationState(locationId, null, ToggleableState.On), - ) - } - configurationRegistry = Faker.buildTestConfigurationRegistry(sharedPreferencesHelper) - - customSyncWorker = - TestListenableWorkerBuilder( - ApplicationProvider.getApplicationContext(), - ) - .setWorkerFactory(CustomSyncWorkerFactory()) - .build() - - val expected = runBlocking { customSyncWorker.doWork() } - - Assert.assertEquals(ListenableWorker.Result.success(), expected) - } - - private fun writePrefs(key: String, value: String) { - if (::sharedPreferencesHelper.isInitialized) { - sharedPreferencesHelper.write(key, value) - } - } - - @Test - fun `should create sync worker with failure results`() { - configurationRegistry = Faker.buildTestConfigurationRegistry() - - customSyncWorker = - TestListenableWorkerBuilder( - ApplicationProvider.getApplicationContext(), - ) - .setWorkerFactory(CustomSyncWorkerFactory()) - .build() - - val expected = runBlocking { customSyncWorker.doWork() } - - Assert.assertEquals(ListenableWorker.Result.failure(), expected) - } - - private fun initializeWorkManager() { - val config: Configuration = - Configuration.Builder() - .setMinimumLoggingLevel(Log.DEBUG) - .setExecutor(SynchronousExecutor()) - .build() - - // Initialize WorkManager for instrumentation tests. - WorkManagerTestInitHelper.initializeTestWorkManager( - ApplicationProvider.getApplicationContext(), - config, - ) - } - - inner class CustomSyncWorkerFactory : WorkerFactory() { - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters, - ): ListenableWorker { - return CustomSyncWorker( - appContext = appContext, - workerParams = workerParameters, - configurationRegistry = configurationRegistry, - dispatcherProvider = dispatcherProvider, - fhirResourceDataSource = fhirResourceDataSource, - ) - } - } -} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt index 61205e606b7..7001c208fab 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt @@ -51,7 +51,6 @@ import org.hl7.fhir.r4.model.ResourceType import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration -import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager import org.smartregister.fhircore.engine.ui.base.AlertDialogButton @@ -211,21 +210,9 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { } override fun onSync(syncJobStatus: CurrentSyncJobStatus) { - onSync(syncJobStatus, isCustomSync = false) - } - - private fun onSync(syncJobStatus: CurrentSyncJobStatus, isCustomSync: Boolean) { when (syncJobStatus) { is CurrentSyncJobStatus.Running -> { - if (syncJobStatus.inProgressSyncJob is SyncJobStatus.Started) { - lifecycleScope.launch { - if (isCustomSync) { - geoWidgetLauncherViewModel.emitSnackBarState( - SnackBarMessageConfig(message = getString(R.string.syncing_custom_resources_toast)), - ) - } - } - } else { + if (syncJobStatus.inProgressSyncJob is SyncJobStatus.InProgress) { val inProgressSyncJob = syncJobStatus.inProgressSyncJob as SyncJobStatus.InProgress val isSyncUpload = inProgressSyncJob.syncOperation == SyncOperation.UPLOAD val progressPercentage = appMainViewModel.calculatePercentageProgress(inProgressSyncJob) @@ -256,11 +243,6 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - configurationRegistry.syncState - .onEach { syncJobStatus -> onSync(syncJobStatus, true) } - .launchIn(this) - } launch { eventBus.events .getFor(MainNavigationScreen.GeoWidgetLauncher.eventId(navArgs.geoWidgetId)) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt index 1ea75e601f1..42fc7638bf2 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt @@ -59,7 +59,6 @@ import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger import org.smartregister.fhircore.engine.data.local.register.RegisterRepository import org.smartregister.fhircore.engine.domain.model.LauncherType import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction -import org.smartregister.fhircore.engine.sync.CustomSyncWorker import org.smartregister.fhircore.engine.sync.SyncBroadcaster import org.smartregister.fhircore.engine.task.FhirCarePlanGenerator import org.smartregister.fhircore.engine.task.FhirCompleteCarePlanWorker @@ -237,12 +236,10 @@ constructor( } } is AppMainEvent.CancelSyncData -> { - viewModelScope.launch { - workManager.cancelUniqueWork( - "org.smartregister.fhircore.engine.sync.AppSyncWorker-oneTimeSync", - ) - updateAppDrawerUIState(currentSyncJobStatus = CurrentSyncJobStatus.Cancelled) - } + workManager.cancelUniqueWork( + "org.smartregister.fhircore.engine.sync.AppSyncWorker-oneTimeSync", + ) + updateAppDrawerUIState(currentSyncJobStatus = CurrentSyncJobStatus.Cancelled) } is AppMainEvent.OpenRegistersBottomSheet -> displayRegisterBottomSheet(event) is AppMainEvent.UpdateSyncState -> { @@ -468,12 +465,6 @@ constructor( initialDelay = INITIAL_DELAY, ) - schedulePeriodically( - workId = CustomSyncWorker.WORK_ID, - repeatInterval = applicationConfiguration.syncInterval, - initialDelay = 0, - ) - measureReportConfigurations.forEach { measureReportConfig -> measureReportConfig.scheduledGenerationDuration?.let { scheduledGenerationDuration -> schedulePeriodically( diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt index e4ca10838bb..cc63160c9c0 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt @@ -53,11 +53,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.hl7.fhir.r4.model.QuestionnaireResponse import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry -import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager import org.smartregister.fhircore.engine.ui.theme.AppTheme -import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.event.AppEvent import org.smartregister.fhircore.quest.event.EventBus import org.smartregister.fhircore.quest.navigation.MainNavigationScreen @@ -198,21 +196,9 @@ class RegisterFragment : Fragment(), OnSyncListener { } override fun onSync(syncJobStatus: CurrentSyncJobStatus) { - onSync(syncJobStatus, isCustomSync = false) - } - - private fun onSync(syncJobStatus: CurrentSyncJobStatus, isCustomSync: Boolean) { when (syncJobStatus) { is CurrentSyncJobStatus.Running -> { - if (syncJobStatus.inProgressSyncJob is SyncJobStatus.Started) { - lifecycleScope.launch { - if (isCustomSync) { - registerViewModel.emitSnackBarState( - SnackBarMessageConfig(message = getString(R.string.syncing_custom_resources_toast)), - ) - } - } - } else { + if (syncJobStatus.inProgressSyncJob is SyncJobStatus.InProgress) { val inProgressSyncJob = syncJobStatus.inProgressSyncJob as SyncJobStatus.InProgress val isSyncUpload = inProgressSyncJob.syncOperation == SyncOperation.UPLOAD val progressPercentage = appMainViewModel.calculatePercentageProgress(inProgressSyncJob) @@ -257,11 +243,6 @@ class RegisterFragment : Fragment(), OnSyncListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - configurationRegistry.syncState - .onEach { syncJobStatus -> onSync(syncJobStatus, true) } - .launchIn(this) - } // Each register should have unique eventId launch { eventBus.events diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml index a39950895a5..4e1f1588a9c 100644 --- a/android/quest/src/main/res/values/strings.xml +++ b/android/quest/src/main/res/values/strings.xml @@ -144,5 +144,4 @@ Error rendering profile Are you sure you want to submit? You are about to submit - Syncing custom Resources... From 6d5d5f86d87386c2af0cf2df590dfa980e556514 Mon Sep 17 00:00:00 2001 From: Elly Kitoto Date: Fri, 31 Jan 2025 13:21:09 +0300 Subject: [PATCH 12/16] Refactor sync notification UI Harmonize custom resource and app sync. Each sync is identified by a counter. The max sync count is 2. Signed-off-by: Elly Kitoto --- .../configuration/ConfigurationRegistry.kt | 6 ++ .../fhircore/engine/sync/AppSyncWorker.kt | 46 ++++++-- .../engine/sync/CustomResourceSyncService.kt | 100 ++++++++---------- .../fhircore/engine/sync/OnSyncListener.kt | 6 +- .../fhircore/engine/sync/SyncBroadcaster.kt | 30 ++++-- .../engine/sync/SyncListenerManager.kt | 5 +- .../fhircore/engine/sync/SyncState.kt | 24 +++++ .../engine/util/SharedPreferenceKey.kt | 1 + .../engine/util/test/HiltActivityForTest.kt | 4 +- .../ui/register/RegisterScreenTest.kt | 43 +------- .../ui/geowidget/GeoWidgetLauncherFragment.kt | 26 +++-- .../ui/geowidget/GeoWidgetLauncherScreen.kt | 1 + .../fhircore/quest/ui/main/AppMainActivity.kt | 21 ++-- .../fhircore/quest/ui/main/AppMainEvent.kt | 7 +- .../quest/ui/main/AppMainViewModel.kt | 40 +++++-- .../quest/ui/main/components/AppDrawer.kt | 9 ++ .../quest/ui/register/RegisterFragment.kt | 26 +++-- .../quest/ui/register/RegisterScreen.kt | 1 + .../ui/shared/components/SyncStatusView.kt | 54 ++++++++-- .../ui/shared/models/AppDrawerUIState.kt | 2 + .../ui/usersetting/UserSettingFragment.kt | 10 +- android/quest/src/main/res/values/strings.xml | 1 + 22 files changed, 307 insertions(+), 156 deletions(-) create mode 100644 android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncState.kt diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index 9a095ccb7ee..763b063964c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -842,6 +842,12 @@ constructor( } } } + + // If there are custom resources to be synced return 2 otherwise 1 + sharedPreferencesHelper.write( + SharedPreferenceKey.TOTAL_SYNC_COUNT.name, + if (customResourceSearchParams.isEmpty()) "1" else "2", + ) return Pair(customResourceSearchParams, fhirResourceSearchParams) } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt index f725f09f4e2..5fd1e77b9cf 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt @@ -41,11 +41,14 @@ import com.google.android.fhir.sync.upload.UploadStrategy import com.ibm.icu.util.Calendar import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import java.time.OffsetDateTime import kotlinx.coroutines.runBlocking import org.smartregister.fhircore.engine.R import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.util.NotificationConstants import org.smartregister.fhircore.engine.util.SharedPreferenceKey +import retrofit2.HttpException +import timber.log.Timber @HiltWorker class AppSyncWorker @@ -75,12 +78,39 @@ constructor( ) override suspend fun doWork(): Result { - saveSyncStartTimestamp() - setForeground(getForegroundInfo()) - customResourceSyncService.runCustomResourceSync() - return super.doWork() + kotlin + .runCatching { + saveSyncStartTimestamp() + setForeground(getForegroundInfo()) + customResourceSyncService.runCustomResourceSync() + } + .onSuccess { + return super.doWork() + } + .onFailure { exception -> + when (exception) { + is HttpException -> { + val response = exception.response() + if (response != null && (400..503).contains(response.code())) { + Timber.e("HTTP exception ${response.code()} -> ${response.errorBody()}") + } + } + else -> Timber.e(exception) + } + syncListenerManager.emitSyncStatus( + SyncState( + counter = SYNC_COUNTER_1, + currentSyncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()), + ), + ) + return result() + } + return Result.success() } + private fun result(): Result = + if (inputData.getInt(MAX_RETRIES, 3) > runAttemptCount) Result.retry() else Result.failure() + private fun saveSyncStartTimestamp() { syncListenerManager.sharedPreferencesHelper.write( SharedPreferenceKey.SYNC_START_TIMESTAMP.name, @@ -133,8 +163,8 @@ constructor( private fun getSyncProgress(completed: Int, total: Int) = completed * 100 / if (total > 0) total else 1 - override fun onSync(syncJobStatus: CurrentSyncJobStatus) { - when (syncJobStatus) { + override fun onSync(syncState: SyncState) { + when (val syncJobStatus = syncState.currentSyncJobStatus) { is CurrentSyncJobStatus.Running -> { if (syncJobStatus.inProgressSyncJob is SyncJobStatus.InProgress) { val inProgressSyncJob = syncJobStatus.inProgressSyncJob as SyncJobStatus.InProgress @@ -177,4 +207,8 @@ constructor( buildNotification(progress = progress, isSyncUpload = isSyncUpload, isInitial = false) notificationManager.notify(NotificationConstants.NotificationId.DATA_SYNC, notification) } + + companion object { + const val MAX_RETRIES = "max_retires" + } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomResourceSyncService.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomResourceSyncService.kt index 7db761c4bad..a75efd16f14 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomResourceSyncService.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomResourceSyncService.kt @@ -20,7 +20,6 @@ import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.fhir.sync.SyncJobStatus import com.google.android.fhir.sync.SyncOperation import com.google.android.fhir.sync.concatParams -import java.net.UnknownHostException import java.time.OffsetDateTime import javax.inject.Inject import javax.inject.Singleton @@ -28,8 +27,6 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry.Companion.PAGINATION_NEXT import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource import org.smartregister.fhircore.engine.util.DispatcherProvider -import retrofit2.HttpException -import retrofit2.Response import timber.log.Timber @Singleton @@ -42,42 +39,23 @@ constructor( val syncListenerManager: SyncListenerManager, ) { suspend fun runCustomResourceSync() { - try { - with(configurationRegistry) { - val (resourceSearchParams, _) = loadResourceSearchParams() - if (resourceSearchParams.isEmpty()) return + val (resourceSearchParams, _) = configurationRegistry.loadResourceSearchParams() + if (resourceSearchParams.isEmpty()) return - // Process resource URLs - val resourceUrls = - resourceSearchParams - .asIterable() - .filter { it.value.isNotEmpty() } - .map { "${it.key}?${it.value.concatParams()}" } + val resourceUrls = + resourceSearchParams + .asIterable() + .filter { it.value.isNotEmpty() } + .map { "${it.key}?${it.value.concatParams()}" } - // Fetch summary count first - val summaryCount = fetchSummaryCount(resourceUrls).values.sumOf { it ?: 0 } - Timber.d("Fetched summary count: $summaryCount") + val summaryCount = fetchSummaryCount(resourceUrls).values.sumOf { it ?: 0 } - // Fetch resources - resourceUrls.forEach { url -> - fetchCustomResources( - gatewayModeHeaderValue = ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE, - url = url, - totalCounts = summaryCount, - ) - } - } - } catch (httpException: HttpException) { - Timber.e(httpException) - val response: Response<*>? = httpException.response() - if (response != null && (400..503).contains(response.code())) { - Timber.e("HTTP exception ${response.code()} -> ${response.errorBody()}") - } - } catch (unknownHostException: UnknownHostException) { - Timber.e(unknownHostException) - } catch (exception: Exception) { - Timber.e(exception) - syncListenerManager.emitSyncStatus(CurrentSyncJobStatus.Failed(OffsetDateTime.now())) + resourceUrls.forEach { url -> + fetchCustomResources( + gatewayModeHeaderValue = ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE, + url = url, + totalCounts = summaryCount, + ) } } @@ -90,12 +68,16 @@ constructor( runCatching { Timber.d("Setting state: Running") syncListenerManager.emitSyncStatus( - CurrentSyncJobStatus.Running( - SyncJobStatus.InProgress( - syncOperation = SyncOperation.DOWNLOAD, - total = totalCounts, - completed = completedRecords, - ), + SyncState( + counter = SYNC_COUNTER_1, + currentSyncJobStatus = + CurrentSyncJobStatus.Running( + SyncJobStatus.InProgress( + syncOperation = SyncOperation.DOWNLOAD, + total = totalCounts, + completed = completedRecords, + ), + ), ), ) Timber.d("Fetching page with URL: $url") @@ -107,19 +89,28 @@ constructor( } .onFailure { throwable -> Timber.e("Error occurred while retrieving resource via URL $url", throwable) - syncListenerManager.emitSyncStatus(CurrentSyncJobStatus.Failed(OffsetDateTime.now())) + syncListenerManager.emitSyncStatus( + SyncState( + counter = SYNC_COUNTER_1, + currentSyncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()), + ), + ) return } .onSuccess { resultBundle -> configurationRegistry.processResultBundleEntries(resultBundle.entry) val newCompletedRecords = completedRecords + resultBundle.entry.size syncListenerManager.emitSyncStatus( - CurrentSyncJobStatus.Running( - SyncJobStatus.InProgress( - syncOperation = SyncOperation.DOWNLOAD, - total = totalCounts, - completed = newCompletedRecords, - ), + SyncState( + counter = SYNC_COUNTER_1, + currentSyncJobStatus = + CurrentSyncJobStatus.Running( + SyncJobStatus.InProgress( + syncOperation = SyncOperation.DOWNLOAD, + total = totalCounts, + completed = newCompletedRecords, + ), + ), ), ) @@ -134,7 +125,12 @@ constructor( ) } else { Timber.d("Fetch complete. Emitting SyncStatus.Succeeded.") - syncListenerManager.emitSyncStatus(CurrentSyncJobStatus.Succeeded(OffsetDateTime.now())) + syncListenerManager.emitSyncStatus( + SyncState( + counter = SYNC_COUNTER_1, + currentSyncJobStatus = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), + ), + ) } } } @@ -152,8 +148,4 @@ constructor( summaryUrl to total } .also { summaries -> Timber.i("Summary fetch results: $summaries") } - - companion object { - const val WORK_ID = "CustomResourceSyncWorker" - } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/OnSyncListener.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/OnSyncListener.kt index 66e2a05098d..b097eb7cf84 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/OnSyncListener.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/OnSyncListener.kt @@ -19,10 +19,10 @@ package org.smartregister.fhircore.engine.sync import com.google.android.fhir.sync.CurrentSyncJobStatus /** - * An interface the exposes a callback method [onSync] which accepts an application level FHIR - * [CurrentSyncJobStatus]. + * An interface the exposes a callback method [onSync] which handles the [CurrentSyncJobStatus] + * emitted via the sync job. */ interface OnSyncListener { /** Callback method invoked to handle sync [CurrentSyncJobStatus] */ - fun onSync(syncJobStatus: CurrentSyncJobStatus) + fun onSync(syncState: SyncState) } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt index b5db77630d0..c77668e42aa 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt @@ -107,14 +107,21 @@ constructor( private fun Flow.handlePeriodicSyncJobStatus( coroutineScope: CoroutineScope, ) { - this.onEach { + this.onEach { periodicSyncJobStatus -> syncListenerManager.onSyncListeners.forEach { onSyncListener -> - onSyncListener.onSync( - if (it.lastSyncJobStatus as? LastSyncJobStatus.Succeeded != null) { - CurrentSyncJobStatus.Succeeded((it.lastSyncJobStatus as LastSyncJobStatus).timestamp) + val currentSyncJobStatus = + if (periodicSyncJobStatus.lastSyncJobStatus as? LastSyncJobStatus.Succeeded != null) { + CurrentSyncJobStatus.Succeeded( + (periodicSyncJobStatus.lastSyncJobStatus as LastSyncJobStatus).timestamp, + ) } else { - it.currentSyncJobStatus - }, + periodicSyncJobStatus.currentSyncJobStatus + } + onSyncListener.onSync( + SyncState( + counter = SYNC_COUNTER_2, + currentSyncJobStatus = currentSyncJobStatus, + ), ) } } @@ -126,8 +133,15 @@ constructor( private fun Flow.handleOneTimeSyncJobStatus( coroutineScope: CoroutineScope, ) { - this.onEach { - syncListenerManager.onSyncListeners.forEach { onSyncListener -> onSyncListener.onSync(it) } + this.onEach { currentSyncJobStatus -> + syncListenerManager.onSyncListeners.forEach { onSyncListener -> + onSyncListener.onSync( + SyncState( + counter = SYNC_COUNTER_2, + currentSyncJobStatus = currentSyncJobStatus, + ), + ) + } } .catch { throwable -> Timber.e("Encountered an error during one time sync:", throwable) } .shareIn(coroutineScope, SharingStarted.Eagerly, 1) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncListenerManager.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncListenerManager.kt index ea9512b58ff..8be1f98d8a4 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncListenerManager.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncListenerManager.kt @@ -20,7 +20,6 @@ import android.content.Context import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner -import com.google.android.fhir.sync.CurrentSyncJobStatus import com.google.android.fhir.sync.SyncJobStatus import com.google.android.fhir.sync.download.ResourceSearchParams import dagger.hilt.android.qualifiers.ApplicationContext @@ -107,7 +106,7 @@ constructor( return resourceSearchParams } - fun emitSyncStatus(currentSyncJobStatus: CurrentSyncJobStatus) { - _onSyncListeners.forEach { it.get()?.onSync(currentSyncJobStatus) } + fun emitSyncStatus(syncState: SyncState) { + _onSyncListeners.forEach { it.get()?.onSync(syncState) } } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncState.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncState.kt new file mode 100644 index 00000000000..936c408ef0c --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncState.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * 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 org.smartregister.fhircore.engine.sync + +import com.google.android.fhir.sync.CurrentSyncJobStatus + +const val SYNC_COUNTER_1 = 1 +const val SYNC_COUNTER_2 = 2 + +data class SyncState(val counter: Int, val currentSyncJobStatus: CurrentSyncJobStatus) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferenceKey.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferenceKey.kt index 7e61da32a14..922577c9e01 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferenceKey.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/SharedPreferenceKey.kt @@ -37,4 +37,5 @@ enum class SharedPreferenceKey { SYNC_START_TIMESTAMP, SYNC_END_TIMESTAMP, LAST_CONFIG_SYNC_TIMESTAMP, + TOTAL_SYNC_COUNT, } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/test/HiltActivityForTest.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/test/HiltActivityForTest.kt index 5dd0f6ec014..2efef53a169 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/test/HiltActivityForTest.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/test/HiltActivityForTest.kt @@ -18,10 +18,10 @@ package org.smartregister.fhircore.engine.util.test import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import com.google.android.fhir.sync.CurrentSyncJobStatus import dagger.hilt.android.AndroidEntryPoint import org.smartregister.fhircore.engine.R import org.smartregister.fhircore.engine.sync.OnSyncListener +import org.smartregister.fhircore.engine.sync.SyncState import org.smartregister.fhircore.engine.util.annotation.ExcludeFromJacocoGeneratedReport @ExcludeFromJacocoGeneratedReport @@ -35,7 +35,7 @@ class HiltActivityForTest : AppCompatActivity(), OnSyncListener { super.onCreate(savedInstanceState) } - override fun onSync(syncJobStatus: CurrentSyncJobStatus) { + override fun onSync(syncState: SyncState) { // DO nothing. This activity implements OnSyncListener for testing purposes } diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt index 0ffe52cc1dd..5b3fe7cec05 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/RegisterScreenTest.kt @@ -51,18 +51,13 @@ import org.junit.Rule import org.junit.Test import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry -import org.smartregister.fhircore.engine.configuration.navigation.NavigationBottomSheetRegisterConfig -import org.smartregister.fhircore.engine.configuration.navigation.NavigationConfiguration -import org.smartregister.fhircore.engine.configuration.navigation.NavigationMenuConfig import org.smartregister.fhircore.engine.configuration.register.NoResultsConfig import org.smartregister.fhircore.engine.configuration.register.RegisterConfiguration import org.smartregister.fhircore.engine.configuration.register.RegisterContentConfig import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger import org.smartregister.fhircore.engine.domain.model.ActionConfig -import org.smartregister.fhircore.engine.domain.model.Language import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.quest.integration.Faker -import org.smartregister.fhircore.quest.ui.main.appMainUiStateOf import org.smartregister.fhircore.quest.ui.register.FAB_BUTTON_REGISTER_TEST_TAG import org.smartregister.fhircore.quest.ui.register.FIRST_TIME_SYNC_DIALOG import org.smartregister.fhircore.quest.ui.register.NO_REGISTER_VIEW_COLUMN_TEST_TAG @@ -84,41 +79,6 @@ class RegisterScreenTest { private val applicationContext = ApplicationProvider.getApplicationContext() - private val navigationConfiguration = - NavigationConfiguration( - appId = "appId", - configType = ConfigType.Navigation.name, - staticMenu = listOf(), - clientRegisters = - listOf( - NavigationMenuConfig(id = "id3", visible = true, display = "Register 1"), - NavigationMenuConfig(id = "id4", visible = false, display = "Register 2"), - ), - bottomSheetRegisters = - NavigationBottomSheetRegisterConfig( - visible = true, - display = "My Register", - registers = - listOf(NavigationMenuConfig(id = "id2", visible = true, display = "Title My Register")), - ), - menuActionButton = - NavigationMenuConfig(id = "id1", visible = true, display = "Register Household"), - ) - - private val appUiState = - appMainUiStateOf( - appTitle = "MOH VTS", - username = "Demo", - lastSyncTime = "05:30 PM, Mar 3", - currentLanguage = "English", - languages = listOf(Language("en", "English"), Language("sw", "Swahili")), - navigationConfiguration = - navigationConfiguration.copy( - bottomSheetRegisters = - navigationConfiguration.bottomSheetRegisters?.copy(display = "Random name"), - ), - ) - @Test fun testFloatingActionButtonIsDisplayed() { val configurationRegistry: ConfigurationRegistry = Faker.buildTestConfigurationRegistry() @@ -552,6 +512,7 @@ class RegisterScreenTest { ), appDrawerUIState = AppDrawerUIState( + totalSyncCount = 1, currentSyncJobStatus = CurrentSyncJobStatus.Running( SyncJobStatus.InProgress( @@ -609,6 +570,7 @@ class RegisterScreenTest { ), appDrawerUIState = AppDrawerUIState( + totalSyncCount = 1, currentSyncJobStatus = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), ), onAppMainEvent = {}, @@ -664,6 +626,7 @@ class RegisterScreenTest { ), appDrawerUIState = AppDrawerUIState( + totalSyncCount = 1, currentSyncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()), ), onAppMainEvent = {}, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt index 7001c208fab..9437aed9752 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt @@ -53,6 +53,7 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager +import org.smartregister.fhircore.engine.sync.SyncState import org.smartregister.fhircore.engine.ui.base.AlertDialogButton import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.ui.base.AlertIntent @@ -209,8 +210,8 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { syncListenerManager.registerSyncListener(this, lifecycle) } - override fun onSync(syncJobStatus: CurrentSyncJobStatus) { - when (syncJobStatus) { + override fun onSync(syncState: SyncState) { + when (val syncJobStatus = syncState.currentSyncJobStatus) { is CurrentSyncJobStatus.Running -> { if (syncJobStatus.inProgressSyncJob is SyncJobStatus.InProgress) { val inProgressSyncJob = syncJobStatus.inProgressSyncJob as SyncJobStatus.InProgress @@ -218,16 +219,23 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { val progressPercentage = appMainViewModel.calculatePercentageProgress(inProgressSyncJob) appMainViewModel.updateAppDrawerUIState( isSyncUpload = isSyncUpload, + syncCounter = syncState.counter, currentSyncJobStatus = syncJobStatus, percentageProgress = progressPercentage, ) } } is CurrentSyncJobStatus.Succeeded -> { - appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) + appMainViewModel.updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) } is CurrentSyncJobStatus.Failed -> { - appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) + appMainViewModel.updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) geoWidgetLauncherViewModel.onEvent( GeoWidgetEvent.RetrieveFeatures( geoWidgetConfig = geoWidgetConfiguration, @@ -235,7 +243,11 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { ), ) } - else -> appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) + else -> + appMainViewModel.updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) } } @@ -302,11 +314,11 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { override fun onPause() { super.onPause() - appMainViewModel.updateAppDrawerUIState(false, null, 0) + appMainViewModel.updateAppDrawerUIState(false, null, null, 0) } override fun onDestroy() { super.onDestroy() - appMainViewModel.updateAppDrawerUIState(false, null, 0) + appMainViewModel.updateAppDrawerUIState(false, null, null, 0) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt index 516a7c460cb..f363c98cfdf 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt @@ -122,6 +122,7 @@ fun GeoWidgetLauncherScreen( }, bottomBar = { SyncBottomBar( + totalSyncCount = appDrawerUIState.totalSyncCount, isFirstTimeSync = isFirstTimeSync, appDrawerUIState = appDrawerUIState, onAppMainEvent = onAppMainEvent, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt index 6f446cc57af..bee5772e618 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt @@ -52,6 +52,7 @@ import org.smartregister.fhircore.engine.domain.model.LauncherType import org.smartregister.fhircore.engine.rulesengine.services.LocationCoordinate import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager +import org.smartregister.fhircore.engine.sync.SyncState import org.smartregister.fhircore.engine.ui.base.AlertDialogButton import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.ui.base.AlertIntent @@ -278,27 +279,35 @@ open class AppMainActivity : BaseMultiLanguageActivity(), QuestionnaireHandler, } } - override fun onSync(syncJobStatus: CurrentSyncJobStatus) { - when (syncJobStatus) { + override fun onSync(syncState: SyncState) { + when (val syncJobStatus = syncState.currentSyncJobStatus) { is CurrentSyncJobStatus.Succeeded -> appMainViewModel.run { onEvent( AppMainEvent.UpdateSyncState( - state = syncJobStatus, + syncCounter = syncState.counter, + currentSyncJobStatus = syncState.currentSyncJobStatus, lastSyncTime = formatLastSyncTimestamp(syncJobStatus.timestamp), ), ) - appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) + appMainViewModel.updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) } is CurrentSyncJobStatus.Failed -> appMainViewModel.run { onEvent( AppMainEvent.UpdateSyncState( - state = syncJobStatus, + syncCounter = syncState.counter, + currentSyncJobStatus = syncState.currentSyncJobStatus, lastSyncTime = formatLastSyncTimestamp(syncJobStatus.timestamp), ), ) - updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) + updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) } else -> { // Do Nothing diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainEvent.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainEvent.kt index e8cc830c469..84d0e55cec3 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainEvent.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainEvent.kt @@ -33,8 +33,11 @@ sealed class AppMainEvent { val registersList: List?, ) : AppMainEvent() - data class UpdateSyncState(val state: CurrentSyncJobStatus, val lastSyncTime: String?) : - AppMainEvent() + data class UpdateSyncState( + val syncCounter: Int, + val currentSyncJobStatus: CurrentSyncJobStatus, + val lastSyncTime: String?, + ) : AppMainEvent() data class TriggerWorkflow(val navController: NavController, val navMenu: NavigationMenuConfig) : AppMainEvent() diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt index 42fc7638bf2..bf34cb84d52 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt @@ -45,6 +45,7 @@ import kotlin.time.Duration import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.hl7.fhir.r4.model.Parameters import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Task import org.smartregister.fhircore.engine.R @@ -112,13 +113,8 @@ constructor( ), ), ) - private val simpleDateFormat = SimpleDateFormat(SYNC_TIMESTAMP_OUTPUT_FORMAT, Locale.getDefault()) - private val registerCountMap: SnapshotStateMap = mutableStateMapOf() - val appDrawerUiState = mutableStateOf(AppDrawerUIState()) - val resetRegisterFilters = MutableLiveData(false) - val unSyncedResourcesCount = mutableIntStateOf(0) val applicationConfiguration: ApplicationConfiguration by lazy { @@ -133,6 +129,9 @@ constructor( configurationRegistry.retrieveConfigurations(ConfigType.MeasureReport) } + private val simpleDateFormat = SimpleDateFormat(SYNC_TIMESTAMP_OUTPUT_FORMAT, Locale.getDefault()) + private val registerCountMap: SnapshotStateMap = mutableStateMapOf() + fun retrieveAppMainUiState(refreshAll: Boolean = true) { if (refreshAll) { appMainUiState.value = @@ -239,14 +238,17 @@ constructor( workManager.cancelUniqueWork( "org.smartregister.fhircore.engine.sync.AppSyncWorker-oneTimeSync", ) - updateAppDrawerUIState(currentSyncJobStatus = CurrentSyncJobStatus.Cancelled) + updateAppDrawerUIState( + syncCounter = null, + currentSyncJobStatus = CurrentSyncJobStatus.Cancelled, + ) } is AppMainEvent.OpenRegistersBottomSheet -> displayRegisterBottomSheet(event) is AppMainEvent.UpdateSyncState -> { - if (event.state is CurrentSyncJobStatus.Succeeded) { + if (event.currentSyncJobStatus is CurrentSyncJobStatus.Succeeded) { sharedPreferencesHelper.write( SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, - event.state.timestamp.toInstant().toEpochMilli().toString(), + event.currentSyncJobStatus.timestamp.toInstant().toEpochMilli().toString(), ) retrieveAppMainUiState() viewModelScope.launch { retrieveAppMainUiState() } @@ -316,7 +318,7 @@ constructor( fun retrieveLastSyncTimestamp(): String? = sharedPreferencesHelper.read(SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, null) - fun schedulePeriodicSync() { + private fun schedulePeriodicSync() { viewModelScope.launch { syncBroadcaster.schedulePeriodicSync(applicationConfiguration.syncInterval) } @@ -398,12 +400,15 @@ constructor( fun updateAppDrawerUIState( isSyncUpload: Boolean? = null, + syncCounter: Int?, currentSyncJobStatus: CurrentSyncJobStatus?, percentageProgress: Int? = null, ) { appDrawerUiState.value = AppDrawerUIState( isSyncUpload = isSyncUpload, + syncCounter = syncCounter, + totalSyncCount = retrieveTotalSyncCount(), currentSyncJobStatus = currentSyncJobStatus, percentageProgress = percentageProgress, ) @@ -482,6 +487,23 @@ constructor( } } + /** + * This function returns either '1' or '2' depending on whether there are custom resources (not + * included in ResourceType enum) in the sync configuration. The custom resources are configured + * in the sync configuration JSON file as valid FHIR SearchParameter of type 'special'. If there + * are custom resources to be synced with the data, the application will first download the custom + * resources then the rest of the app data. + */ + private fun retrieveTotalSyncCount(): Int { + val totalSyncCount = sharedPreferencesHelper.read(SharedPreferenceKey.TOTAL_SYNC_COUNT.name, "") + return if (totalSyncCount.isNullOrBlank()) { + configurationRegistry + .retrieveResourceConfiguration(ConfigType.Sync) + .parameter + .count { it.hasType() } + } else totalSyncCount.toInt() + } + companion object { private const val INITIAL_DELAY = 15L const val SYNC_TIMESTAMP_INPUT_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt index 600ca523f66..7b833ec8cdd 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt @@ -266,6 +266,8 @@ private fun NavBottomSection( is CurrentSyncJobStatus.Running -> { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, + syncCounter = appDrawerUIState.syncCounter, + totalSyncCount = appDrawerUIState.totalSyncCount, currentSyncJobStatus = currentSyncJobStatus, minimized = false, progressPercentage = appDrawerUIState.percentageProgress, @@ -279,6 +281,8 @@ private fun NavBottomSection( is CurrentSyncJobStatus.Failed -> { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, + syncCounter = appDrawerUIState.syncCounter, + totalSyncCount = appDrawerUIState.totalSyncCount, currentSyncJobStatus = currentSyncJobStatus, minimized = false, ) { @@ -305,6 +309,8 @@ private fun NavBottomSection( } else { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, + syncCounter = appDrawerUIState.syncCounter, + totalSyncCount = appDrawerUIState.totalSyncCount, currentSyncJobStatus = currentSyncJobStatus, minimized = false, ) @@ -700,6 +706,7 @@ fun AppDrawerOnSyncCompletePreview() { ), appDrawerUIState = AppDrawerUIState( + syncCounter = 1, currentSyncJobStatus = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), ), navController = rememberNavController(), @@ -743,6 +750,7 @@ fun AppDrawerOnSyncFailedPreview() { ), appDrawerUIState = AppDrawerUIState( + syncCounter = 1, currentSyncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()), ), navController = rememberNavController(), @@ -786,6 +794,7 @@ fun AppDrawerOnSyncRunningPreview() { ), appDrawerUIState = AppDrawerUIState( + syncCounter = 1, currentSyncJobStatus = CurrentSyncJobStatus.Running(SyncJobStatus.InProgress(SyncOperation.DOWNLOAD, 200, 35)), ), diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt index cc63160c9c0..7d9df7aa3ee 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt @@ -55,6 +55,7 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager +import org.smartregister.fhircore.engine.sync.SyncState import org.smartregister.fhircore.engine.ui.theme.AppTheme import org.smartregister.fhircore.quest.event.AppEvent import org.smartregister.fhircore.quest.event.EventBus @@ -195,8 +196,8 @@ class RegisterFragment : Fragment(), OnSyncListener { syncListenerManager.registerSyncListener(this, lifecycle) } - override fun onSync(syncJobStatus: CurrentSyncJobStatus) { - when (syncJobStatus) { + override fun onSync(syncState: SyncState) { + when (val syncJobStatus = syncState.currentSyncJobStatus) { is CurrentSyncJobStatus.Running -> { if (syncJobStatus.inProgressSyncJob is SyncJobStatus.InProgress) { val inProgressSyncJob = syncJobStatus.inProgressSyncJob as SyncJobStatus.InProgress @@ -204,6 +205,7 @@ class RegisterFragment : Fragment(), OnSyncListener { val progressPercentage = appMainViewModel.calculatePercentageProgress(inProgressSyncJob) appMainViewModel.updateAppDrawerUIState( isSyncUpload = isSyncUpload, + syncCounter = syncState.counter, currentSyncJobStatus = syncJobStatus, percentageProgress = progressPercentage, ) @@ -211,13 +213,23 @@ class RegisterFragment : Fragment(), OnSyncListener { } is CurrentSyncJobStatus.Succeeded -> { refreshRegisterData() - appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) + appMainViewModel.updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) } is CurrentSyncJobStatus.Failed -> { refreshRegisterData() - appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) + appMainViewModel.updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) } - else -> appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) + else -> + appMainViewModel.updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) } } @@ -273,12 +285,12 @@ class RegisterFragment : Fragment(), OnSyncListener { override fun onPause() { super.onPause() - appMainViewModel.updateAppDrawerUIState(false, null, 0) + appMainViewModel.updateAppDrawerUIState(false, null, null, 0) } override fun onDestroy() { super.onDestroy() - appMainViewModel.updateAppDrawerUIState(false, null, 0) + appMainViewModel.updateAppDrawerUIState(false, null, null, 0) } suspend fun handleQuestionnaireSubmission(questionnaireSubmission: QuestionnaireSubmission) { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt index 4db108fa4f7..6804ca893a9 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt @@ -162,6 +162,7 @@ fun RegisterScreen( }, bottomBar = { SyncBottomBar( + totalSyncCount = appDrawerUIState.totalSyncCount, isFirstTimeSync = registerUiState.isFirstTimeSync, appDrawerUIState = appDrawerUIState, onAppMainEvent = onAppMainEvent, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt index cb371dfb207..5da0b12d63a 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt @@ -72,6 +72,7 @@ import org.smartregister.fhircore.engine.ui.theme.SubtitleTextColor import org.smartregister.fhircore.engine.ui.theme.SuccessColor import org.smartregister.fhircore.engine.ui.theme.SyncBarBackgroundColor import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated +import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.ui.main.AppMainEvent import org.smartregister.fhircore.quest.ui.shared.models.AppDrawerUIState import org.smartregister.fhircore.quest.util.extensions.conditional @@ -81,6 +82,7 @@ const val SYNC_PROGRESS_INDICATOR_TEST_TAG = "syncProgressIndicatorTestTag" @Composable fun SyncBottomBar( + totalSyncCount: Int, isFirstTimeSync: Boolean, appDrawerUIState: AppDrawerUIState, onAppMainEvent: (AppMainEvent) -> Unit, @@ -174,6 +176,8 @@ fun SyncBottomBar( is CurrentSyncJobStatus.Running -> { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, + syncCounter = appDrawerUIState.syncCounter, + totalSyncCount = totalSyncCount, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, progressPercentage = appDrawerUIState.percentageProgress, @@ -184,6 +188,8 @@ fun SyncBottomBar( is CurrentSyncJobStatus.Failed -> { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, + syncCounter = appDrawerUIState.syncCounter, + totalSyncCount = totalSyncCount, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, onRetry = { @@ -196,6 +202,8 @@ fun SyncBottomBar( if (!hideSyncCompleteStatus.value) { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, + syncCounter = appDrawerUIState.syncCounter, + totalSyncCount = totalSyncCount, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, ) @@ -213,6 +221,8 @@ fun SyncBottomBar( @Composable fun SyncStatusView( isSyncUpload: Boolean?, + syncCounter: Int?, + totalSyncCount: Int, currentSyncJobStatus: CurrentSyncJobStatus?, progressPercentage: Int? = null, minimized: Boolean = false, @@ -262,6 +272,8 @@ fun SyncStatusView( } else { stringResource(org.smartregister.fhircore.engine.R.string.sync_error) }, + syncCounter = syncCounter, + totalSyncCount = totalSyncCount, minimized = minimized, startPadding = if (minimized) 0 else 16, ) @@ -286,6 +298,8 @@ fun SyncStatusView( }, progressPercentage ?: 0, ), + syncCounter = syncCounter, + totalSyncCount = totalSyncCount, minimized = false, color = Color.White, startPadding = 0, @@ -356,17 +370,29 @@ fun SyncStatusView( @Composable private fun SyncStatusTitle( text: String, + syncCounter: Int?, + totalSyncCount: Int, color: Color = Color.Unspecified, minimized: Boolean, startPadding: Int, ) { - Text( - text = text, - modifier = Modifier.padding(start = startPadding.dp), - fontWeight = FontWeight.SemiBold, - fontSize = if (minimized) 14.sp else 16.sp, - color = color, - ) + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Text( + text = text, + modifier = Modifier.padding(start = startPadding.dp), + fontWeight = FontWeight.SemiBold, + fontSize = if (minimized) 14.sp else 16.sp, + color = color, + ) + Text( + textAlign = TextAlign.Right, + text = stringResource(R.string.sync_counter, syncCounter ?: 1, totalSyncCount), + modifier = Modifier.padding(horizontal = 16.dp), + fontSize = 12.4.sp, + fontWeight = FontWeight.Bold, + color = color, + ) + } } @Composable @@ -376,6 +402,8 @@ fun SyncStatusSucceededPreview() { Column(modifier = Modifier.background(SuccessColor.copy(alpha = TRANSPARENCY))) { SyncStatusView( isSyncUpload = false, + syncCounter = 1, + totalSyncCount = 2, currentSyncJobStatus = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), ) } @@ -389,6 +417,8 @@ fun SyncStatusFailedPreview() { Column(modifier = Modifier.background(DangerColor.copy(alpha = TRANSPARENCY))) { SyncStatusView( isSyncUpload = false, + syncCounter = 1, + totalSyncCount = 2, currentSyncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()), ) } @@ -402,6 +432,8 @@ fun SyncStatusInProgressUploadPreview() { Column(modifier = Modifier.background(SyncBarBackgroundColor)) { SyncStatusView( isSyncUpload = true, + syncCounter = 1, + totalSyncCount = 2, currentSyncJobStatus = CurrentSyncJobStatus.Running( inProgressSyncJob = @@ -423,6 +455,8 @@ fun SyncStatusInProgressDownloadPreview() { Column(modifier = Modifier.background(SyncBarBackgroundColor)) { SyncStatusView( isSyncUpload = false, + syncCounter = 1, + totalSyncCount = 2, currentSyncJobStatus = CurrentSyncJobStatus.Running( inProgressSyncJob = @@ -444,6 +478,8 @@ fun SyncStatusSucceededMinimizedPreview() { Column(modifier = Modifier.background(SuccessColor.copy(alpha = TRANSPARENCY))) { SyncStatusView( isSyncUpload = false, + syncCounter = 1, + totalSyncCount = 2, currentSyncJobStatus = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), minimized = true, ) @@ -458,6 +494,8 @@ fun SyncStatusFailedMinimizedPreview() { Column(modifier = Modifier.background(DangerColor.copy(alpha = TRANSPARENCY))) { SyncStatusView( isSyncUpload = false, + syncCounter = 1, + totalSyncCount = 2, currentSyncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()), minimized = true, ) @@ -472,6 +510,8 @@ fun SyncStatusRunningMinimizedPreview() { Column(modifier = Modifier.background(SyncBarBackgroundColor)) { SyncStatusView( isSyncUpload = false, + syncCounter = 1, + totalSyncCount = 2, currentSyncJobStatus = CurrentSyncJobStatus.Running( inProgressSyncJob = diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/AppDrawerUIState.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/AppDrawerUIState.kt index b998337ffd7..9c942d73334 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/AppDrawerUIState.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/models/AppDrawerUIState.kt @@ -20,6 +20,8 @@ import com.google.android.fhir.sync.CurrentSyncJobStatus data class AppDrawerUIState( val isSyncUpload: Boolean? = false, + val syncCounter: Int? = null, + val totalSyncCount: Int = 1, val currentSyncJobStatus: CurrentSyncJobStatus? = null, val percentageProgress: Int? = 0, ) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/usersetting/UserSettingFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/usersetting/UserSettingFragment.kt index ccfdeddee68..7949a26277c 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/usersetting/UserSettingFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/usersetting/UserSettingFragment.kt @@ -42,6 +42,7 @@ import org.smartregister.fhircore.engine.BuildConfig import org.smartregister.fhircore.engine.configuration.app.SettingsOptions import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager +import org.smartregister.fhircore.engine.sync.SyncState import org.smartregister.fhircore.engine.ui.theme.AppTheme import org.smartregister.fhircore.quest.ui.main.AppMainViewModel import org.smartregister.fhircore.quest.ui.shared.components.SnackBarMessage @@ -131,7 +132,8 @@ class UserSettingFragment : Fragment(), OnSyncListener { syncListenerManager.registerSyncListener(this, lifecycle) } - override fun onSync(syncJobStatus: CurrentSyncJobStatus) { + override fun onSync(syncState: SyncState) { + val syncJobStatus = syncState.currentSyncJobStatus if (syncJobStatus is CurrentSyncJobStatus.Running) { if (syncJobStatus.inProgressSyncJob is SyncJobStatus.InProgress) { val inProgressSyncJob = syncJobStatus.inProgressSyncJob as SyncJobStatus.InProgress @@ -139,12 +141,16 @@ class UserSettingFragment : Fragment(), OnSyncListener { val progressPercentage = appMainViewModel.calculatePercentageProgress(inProgressSyncJob) appMainViewModel.updateAppDrawerUIState( isSyncUpload = isSyncUpload, + syncCounter = syncState.counter, currentSyncJobStatus = syncJobStatus, percentageProgress = progressPercentage, ) } } else { - appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) + appMainViewModel.updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) } } diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml index 4e1f1588a9c..fc6471c0784 100644 --- a/android/quest/src/main/res/values/strings.xml +++ b/android/quest/src/main/res/values/strings.xml @@ -144,4 +144,5 @@ Error rendering profile Are you sure you want to submit? You are about to submit + Sync %1$d of %2$d From 0502bc4157abd3a3d97aad4fbc81b31c14388a36 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Fri, 31 Jan 2025 15:01:00 +0300 Subject: [PATCH 13/16] Fix failing tests. Signed-off-by: Lentumunai-Mark --- .../quest/ui/main/AppMainActivityTest.kt | 25 ++++++++++++++----- .../quest/ui/main/AppMainViewModelTest.kt | 3 ++- .../quest/ui/register/RegisterFragmentTest.kt | 23 +++++++++++------ 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt index 98f71bba260..d5f9893655c 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt @@ -46,6 +46,7 @@ import org.junit.Test import org.robolectric.Robolectric import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig +import org.smartregister.fhircore.engine.sync.SyncState import org.smartregister.fhircore.engine.task.FhirCarePlanGenerator import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.quest.app.fakes.Faker @@ -94,7 +95,11 @@ class AppMainActivityTest : ActivityRobolectricTest() { val initialSyncTime = viewModel.appMainUiState.value.lastSyncTime appMainActivity.onSync( - CurrentSyncJobStatus.Running(SyncJobStatus.InProgress(SyncOperation.DOWNLOAD)), + SyncState( + currentSyncJobStatus = + CurrentSyncJobStatus.Running(SyncJobStatus.InProgress(SyncOperation.DOWNLOAD)), + counter = 1, + ), ) // Timestamp will only updated for Finished. @@ -110,7 +115,7 @@ class AppMainActivityTest : ActivityRobolectricTest() { ) val initialTimestamp = viewModel.appMainUiState.value.lastSyncTime val syncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()) - appMainActivity.onSync(syncJobStatus) + appMainActivity.onSync(SyncState(currentSyncJobStatus = syncJobStatus, counter = 1)) // Timestamp not update if status is Failed. Initial timestamp remains the same Assert.assertEquals(initialTimestamp, viewModel.appMainUiState.value.lastSyncTime) @@ -119,7 +124,13 @@ class AppMainActivityTest : ActivityRobolectricTest() { @Test fun testOnSyncWithSyncStateFailedWhenTimestampIsNotNull() { val viewModel = appMainActivity.appMainViewModel - appMainActivity.onSync(CurrentSyncJobStatus.Failed(OffsetDateTime.now())) + appMainActivity.onSync( + syncState = + SyncState( + counter = 1, + currentSyncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()), + ), + ) Assert.assertNotNull(viewModel.appMainUiState.value.lastSyncTime) } @@ -127,11 +138,13 @@ class AppMainActivityTest : ActivityRobolectricTest() { fun testOnSyncWithSyncStateSucceeded() { // Arrange val viewModel = appMainActivity.appMainViewModel - val stateSucceded = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()) - appMainActivity.onSync(stateSucceded) + val stateSucceeded = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()) + appMainActivity.onSync( + syncState = SyncState(counter = 1, currentSyncJobStatus = stateSucceeded), + ) Assert.assertEquals( - viewModel.formatLastSyncTimestamp(timestamp = stateSucceded.timestamp) + " (0s)", + viewModel.formatLastSyncTimestamp(timestamp = stateSucceeded.timestamp) + " (0s)", viewModel.getSyncTime(), ) } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModelTest.kt index 22d2fc239cc..187cf9754c5 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModelTest.kt @@ -182,7 +182,8 @@ class AppMainViewModelTest : RobolectricTest() { appMainViewModel.onEvent( AppMainEvent.UpdateSyncState( - syncFinishedSyncJobStatus, + syncCounter = 1, + currentSyncJobStatus = syncFinishedSyncJobStatus, appMainViewModel.formatLastSyncTimestamp(syncFinishedTimestamp), ), ) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterFragmentTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterFragmentTest.kt index cdc1fa1d920..5a4c7b22b25 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterFragmentTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterFragmentTest.kt @@ -51,6 +51,7 @@ import org.smartregister.fhircore.engine.domain.model.ActionConfig import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation +import org.smartregister.fhircore.engine.sync.SyncState import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.quest.app.fakes.Faker import org.smartregister.fhircore.quest.event.EventBus @@ -130,9 +131,13 @@ class RegisterFragmentTest : RobolectricTest() { @Test fun testOnSyncState() { val syncJobStatus = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()) - coEvery { registerFragmentMock.onSync(syncJobStatus) } just runs - registerFragmentMock.onSync(syncJobStatus = syncJobStatus) - verify { registerFragmentMock.onSync(syncJobStatus) } + coEvery { + registerFragmentMock.onSync(SyncState(currentSyncJobStatus = syncJobStatus, counter = 1)) + } just runs + registerFragmentMock.onSync(SyncState(currentSyncJobStatus = syncJobStatus, counter = 1)) + verify { + registerFragmentMock.onSync(SyncState(currentSyncJobStatus = syncJobStatus, counter = 1)) + } } @Test @@ -195,8 +200,10 @@ class RegisterFragmentTest : RobolectricTest() { fun testOnSyncWithFailedJobStatusNonAuthErrorRendersSyncFailedMessage() { val syncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()) val registerFragmentSpy = spyk(registerFragment) - registerFragmentSpy.onSync(syncJobStatus = syncJobStatus) - verify { registerFragmentSpy.onSync(syncJobStatus) } + registerFragmentSpy.onSync(SyncState(currentSyncJobStatus = syncJobStatus, counter = 1)) + verify { + registerFragmentSpy.onSync(SyncState(currentSyncJobStatus = syncJobStatus, counter = 1)) + } verify { registerFragmentSpy.getString( org.smartregister.fhircore.engine.R.string.sync_completed_with_errors, @@ -209,8 +216,10 @@ class RegisterFragmentTest : RobolectricTest() { val syncJobStatus: CurrentSyncJobStatus.Failed = mockk() val registerFragmentSpy = spyk(registerFragment) - registerFragmentSpy.onSync(syncJobStatus = syncJobStatus) - verify { registerFragmentSpy.onSync(syncJobStatus) } + registerFragmentSpy.onSync(SyncState(currentSyncJobStatus = syncJobStatus, counter = 1)) + verify { + registerFragmentSpy.onSync(SyncState(currentSyncJobStatus = syncJobStatus, counter = 1)) + } verify { registerFragmentSpy.getString( org.smartregister.fhircore.engine.R.string.sync_completed_with_errors, From 21f3e2bcfc59d73f0710ca87e8284f62589070be Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Fri, 31 Jan 2025 15:37:38 +0300 Subject: [PATCH 14/16] Fix failing tests on engine module. Signed-off-by: Lentumunai-Mark --- .../org/smartregister/fhircore/engine/sync/AppSyncWorkerTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/sync/AppSyncWorkerTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/sync/AppSyncWorkerTest.kt index 288f0407fe6..d7df85122f0 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/sync/AppSyncWorkerTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/sync/AppSyncWorkerTest.kt @@ -43,6 +43,7 @@ class AppSyncWorkerTest : RobolectricTest() { val fhirEngine = mockk() val taskExecutor = mockk() val timeContext = mockk() + val customResourceSyncService = mockk() val configService = mockk() every { taskExecutor.serialTaskExecutor } returns mockk() @@ -58,6 +59,7 @@ class AppSyncWorkerTest : RobolectricTest() { fhirEngine, timeContext, configService, + customResourceSyncService, ) appSyncWorker.getDownloadWorkManager() From e1574c305b9bdecf6b12c2c9ee9d4cb0862d78f8 Mon Sep 17 00:00:00 2001 From: Elly Kitoto Date: Fri, 31 Jan 2025 20:59:54 +0300 Subject: [PATCH 15/16] Fix sync count on apps with no custom resources Signed-off-by: Elly Kitoto --- .../configuration/ConfigurationRegistry.kt | 17 ++++++++++++++++ .../fhircore/engine/sync/SyncBroadcaster.kt | 4 ++-- .../fhircore/engine/sync/SyncState.kt | 1 - .../quest/ui/main/AppMainViewModel.kt | 20 +------------------ 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index 763b063964c..c1a5924a901 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -851,6 +851,23 @@ constructor( return Pair(customResourceSearchParams, fhirResourceSearchParams) } + /** + * This function returns either '1' or '2' depending on whether there are custom resources (not + * included in ResourceType enum) in the sync configuration. The custom resources are configured + * in the sync configuration JSON file as valid FHIR SearchParameter of type 'special'. If there + * are custom resources to be synced with the data, the application will first download the custom + * resources then the rest of the app data. + */ + fun retrieveTotalSyncCount(): Int { + val totalSyncCount = sharedPreferencesHelper.read(SharedPreferenceKey.TOTAL_SYNC_COUNT.name, "") + return if (totalSyncCount.isNullOrBlank()) { + retrieveResourceConfiguration(ConfigType.Sync) + .parameter + .map { it.resource as SearchParameter } + .count { it.hasType() && it.type == Enumerations.SearchParamType.SPECIAL } + } else totalSyncCount.toInt() + } + companion object { const val BASE_CONFIG_PATH = "configs/%s" const val COMPOSITION_CONFIG_PATH = "configs/%s/composition_config.json" diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt index c77668e42aa..bd645e70b96 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt @@ -119,7 +119,7 @@ constructor( } onSyncListener.onSync( SyncState( - counter = SYNC_COUNTER_2, + counter = configurationRegistry.retrieveTotalSyncCount(), currentSyncJobStatus = currentSyncJobStatus, ), ) @@ -137,7 +137,7 @@ constructor( syncListenerManager.onSyncListeners.forEach { onSyncListener -> onSyncListener.onSync( SyncState( - counter = SYNC_COUNTER_2, + counter = configurationRegistry.retrieveTotalSyncCount(), currentSyncJobStatus = currentSyncJobStatus, ), ) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncState.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncState.kt index 936c408ef0c..d54859dcd63 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncState.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncState.kt @@ -19,6 +19,5 @@ package org.smartregister.fhircore.engine.sync import com.google.android.fhir.sync.CurrentSyncJobStatus const val SYNC_COUNTER_1 = 1 -const val SYNC_COUNTER_2 = 2 data class SyncState(val counter: Int, val currentSyncJobStatus: CurrentSyncJobStatus) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt index bf34cb84d52..04629e73f70 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt @@ -45,7 +45,6 @@ import kotlin.time.Duration import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.hl7.fhir.r4.model.Parameters import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Task import org.smartregister.fhircore.engine.R @@ -408,7 +407,7 @@ constructor( AppDrawerUIState( isSyncUpload = isSyncUpload, syncCounter = syncCounter, - totalSyncCount = retrieveTotalSyncCount(), + totalSyncCount = configurationRegistry.retrieveTotalSyncCount(), currentSyncJobStatus = currentSyncJobStatus, percentageProgress = percentageProgress, ) @@ -487,23 +486,6 @@ constructor( } } - /** - * This function returns either '1' or '2' depending on whether there are custom resources (not - * included in ResourceType enum) in the sync configuration. The custom resources are configured - * in the sync configuration JSON file as valid FHIR SearchParameter of type 'special'. If there - * are custom resources to be synced with the data, the application will first download the custom - * resources then the rest of the app data. - */ - private fun retrieveTotalSyncCount(): Int { - val totalSyncCount = sharedPreferencesHelper.read(SharedPreferenceKey.TOTAL_SYNC_COUNT.name, "") - return if (totalSyncCount.isNullOrBlank()) { - configurationRegistry - .retrieveResourceConfiguration(ConfigType.Sync) - .parameter - .count { it.hasType() } - } else totalSyncCount.toInt() - } - companion object { private const val INITIAL_DELAY = 15L const val SYNC_TIMESTAMP_INPUT_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" From de5b5e6a2014016bcbf7e0aeefb94c67ba3774ba Mon Sep 17 00:00:00 2001 From: Elly Kitoto Date: Mon, 3 Feb 2025 17:09:41 +0300 Subject: [PATCH 16/16] Fix failing tests Signed-off-by: Elly Kitoto --- .../java/org/smartregister/fhircore/quest/app/fakes/Faker.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/app/fakes/Faker.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/app/fakes/Faker.kt index 3e5865b8c58..23f719f66d9 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/app/fakes/Faker.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/app/fakes/Faker.kt @@ -90,7 +90,8 @@ object Faker { ConfigurationRegistry( fhirEngine = mockk(), fhirResourceDataSource = fhirResourceDataSource, - sharedPreferencesHelper = mockk(), + sharedPreferencesHelper = + spyk(SharedPreferencesHelper(ApplicationProvider.getApplicationContext(), Gson())), dispatcherProvider = testDispatcherProvider, configService = configService, json = json,