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 c50c839ce95..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 @@ -25,6 +25,7 @@ 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.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 @@ -55,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 @@ -565,7 +565,7 @@ constructor( return resultBundle } - suspend fun fetchResources( + private suspend fun fetchResources( gatewayModeHeaderValue: String? = null, url: String, ) { @@ -581,11 +581,8 @@ constructor( 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, @@ -594,7 +591,7 @@ constructor( } } - private suspend fun processResultBundleEntries( + suspend fun processResultBundleEntries( resultBundleEntries: List, ) { resultBundleEntries.forEach { bundleEntryComponent -> @@ -784,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 = @@ -847,9 +842,32 @@ 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) } + /** + * 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/AppSyncWorker.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt index 80d79883204..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 @@ -57,6 +60,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 @@ -74,11 +78,39 @@ constructor( ) override suspend fun doWork(): Result { - saveSyncStartTimestamp() - setForeground(getForegroundInfo()) - 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, @@ -131,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 @@ -175,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 new file mode 100644 index 00000000000..a75efd16f14 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomResourceSyncService.kt @@ -0,0 +1,151 @@ +/* + * 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.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 timber.log.Timber + +@Singleton +class CustomResourceSyncService +@Inject +constructor( + val configurationRegistry: ConfigurationRegistry, + val dispatcherProvider: DispatcherProvider, + val fhirResourceDataSource: FhirResourceDataSource, + val syncListenerManager: SyncListenerManager, +) { + suspend fun runCustomResourceSync() { + val (resourceSearchParams, _) = configurationRegistry.loadResourceSearchParams() + if (resourceSearchParams.isEmpty()) return + + val resourceUrls = + resourceSearchParams + .asIterable() + .filter { it.value.isNotEmpty() } + .map { "${it.key}?${it.value.concatParams()}" } + + val summaryCount = fetchSummaryCount(resourceUrls).values.sumOf { it ?: 0 } + + resourceUrls.forEach { url -> + fetchCustomResources( + gatewayModeHeaderValue = ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE, + url = url, + totalCounts = summaryCount, + ) + } + } + + private suspend fun fetchCustomResources( + gatewayModeHeaderValue: String? = null, + url: String, + totalCounts: Int = 0, + completedRecords: Int = 0, + ) { + runCatching { + Timber.d("Setting state: Running") + syncListenerManager.emitSyncStatus( + SyncState( + counter = SYNC_COUNTER_1, + currentSyncJobStatus = + 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( + 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( + SyncState( + counter = SYNC_COUNTER_1, + currentSyncJobStatus = + 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( + SyncState( + counter = SYNC_COUNTER_1, + currentSyncJobStatus = 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") } +} 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 d084c33d4d5..00000000000 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt +++ /dev/null @@ -1,83 +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.concatParams -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import java.net.UnknownHostException -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") - resourceSearchParams - .asIterable() - .filter { it.value.isNotEmpty() } - .map { "${it.key}?${it.value.concatParams()}" } - .forEach { url -> - fetchResources( - gatewayModeHeaderValue = ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE, - url = url, - ) - } - } - 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) - Result.failure() - } - } - } - - 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 59c9413d934..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 @@ -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(), - ) } /** @@ -121,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 = configurationRegistry.retrieveTotalSyncCount(), + currentSyncJobStatus = currentSyncJobStatus, + ), ) } } @@ -140,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 = configurationRegistry.retrieveTotalSyncCount(), + 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 9015ef7a14e..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 @@ -105,4 +105,8 @@ constructor( Timber.i("FHIR resource sync parameters $resourceSearchParams") return resourceSearchParams } + + 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..d54859dcd63 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncState.kt @@ -0,0 +1,23 @@ +/* + * 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 + +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/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/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/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() 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/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 9e389354975..21d47c6ac23 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 e03fae77779..db726bdf9e2 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 @@ -210,8 +211,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 @@ -219,17 +220,23 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { val progressPercentage = appMainViewModel.calculatePercentageProgress(inProgressSyncJob) appMainViewModel.updateAppDrawerUIState( isSyncUpload = isSyncUpload, + syncCounter = syncState.counter, currentSyncJobStatus = syncJobStatus, percentageProgress = progressPercentage, ) } } - is CurrentSyncJobStatus.Succeeded, - is CurrentSyncJobStatus.Failed, -> { - appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) - if (syncJobStatus is CurrentSyncJobStatus.Succeeded) { - geoWidgetLauncherViewModel.onEvent(GeoWidgetEvent.ClearMap, context = requireContext()) - } + is CurrentSyncJobStatus.Succeeded -> { + appMainViewModel.updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) + } + is CurrentSyncJobStatus.Failed -> { + appMainViewModel.updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) geoWidgetLauncherViewModel.onEvent( GeoWidgetEvent.RetrieveFeatures( geoWidgetConfig = geoWidgetConfiguration, @@ -238,7 +245,11 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { context = requireContext(), ) } - else -> appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) + else -> + appMainViewModel.updateAppDrawerUIState( + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + ) } } @@ -266,7 +277,7 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { } } } - .launchIn(lifecycleScope) + .launchIn(this) } } geoWidgetLauncherViewModel.noLocationFoundDialog.observe(viewLifecycleOwner) { show -> @@ -305,11 +316,21 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { override fun onPause() { super.onPause() - appMainViewModel.updateAppDrawerUIState(false, null, 0) + appMainViewModel.updateAppDrawerUIState( + isSyncUpload = false, + syncCounter = null, + currentSyncJobStatus = null, + percentageProgress = 0, + ) } override fun onDestroy() { super.onDestroy() - appMainViewModel.updateAppDrawerUIState(false, null, 0) + appMainViewModel.updateAppDrawerUIState( + isSyncUpload = false, + syncCounter = null, + currentSyncJobStatus = null, + percentageProgress = 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 1ea75e601f1..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 @@ -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 @@ -113,13 +112,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 { @@ -134,6 +128,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 = @@ -237,19 +234,20 @@ 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( + 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() } @@ -319,7 +317,7 @@ constructor( fun retrieveLastSyncTimestamp(): String? = sharedPreferencesHelper.read(SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, null) - fun schedulePeriodicSync() { + private fun schedulePeriodicSync() { viewModelScope.launch { syncBroadcaster.schedulePeriodicSync(applicationConfiguration.syncInterval) } @@ -401,12 +399,15 @@ constructor( fun updateAppDrawerUIState( isSyncUpload: Boolean? = null, + syncCounter: Int?, currentSyncJobStatus: CurrentSyncJobStatus?, percentageProgress: Int? = null, ) { appDrawerUiState.value = AppDrawerUIState( isSyncUpload = isSyncUpload, + syncCounter = syncCounter, + totalSyncCount = configurationRegistry.retrieveTotalSyncCount(), currentSyncJobStatus = currentSyncJobStatus, percentageProgress = percentageProgress, ) @@ -468,12 +469,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/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 9e6aff8fb8d..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 @@ -52,8 +52,10 @@ 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.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 @@ -76,6 +78,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() @@ -192,31 +196,40 @@ 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 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, + syncCounter = syncState.counter, + currentSyncJobStatus = syncJobStatus, + percentageProgress = progressPercentage, + ) } } 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, + ) } } @@ -243,19 +256,21 @@ class RegisterFragment : Fragment(), OnSyncListener { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { // 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) + } } } @@ -270,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 4f737db8ee8..1ed1f33f747 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, @@ -175,6 +177,8 @@ fun SyncBottomBar( val maxPercentage = appDrawerUIState.percentageProgress?.coerceAtMost(100) ?: 0 SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, + syncCounter = appDrawerUIState.syncCounter, + totalSyncCount = totalSyncCount, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, progressPercentage = maxPercentage, @@ -185,6 +189,8 @@ fun SyncBottomBar( is CurrentSyncJobStatus.Failed -> { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, + syncCounter = appDrawerUIState.syncCounter, + totalSyncCount = totalSyncCount, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, onRetry = { @@ -197,6 +203,8 @@ fun SyncBottomBar( if (!hideSyncCompleteStatus.value) { SyncStatusView( isSyncUpload = appDrawerUIState.isSyncUpload, + syncCounter = appDrawerUIState.syncCounter, + totalSyncCount = totalSyncCount, currentSyncJobStatus = currentSyncJobStatus, minimized = !syncNotificationBarExpanded, ) @@ -214,6 +222,8 @@ fun SyncBottomBar( @Composable fun SyncStatusView( isSyncUpload: Boolean?, + syncCounter: Int?, + totalSyncCount: Int, currentSyncJobStatus: CurrentSyncJobStatus?, progressPercentage: Int? = null, minimized: Boolean = false, @@ -263,6 +273,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, ) @@ -287,6 +299,8 @@ fun SyncStatusView( }, progressPercentage ?: 0, ), + syncCounter = syncCounter, + totalSyncCount = totalSyncCount, minimized = false, color = Color.White, startPadding = 0, @@ -357,17 +371,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 @@ -377,6 +403,8 @@ fun SyncStatusSucceededPreview() { Column(modifier = Modifier.background(SuccessColor.copy(alpha = TRANSPARENCY))) { SyncStatusView( isSyncUpload = false, + syncCounter = 1, + totalSyncCount = 2, currentSyncJobStatus = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now()), ) } @@ -390,6 +418,8 @@ fun SyncStatusFailedPreview() { Column(modifier = Modifier.background(DangerColor.copy(alpha = TRANSPARENCY))) { SyncStatusView( isSyncUpload = false, + syncCounter = 1, + totalSyncCount = 2, currentSyncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()), ) } @@ -403,6 +433,8 @@ fun SyncStatusInProgressUploadPreview() { Column(modifier = Modifier.background(SyncBarBackgroundColor)) { SyncStatusView( isSyncUpload = true, + syncCounter = 1, + totalSyncCount = 2, currentSyncJobStatus = CurrentSyncJobStatus.Running( inProgressSyncJob = @@ -424,6 +456,8 @@ fun SyncStatusInProgressDownloadPreview() { Column(modifier = Modifier.background(SyncBarBackgroundColor)) { SyncStatusView( isSyncUpload = false, + syncCounter = 1, + totalSyncCount = 2, currentSyncJobStatus = CurrentSyncJobStatus.Running( inProgressSyncJob = @@ -445,6 +479,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, ) @@ -459,6 +495,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, ) @@ -473,6 +511,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 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, 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,