diff --git a/android/engine/src/main/AndroidManifest.xml b/android/engine/src/main/AndroidManifest.xml
index e101670a7d..cb6d1b88c0 100644
--- a/android/engine/src/main/AndroidManifest.xml
+++ b/android/engine/src/main/AndroidManifest.xml
@@ -49,5 +49,7 @@
tools:node="remove" />
+
+
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/Configuration.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/Configuration.kt
index 411559b9a4..9ea51609ed 100644
--- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/Configuration.kt
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/Configuration.kt
@@ -43,6 +43,7 @@ data class AppConfiguration(
val appFeatures: AppFeatureConfig,
val syncConfig: SyncConfig,
val formConfigs: List = listOf(),
+ val organizationSyncConfig: OrganizationSyncConfig,
)
@Serializable
@@ -66,3 +67,12 @@ data class Resource(
data class Parameter(
@SerializedName("resource") var resource: Resource,
)
+
+@Serializable data class OrganizationSyncConfig(@SerializedName("items") val items: List- )
+
+@Serializable
+data class Item(
+ @SerializedName("id") val id: String,
+ @SerializedName("name") val name: String,
+ @SerializedName("offline_first") val offlineFirst: Boolean,
+)
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 0244e166a0..095d9fb832 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
@@ -77,6 +77,9 @@ constructor(
fun getFormConfigs(): List? = applicationConfiguration.value?.formConfigs
+ fun getPerOrgSyncConfigs(): OrganizationSyncConfig? =
+ applicationConfiguration.value?.organizationSyncConfig
+
private suspend fun getBinary(id: String): Binary = fhirEngine.get(id)
companion object {
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/TingatheDatabase.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/TingatheDatabase.kt
index c6f355033d..5704c7c139 100644
--- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/TingatheDatabase.kt
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/TingatheDatabase.kt
@@ -24,19 +24,23 @@ import org.smartregister.fhircore.engine.data.local.localChange.LocalChangeDao
import org.smartregister.fhircore.engine.data.local.localChange.LocalChangeEntity
import org.smartregister.fhircore.engine.data.local.syncAttempt.SyncAttemptTrackerDao
import org.smartregister.fhircore.engine.data.local.syncAttempt.SyncAttemptTrackerEntity
+import org.smartregister.fhircore.engine.data.local.syncStrategy.SyncStrategyCacheDao
+import org.smartregister.fhircore.engine.data.local.syncStrategy.SyncStrategyCacheEntity
@Database(
- version = 2,
+ version = 3,
entities =
[
LocalChangeEntity::class,
SyncAttemptTrackerEntity::class,
+ SyncStrategyCacheEntity::class,
],
)
abstract class TingatheDatabase : RoomDatabase() {
abstract val localChangeDao: LocalChangeDao
abstract val syncAttemptTrackerDao: SyncAttemptTrackerDao
+ abstract val syncStrategyCacheDao: SyncStrategyCacheDao
companion object {
fun databaseBuilder(context: Context): Builder {
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/syncStrategy/SyncStrategyCacheDao.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/syncStrategy/SyncStrategyCacheDao.kt
new file mode 100644
index 0000000000..e0f25ba0fc
--- /dev/null
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/syncStrategy/SyncStrategyCacheDao.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 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.data.local.syncStrategy
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+
+@Dao
+abstract class SyncStrategyCacheDao {
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ abstract suspend fun upsert(syncStrategyCacheEntity: List)
+
+ @Query("DELETE FROM syncstrategycacheentity") abstract suspend fun deleteAll()
+
+ @Query("SELECT * FROM syncstrategycacheentity")
+ abstract suspend fun query(): List
+}
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/syncStrategy/SyncStrategyCacheEntity.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/syncStrategy/SyncStrategyCacheEntity.kt
new file mode 100644
index 0000000000..a2ebdb8592
--- /dev/null
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/syncStrategy/SyncStrategyCacheEntity.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 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.data.local.syncStrategy
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class SyncStrategyCacheEntity(
+ @PrimaryKey val logicalId: String,
+ val shouldSync: Boolean = false,
+ val timestamp: Long = System.currentTimeMillis(),
+)
+
+fun List.toEntity() = map { SyncStrategyCacheEntity(logicalId = it) }
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/ApiRepositoryImpl.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/ApiRepositoryImpl.kt
index 775ed662b1..f12b0928e2 100644
--- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/ApiRepositoryImpl.kt
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/ApiRepositoryImpl.kt
@@ -36,6 +36,8 @@ import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.R
import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirApiService
import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceService
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.ApiRepositoryImpl.Companion.SYNC_TIMESTAMP_INPUT_FORMAT
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.ApiRepositoryImpl.OfType
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.repository.ApiRepository
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.Progress
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SearchBy
@@ -160,7 +162,7 @@ class ApiRepositoryImpl(
.onFailure { Timber.e(it) }
progressStatus(Progress(index, patientSize, logicalId))
}
- saveLastUpdatedTimestamp()
+ saveLastUpdatedTimestamp(appDataStore)
onCompleteListener(runSync, patientSize)
}
@@ -170,7 +172,7 @@ class ApiRepositoryImpl(
.map { it.resource }
.lastOrNull()
- private enum class OfType {
+ enum class OfType {
Patient,
Encounter,
Observation,
@@ -183,44 +185,44 @@ class ApiRepositoryImpl(
Appointment,
}
- private suspend fun saveLastUpdatedTimestamp() {
- OfType.entries
- .map {
- when (it) {
- OfType.Patient -> ResourceType.Patient
- OfType.Observation -> ResourceType.Observation
- OfType.CarePlan -> ResourceType.CarePlan
- OfType.Task -> ResourceType.Task
- OfType.Condition -> ResourceType.Condition
- OfType.Appointment -> ResourceType.Appointment
- OfType.Encounter -> ResourceType.Encounter
- OfType.List -> ResourceType.List
- OfType.Practitioner -> ResourceType.Practitioner
- OfType.RelatedPerson -> ResourceType.RelatedPerson
- }
- }
- .onEach { resourceType ->
- val lastSyncTimestamp = Date().toOffsetDateTime().formatLastSyncTimestamp()
- appDataStore.saveLastUpdatedTimestamp(resourceType, lastSyncTimestamp)
- }
+ companion object {
+ const val SYNC_TIMESTAMP_INPUT_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
}
+}
- private fun Date.toOffsetDateTime(): OffsetDateTime {
- return OffsetDateTime.ofInstant(toInstant(), ZoneId.systemDefault())
- }
+fun simpleDateFormat() = SimpleDateFormat(SYNC_TIMESTAMP_INPUT_FORMAT, Locale.getDefault())
- private fun OffsetDateTime.formatLastSyncTimestamp(): String {
- val syncTimestampFormatter =
- SimpleDateFormat(SYNC_TIMESTAMP_INPUT_FORMAT, Locale.getDefault()).apply {
- timeZone = TimeZone.getDefault()
- }
- val parse: Date? = syncTimestampFormatter.parse(toString())
- return if (parse == null) "" else simpleDateFormat.format(parse)
- }
+fun Date.toOffsetDateTime(): OffsetDateTime {
+ return OffsetDateTime.ofInstant(toInstant(), ZoneId.systemDefault())
+}
- companion object {
- const val SYNC_TIMESTAMP_INPUT_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
- }
+private fun OffsetDateTime.formatLastSyncTimestamp(): String {
+ val syncTimestampFormatter =
+ SimpleDateFormat(SYNC_TIMESTAMP_INPUT_FORMAT, Locale.getDefault()).apply {
+ timeZone = TimeZone.getDefault()
+ }
+ val parse: Date? = syncTimestampFormatter.parse(toString())
+ return if (parse == null) "" else simpleDateFormat().format(parse)
+}
- private val simpleDateFormat = SimpleDateFormat(SYNC_TIMESTAMP_INPUT_FORMAT, Locale.getDefault())
+suspend fun saveLastUpdatedTimestamp(appDataStore: AppDataStore) {
+ OfType.entries
+ .map {
+ when (it) {
+ OfType.Patient -> ResourceType.Patient
+ OfType.Observation -> ResourceType.Observation
+ OfType.CarePlan -> ResourceType.CarePlan
+ OfType.Task -> ResourceType.Task
+ OfType.Condition -> ResourceType.Condition
+ OfType.Appointment -> ResourceType.Appointment
+ OfType.Encounter -> ResourceType.Encounter
+ OfType.List -> ResourceType.List
+ OfType.Practitioner -> ResourceType.Practitioner
+ OfType.RelatedPerson -> ResourceType.RelatedPerson
+ }
+ }
+ .onEach { resourceType ->
+ val lastSyncTimestamp = Date().toOffsetDateTime().formatLastSyncTimestamp()
+ appDataStore.saveLastUpdatedTimestamp(resourceType, lastSyncTimestamp)
+ }
}
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/ListResourceExt.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/ListResourceExt.kt
index 04c25d5a94..34aff38c9b 100644
--- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/ListResourceExt.kt
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/ListResourceExt.kt
@@ -17,75 +17,29 @@
package org.smartregister.fhircore.engine.data.remote.resource.syncStrategy
import com.google.android.fhir.FhirEngine
-import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.search.Search
import org.hl7.fhir.r4.model.ListResource
import org.hl7.fhir.r4.model.ResourceType
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.Progress
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SearchBy
-import org.smartregister.fhircore.engine.util.SharedPreferenceKey.PATIENT_IDENTIFIER_LIST_TIMESTAMP
-import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
-import timber.log.Timber
-class ListResourceExt(
- private val fhirEngine: FhirEngine,
- private val sharedPreferences: SharedPreferencesHelper,
- private val apiRepository: ApiRepositoryImpl,
-) {
-
- private var invokeCounter = 0
-
- suspend fun onListResourceExt(
- onProgress: (Progress) -> Unit,
- ): Boolean {
- var shouldRunSync = false
- var size = 0
- shouldRun()
- ?.filterNotNull()
- ?.also { size = it.size }
- ?.forEachIndexed { index, identifier ->
- apiRepository.search(identifier.toString(), SearchBy.IDENTIFIER).onEach { patient ->
- kotlin
- .runCatching { fhirEngine.get(patient.resourceType, patient.idPart) }
- .onFailure { throwable ->
- if (throwable is ResourceNotFoundException) {
- apiRepository.fetchAndSaveToDb(
- logicalId = patient.idPart,
- onCompleteListener = { shouldRunSync = it },
- )
- }
- }
- .onSuccess { Timber.e("Skipping -> ${it.resourceType} - ${it.idPart}") }
- }
- onProgress(Progress(index, size, identifier.toString()))
- }
-
- return shouldRunSync
- }
-
- private suspend fun get() =
- fhirEngine
- .search(
- Search(ResourceType.List).apply {
- filter(ListResource.TITLE, { value = "Patient Identifier List" })
- },
- )
- .map { it.resource }
- .firstOrNull()
-
- suspend fun shouldRun(): List? {
- invokeCounter += 1
- if (invokeCounter == 1) {
- val listResource: ListResource = get() ?: return null
- val preferenceKey = PATIENT_IDENTIFIER_LIST_TIMESTAMP.name
- val oldTimestamp = sharedPreferences.read(preferenceKey, 0L)
- val currentTimestamp = listResource.meta.lastUpdated.time
- if (oldTimestamp == 0L || oldTimestamp > currentTimestamp) {
- sharedPreferences.write(preferenceKey, currentTimestamp)
- return listResource.entry.map { entry -> entry.item.display.toIntOrNull() }.toList()
- }
- return null
- }
- return null
- }
+suspend fun getListResource(fhirEngine: FhirEngine) =
+ fhirEngine
+ .search(
+ Search(ResourceType.List).apply {
+ filter(ListResource.TITLE, { value = "Patient Identifier List" })
+ },
+ )
+ .map { it.resource }
+ .firstOrNull()
+
+suspend fun getIdentifiers(fhirEngine: FhirEngine): ListResourceItem? {
+ val listResource: ListResource = getListResource(fhirEngine) ?: return null
+ return ListResourceItem(
+ data = listResource.entry.mapNotNull { entry -> entry.item.display.toInt() }.toList(),
+ idPart = listResource.idPart,
+ )
}
+
+data class ListResourceItem(
+ val data: List,
+ val idPart: String,
+)
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/Utils.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/Utils.kt
new file mode 100644
index 0000000000..c2298ce9b7
--- /dev/null
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/Utils.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2021 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.data.remote.resource.syncStrategy
+
+import com.google.android.fhir.FhirEngine
+import com.google.android.fhir.search.search
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.runBlocking
+import org.hl7.fhir.r4.model.ListResource
+import org.hl7.fhir.r4.model.Patient
+import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
+import org.smartregister.fhircore.engine.configuration.Item
+import org.smartregister.fhircore.engine.data.local.syncStrategy.SyncStrategyCacheDao
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SyncState
+import org.smartregister.fhircore.engine.util.SharedPreferenceKey
+import org.smartregister.fhircore.engine.util.SharedPreferenceKey.SYNC_STATUS
+import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
+
+fun logicalIds(fhirEngine: FhirEngine) = runBlocking {
+ fhirEngine
+ .search { filter(Patient.ACTIVE, { value = of(true) }) }
+ .map { it.resource.idPart }
+}
+
+fun List.subListIds(syncStrategyCacheDao: SyncStrategyCacheDao): List {
+ return runBlocking {
+ val cachedIds = syncStrategyCacheDao.query().map { it.logicalId }
+ ((cachedIds union this@subListIds) - (cachedIds intersect this@subListIds.toSet())).toList()
+ }
+}
+
+fun hasCompletedInitialSync(sharedPreferencesHelper: SharedPreferencesHelper) =
+ getSyncState(sharedPreferencesHelper) < SyncState.CompletedInitialSync.value
+
+fun setSubSequentSync(sharedPreferencesHelper: SharedPreferencesHelper) =
+ sharedPreferencesHelper.write(SYNC_STATUS.name, SyncState.SubSequentSync.value)
+
+fun isSubSequentSync(sharedPreferencesHelper: SharedPreferencesHelper) =
+ getSyncState(sharedPreferencesHelper) == SyncState.SubSequentSync.value
+
+fun isRunSyncNow(sharedPreferencesHelper: SharedPreferencesHelper) =
+ getSyncState(sharedPreferencesHelper) == SyncState.RunSyncNow.value
+
+fun getSyncState(sharedPreferencesHelper: SharedPreferencesHelper) =
+ sharedPreferencesHelper.read(SYNC_STATUS.name, SyncState.InitialSync.value)
+
+fun Long.before1min(): Boolean {
+ val currentTimeMillis = System.currentTimeMillis()
+ val diffMillis = currentTimeMillis - this
+ return TimeUnit.MILLISECONDS.toMinutes(diffMillis) < 1
+}
+
+fun String.splitIdTimestamp() = split("|")
+
+fun toIdTimestamp(sharedPreferencesHelper: SharedPreferencesHelper): IdTimestamp? =
+ sharedPreferencesHelper
+ .read(SharedPreferenceKey.SEARCH_PATIENT_ID_TIMESTAMP.name, null)
+ ?.splitIdTimestamp()
+ ?.map { IdTimestamp(it.first().toString(), it.last().toString().toLong()) }
+ ?.firstOrNull()
+
+suspend fun getIdentifier(fhirEngine: FhirEngine) =
+ fhirEngine
+ .search { filter(ListResource.TITLE, { value = "Patient Identifier List" }) }
+ .map { it.resource }
+ .firstOrNull()
+
+data class IdTimestamp(
+ val logicalId: String,
+ val timestamp: Long,
+)
+
+fun perOrgSyncConfig(
+ configurationRegistry: ConfigurationRegistry,
+ sharedPreferencesHelper: SharedPreferencesHelper,
+): Item? =
+ configurationRegistry.getPerOrgSyncConfigs()?.items?.find {
+ it.id == sharedPreferencesHelper.organisationCode()
+ }
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/broadcast/SyncStatusBroadcastReceiver.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/broadcast/SyncStatusBroadcastReceiver.kt
new file mode 100644
index 0000000000..e9a5ef3d31
--- /dev/null
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/broadcast/SyncStatusBroadcastReceiver.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 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.data.remote.resource.syncStrategy.broadcast
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.fhir.ParamSyncStatus
+
+const val SYNC_STATUS_BROADCAST_RECEIVER_KEY = "SYNC_STATUS_BROADCAST_RECEIVER_KEY"
+
+@Suppress("DEPRECATION")
+class SyncStatusBroadcastReceiver(
+ val onReceiveCallback: (paramSyncStatus: ParamSyncStatus) -> Unit,
+) : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (intent == null) return
+ val paramSyncStatus =
+ intent.getSerializableExtra(SYNC_STATUS_BROADCAST_RECEIVER_KEY) as? ParamSyncStatus ?: return
+ onReceiveCallback(paramSyncStatus)
+ }
+}
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/fhir/IdentifierSyncParams.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/fhir/IdentifierSyncParams.kt
new file mode 100644
index 0000000000..21bf958d6c
--- /dev/null
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/fhir/IdentifierSyncParams.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2021 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.data.remote.resource.syncStrategy.fhir
+
+import com.google.android.fhir.sync.DownloadWorkManager
+import com.google.android.fhir.sync.download.DownloadRequest
+import java.util.LinkedList
+import org.hl7.fhir.exceptions.FHIRException
+import org.hl7.fhir.r4.model.Bundle
+import org.hl7.fhir.r4.model.OperationOutcome
+import org.hl7.fhir.r4.model.Resource
+import org.hl7.fhir.r4.model.ResourceType
+
+class IdentifierSyncParams(
+ private val identifiers: List,
+ private val callback: (ParamSyncStatus) -> Unit,
+) : DownloadWorkManager {
+
+ private val urlOfTheNextPagesToDownloadForAResource = LinkedList()
+ private val resourcesToDownloadWithSearchParams = LinkedList(identifiers.chunked(12))
+ private var patientPosition = 0
+
+ override suspend fun getNextRequest(): DownloadRequest? {
+ if (urlOfTheNextPagesToDownloadForAResource.isNotEmpty()) {
+ return urlOfTheNextPagesToDownloadForAResource.poll()?.let { DownloadRequest.of(it) }
+ }
+
+ return resourcesToDownloadWithSearchParams.poll()?.let { ids ->
+ DownloadRequest.of(bundle = ids.bundleOf().also { patientPosition += ids.size })
+ }
+ }
+
+ override suspend fun processResponse(response: Resource): Collection {
+ if (response is OperationOutcome) {
+ throw FHIRException(response.issueFirstRep.diagnostics)
+ }
+
+ if ((response !is Bundle || response.type != Bundle.BundleType.BATCHRESPONSE)) {
+ return emptyList()
+ }
+
+ response.link
+ .firstOrNull { component -> component.relation == "next" }
+ ?.url
+ ?.let { next -> urlOfTheNextPagesToDownloadForAResource.add(next) }
+
+ return response.entry
+ .mapNotNull { it.resource as Bundle }
+ .map { it.entry.map { it.resource } }
+ .flatten()
+ .also { catchIds() }
+ }
+
+ private fun catchIds() =
+ callback(ParamSyncStatus(identifiers.map { it.toString() }, identifiers.size, patientPosition))
+
+ private fun List.bundleOf(): Bundle {
+ return Bundle().apply {
+ type = Bundle.BundleType.BATCH
+ entry = bundleEntryComponent()
+ }
+ }
+
+ private fun List.bundleEntryComponent(): List {
+ return flatMap {
+ listOf("Patient?identifier=$it").map { url ->
+ Bundle.BundleEntryComponent().apply {
+ request =
+ Bundle.BundleEntryRequestComponent().apply {
+ method = Bundle.HTTPVerb.GET
+ this.url = url
+ }
+ }
+ }
+ }
+ }
+
+ override suspend fun getSummaryRequestUrls() = mapOf()
+}
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/fhir/LogicalIdSyncParamsBased.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/fhir/LogicalIdSyncParamsBased.kt
new file mode 100644
index 0000000000..11f07c7427
--- /dev/null
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/fhir/LogicalIdSyncParamsBased.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2021 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.data.remote.resource.syncStrategy.fhir
+
+import com.google.android.fhir.sync.DownloadWorkManager
+import com.google.android.fhir.sync.download.DownloadRequest
+import java.util.LinkedList
+import org.hl7.fhir.exceptions.FHIRException
+import org.hl7.fhir.r4.model.Bundle
+import org.hl7.fhir.r4.model.OperationOutcome
+import org.hl7.fhir.r4.model.Resource
+import org.hl7.fhir.r4.model.ResourceType
+
+class LogicalIdSyncParamsBased(
+ private val logicalIds: List,
+ private val callback: (ParamSyncStatus) -> Unit,
+) : DownloadWorkManager {
+
+ private val urlOfTheNextPagesToDownloadForAResource = LinkedList()
+ private val resourcesToDownloadWithSearchParams = LinkedList(logicalIds.chunked(12))
+ private var patientPosition = 0
+
+ override suspend fun getNextRequest(): DownloadRequest? {
+ if (urlOfTheNextPagesToDownloadForAResource.isNotEmpty()) {
+ return urlOfTheNextPagesToDownloadForAResource.poll()?.let { DownloadRequest.of(it) }
+ }
+
+ return resourcesToDownloadWithSearchParams.poll()?.let { ids ->
+ DownloadRequest.of(bundle = ids.bundleOf().also { patientPosition += ids.size })
+ }
+ }
+
+ override suspend fun processResponse(response: Resource): Collection {
+ if (response is OperationOutcome) {
+ throw FHIRException(response.issueFirstRep.diagnostics)
+ }
+
+ if ((response !is Bundle || response.type != Bundle.BundleType.BATCHRESPONSE)) {
+ return emptyList()
+ }
+
+ response.link
+ .firstOrNull { component -> component.relation == "next" }
+ ?.url
+ ?.let { next -> urlOfTheNextPagesToDownloadForAResource.add(next) }
+
+ return response.entry
+ .mapNotNull { it.resource as Bundle }
+ .map { it.entry.map { it.resource } }
+ .flatten()
+ .also { catchIds() }
+ }
+
+ private fun catchIds() = callback(ParamSyncStatus(logicalIds, logicalIds.size, patientPosition))
+
+ private fun List.bundleOf(): Bundle {
+ return Bundle().apply {
+ type = Bundle.BundleType.BATCH
+ entry = bundleEntryComponent()
+ }
+ }
+
+ private fun List.bundleEntryComponent(): List {
+ return flatMap {
+ listOf(
+ "Patient?_id=$it&_include=Patient:link&_include=Patient:general-practitioner",
+ "Observation?subject=$it&status=preliminary",
+ "CarePlan?subject=$it&_count=1&status=completed&_sort=-_lastUpdated",
+ "CarePlan?subject=$it&status=active,on-hold&_revinclude=Task:based-on",
+ "Task?patient=$it&_count=100000",
+ "Condition?subject=$it&clinical-status=active",
+ "Appointment?actor=$it&status=waitlist,booked,noshow",
+ "List?subject=$it&status=current",
+ )
+ .map { url ->
+ Bundle.BundleEntryComponent().apply {
+ request =
+ Bundle.BundleEntryRequestComponent().apply {
+ method = Bundle.HTTPVerb.GET
+ this.url = url
+ }
+ }
+ }
+ }
+ }
+
+ override suspend fun getSummaryRequestUrls() = mapOf()
+}
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/fhir/ParamSyncStatus.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/fhir/ParamSyncStatus.kt
new file mode 100644
index 0000000000..b3006e4da3
--- /dev/null
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/fhir/ParamSyncStatus.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 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.data.remote.resource.syncStrategy.fhir
+
+import java.io.Serializable
+
+data class ParamSyncStatus(
+ val logicalId: List,
+ val idsTotal: Int,
+ val patientPositionAt: Int,
+) : Serializable
diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/utils/SyncState.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/utils/SyncState.kt
index 0a72b8a935..e08bec6dce 100644
--- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/utils/SyncState.kt
+++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/resource/syncStrategy/utils/SyncState.kt
@@ -17,9 +17,9 @@
package org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils
enum class SyncState(val value: Long) {
- ShowDialog(value = 0),
InitialSync(value = 1),
CompletedInitialSync(value = 2),
SubSequentSync(value = 3),
SubSequentRunSync(value = 4),
+ RunSyncNow(value = 5),
}
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 3fb60ddca4..5914c43e14 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
@@ -17,6 +17,7 @@
package org.smartregister.fhircore.engine.sync
import android.content.Context
+import android.content.Intent
import androidx.hilt.work.HiltWorker
import androidx.work.WorkerParameters
import com.google.android.fhir.FhirEngine
@@ -27,20 +28,30 @@ import com.google.android.fhir.sync.FhirSyncWorker
import com.google.android.fhir.sync.upload.UploadStrategy
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.hl7.fhir.r4.model.ResourceType
+import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.configuration.preferences.SyncUploadStrategy
+import org.smartregister.fhircore.engine.data.local.TingatheDatabase
+import org.smartregister.fhircore.engine.data.local.syncStrategy.toEntity
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.SyncParamStrategy
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.broadcast.SYNC_STATUS_BROADCAST_RECEIVER_KEY
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.broadcast.SyncStatusBroadcastReceiver
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.fhir.LogicalIdSyncParamsBased
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.fhir.ResourceParamsBasedDownload
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.fhir.TimestampContext
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SyncState.CompletedInitialSync
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SyncState.InitialSync
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.hasCompletedInitialSync
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.logicalIds
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.perOrgSyncConfig
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.saveLastUpdatedTimestamp
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.subListIds
import org.smartregister.fhircore.engine.ui.questionnaire.ContentCache
import org.smartregister.fhircore.engine.util.AppDataStore
import org.smartregister.fhircore.engine.util.DispatcherProvider
-import org.smartregister.fhircore.engine.util.SharedPreferenceKey
-import org.smartregister.fhircore.engine.util.SharedPreferenceKey.SYNC_STATUS
+import org.smartregister.fhircore.engine.util.SharedPreferenceKey.SYNC_UPLOAD_STRATEGY
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
+import timber.log.Timber
@HiltWorker
class AppSyncWorker
@@ -53,23 +64,53 @@ constructor(
val dataStore: AppDataStore,
val preference: SharedPreferencesHelper,
val dispatcherProvider: DispatcherProvider,
+ val configurationRegistry: ConfigurationRegistry,
+ val syncBroadcaster: SyncBroadcaster,
+ database: TingatheDatabase,
) : FhirSyncWorker(appContext, workerParams) {
- private fun syncParams(): Map> {
- return if (
- preference.read(
- SYNC_STATUS.name,
- InitialSync.value,
- ) <= CompletedInitialSync.value
- ) {
- preference.write(SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, null)
- SyncParamStrategy(preference).syncParams()
- } else syncListenerManager.loadSyncParams()
+ private val syncStrategyCacheDao = database.syncStrategyCacheDao
+
+ private fun downloadWorkManager(): DownloadWorkManager {
+ return when {
+ hasCompletedInitialSync(preference) -> defaultDownloadManager()
+ else -> {
+ val subList = logicalIds(engine).subListIds(syncStrategyCacheDao)
+ return if (subList.isNotEmpty()) {
+ LogicalIdSyncParamsBased(subList) {
+ Timber.tag("TAG")
+ .e("downloadWorkManager: " + it.patientPositionAt + " of " + it.idsTotal)
+ runBlocking {
+ syncStrategyCacheDao.upsert(it.logicalId.toEntity())
+ saveLastUpdatedTimestamp(dataStore)
+ }
+ val broadcastIntent =
+ Intent(SyncStatusBroadcastReceiver::class.java.name).apply {
+ putExtra(SYNC_STATUS_BROADCAST_RECEIVER_KEY, it)
+ }
+ dataStore.context.sendBroadcast(broadcastIntent)
+ }
+ } else {
+ defaultDownloadManager()
+ }
+ }
+ }
}
- override fun getConflictResolver(): ConflictResolver = AcceptLocalConflictResolver
+ private fun syncParams(): Map> {
+ val configs =
+ perOrgSyncConfig(configurationRegistry, preference)
+ ?: return syncListenerManager.loadSyncParams()
- override fun getDownloadWorkManager(): DownloadWorkManager =
+ if (configs.offlineFirst) return syncListenerManager.loadSyncParams()
+
+ return when {
+ hasCompletedInitialSync(preference) -> SyncParamStrategy(preference).syncParams()
+ else -> syncListenerManager.loadSyncParams()
+ }
+ }
+
+ private fun defaultDownloadManager() =
ResourceParamsBasedDownload(
syncParams = syncParams(),
context =
@@ -86,6 +127,10 @@ constructor(
},
)
+ override fun getConflictResolver(): ConflictResolver = AcceptLocalConflictResolver
+
+ override fun getDownloadWorkManager(): DownloadWorkManager = downloadWorkManager()
+
override suspend fun doWork(): Result {
// Cache resources that might be needed urgent before sync
ContentCache.saveResources(engine)
@@ -97,7 +142,7 @@ constructor(
val strategy =
SyncUploadStrategy.valueOf(
preference.read(
- SharedPreferenceKey.SYNC_UPLOAD_STRATEGY.name,
+ SYNC_UPLOAD_STRATEGY.name,
SyncUploadStrategy.Default.name,
) ?: SyncUploadStrategy.Default.name,
)
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 2c94db3565..1f981f5fc7 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
@@ -35,4 +35,6 @@ enum class SharedPreferenceKey {
SYNC_ON_SAVE,
SYNC_STATUS,
PATIENT_IDENTIFIER_LIST_TIMESTAMP,
+ SEARCH_PATIENT_ID_TIMESTAMP,
+ SEARCH_PATIENT_ID_OPEN,
}
diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts
index 732545242e..38d8826880 100644
--- a/android/quest/build.gradle.kts
+++ b/android/quest/build.gradle.kts
@@ -147,8 +147,8 @@ android {
dimension = "apps"
applicationIdSuffix = ".mwcoreDev"
versionNameSuffix = "-mwcoreDev"
- versionCode = 38
- versionName = "0.2.0.2"
+ versionCode = 40
+ versionName = "0.2.1.0-fhir-sdk-sync-strategy"
}
create("mwcoreStaging") {
dimension = "apps"
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 4b6b852289..97a8b6da34 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
@@ -18,6 +18,8 @@ package org.smartregister.fhircore.quest.ui.main
import android.app.Activity
import android.content.Intent
+import android.content.IntentFilter
+import android.os.Build
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
@@ -28,10 +30,12 @@ import com.google.android.fhir.sync.SyncJobStatus
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.max
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.MutableStateFlow
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.R
import org.smartregister.fhircore.engine.configuration.app.ConfigService
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.broadcast.SyncStatusBroadcastReceiver
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.fhir.ParamSyncStatus
import org.smartregister.fhircore.engine.data.remote.shared.TokenAuthenticator
import org.smartregister.fhircore.engine.sync.OnSyncListener
import org.smartregister.fhircore.engine.sync.SyncBroadcaster
@@ -63,6 +67,8 @@ open class AppMainActivity : BaseMultiLanguageActivity(), OnSyncListener {
val appMainViewModel by viewModels()
+ private lateinit var syncStatusBroadcastReceiver: SyncStatusBroadcastReceiver
+
private val authActivityLauncherForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { res ->
if (res.resultCode == Activity.RESULT_OK) {
@@ -73,11 +79,35 @@ open class AppMainActivity : BaseMultiLanguageActivity(), OnSyncListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupTimeOutListener()
- setContent { AppTheme { MainScreen(appMainViewModel = appMainViewModel) } }
+ val paramSyncStatus = MutableStateFlow(null)
+ syncStatusBroadcastReceiver = SyncStatusBroadcastReceiver {
+ showToast("Syncing ${it.patientPositionAt} of ${it.idsTotal}")
+ paramSyncStatus.value = it
+ }
+ val intentFilter = IntentFilter(SyncStatusBroadcastReceiver::class.java.name)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ registerReceiver(syncStatusBroadcastReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
+ } else {
+ registerReceiver(syncStatusBroadcastReceiver, intentFilter)
+ }
+
+ setContent {
+ AppTheme {
+ MainScreen(
+ appMainViewModel = appMainViewModel,
+ paramSyncStatus = paramSyncStatus,
+ )
+ }
+ }
syncBroadcaster.registerSyncListener(this, lifecycleScope)
scheduleAuthWorkers()
}
+ override fun onDestroy() {
+ super.onDestroy()
+ unregisterReceiver(syncStatusBroadcastReceiver)
+ }
+
override fun onResume() {
super.onResume()
// appMainViewModel.updateRefreshState()
@@ -95,7 +125,14 @@ open class AppMainActivity : BaseMultiLanguageActivity(), OnSyncListener {
}
is SyncJobStatus.InProgress -> {
Timber.d(
- "Syncing in progress: ${state.syncOperation.name} ${state.completed.div(max(state.total, 1).toDouble()).times(100)}%",
+ "Syncing in progress: ${state.syncOperation.name} ${
+ state.completed.div(
+ max(
+ state.total,
+ 1,
+ ).toDouble(),
+ ).times(100)
+ }%",
)
appMainViewModel.onEvent(
AppMainEvent.UpdateSyncState(state, getString(R.string.syncing_in_progress)),
@@ -103,7 +140,7 @@ open class AppMainActivity : BaseMultiLanguageActivity(), OnSyncListener {
}
is SyncJobStatus.Failed -> {
if (
- !state?.exceptions.isNullOrEmpty() &&
+ !state.exceptions.isNullOrEmpty() &&
state.exceptions.first().resourceType == ResourceType.Flag
) {
showToast(state.exceptions.first().exception.message!!)
@@ -111,9 +148,9 @@ open class AppMainActivity : BaseMultiLanguageActivity(), OnSyncListener {
}
showToast(getString(R.string.sync_failed_text))
val hasAuthError =
- state?.exceptions?.any {
+ state.exceptions.any {
it.exception is HttpException && (it.exception as HttpException).code() == 401
- } ?: false
+ }
if (hasAuthError) {
showToast(getString(R.string.session_expired))
}
@@ -134,7 +171,7 @@ open class AppMainActivity : BaseMultiLanguageActivity(), OnSyncListener {
},
)
}
- Timber.w(state?.exceptions?.joinToString { it.exception.message.toString() })
+ Timber.w(state.exceptions.joinToString { it.exception.message.toString() })
scheduleFhirBackgroundWorkers()
}
is SyncJobStatus.Succeeded -> {
diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainScreen.kt
index e78a8310fb..74a7511840 100644
--- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainScreen.kt
+++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainScreen.kt
@@ -35,8 +35,10 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.smartregister.fhircore.engine.appfeature.model.HealthModule
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.fhir.ParamSyncStatus
import org.smartregister.fhircore.engine.domain.model.SideMenuOption
import org.smartregister.fhircore.engine.ui.settings.SettingsScreen
import org.smartregister.fhircore.quest.R
@@ -66,6 +68,7 @@ import org.smartregister.fhircore.quest.ui.tracing.register.TracingRegisterScree
fun MainScreen(
modifier: Modifier = Modifier,
appMainViewModel: AppMainViewModel = hiltViewModel(),
+ paramSyncStatus: MutableStateFlow,
) {
val navController = rememberNavController()
val scope = rememberCoroutineScope()
@@ -115,6 +118,7 @@ fun MainScreen(
openDrawer = openDrawer,
sideMenuOptions = uiState.sideMenuOptions,
appMainViewModel = appMainViewModel,
+ paramSyncStatus = paramSyncStatus,
)
}
}
@@ -128,6 +132,7 @@ private fun AppMainNavigationGraph(
sideMenuOptions: List,
measureReportViewModel: MeasureReportViewModel = hiltViewModel(),
appMainViewModel: AppMainViewModel,
+ paramSyncStatus: MutableStateFlow,
) {
val firstSideMenuOption = sideMenuOptions.first()
val firstScreenTitle = stringResource(firstSideMenuOption.titleResource)
@@ -148,7 +153,12 @@ private fun AppMainNavigationGraph(
MainNavigationScreen.Home ->
composable(
route =
- "${it.route}${NavigationArg.routePathsOf(includeCommonArgs = true, NavigationArg.SCREEN_TITLE)}",
+ "${it.route}${
+ NavigationArg.routePathsOf(
+ includeCommonArgs = true,
+ NavigationArg.SCREEN_TITLE,
+ )
+ }",
arguments =
commonNavArgs.plus(
navArgument(NavigationArg.SCREEN_TITLE) {
@@ -194,7 +204,13 @@ private fun AppMainNavigationGraph(
MainNavigationScreen.PatientProfile ->
composable(
route =
- "${it.route}${NavigationArg.routePathsOf(includeCommonArgs = true, NavigationArg.PATIENT_ID, NavigationArg.FAMILY_ID)}",
+ "${it.route}${
+ NavigationArg.routePathsOf(
+ includeCommonArgs = true,
+ NavigationArg.PATIENT_ID,
+ NavigationArg.FAMILY_ID,
+ )
+ }",
arguments = commonNavArgs.plus(patientIdNavArgument()),
) {
PatientProfileScreen(navController = navController, appMainViewModel = appMainViewModel)
@@ -202,7 +218,14 @@ private fun AppMainNavigationGraph(
MainNavigationScreen.FixPatientProfile ->
composable(
route =
- "${it.route}${NavigationArg.routePathsOf(includeCommonArgs = true, NavigationArg.PATIENT_ID, FixPatientViewModel.NAVIGATION_ARG_START, FixPatientViewModel.NAVIGATION_ARG_CARE_PLAN)}",
+ "${it.route}${
+ NavigationArg.routePathsOf(
+ includeCommonArgs = true,
+ NavigationArg.PATIENT_ID,
+ FixPatientViewModel.NAVIGATION_ARG_START,
+ FixPatientViewModel.NAVIGATION_ARG_CARE_PLAN,
+ )
+ }",
arguments = commonNavArgs.plus(patientIdNavArgument()),
) {
FixPatientScreen(navController = navController, appMainViewModel = appMainViewModel)
@@ -210,7 +233,13 @@ private fun AppMainNavigationGraph(
MainNavigationScreen.TracingProfile ->
composable(
route =
- "${it.route}${NavigationArg.routePathsOf(includeCommonArgs = true, NavigationArg.PATIENT_ID, NavigationArg.FAMILY_ID)}",
+ "${it.route}${
+ NavigationArg.routePathsOf(
+ includeCommonArgs = true,
+ NavigationArg.PATIENT_ID,
+ NavigationArg.FAMILY_ID,
+ )
+ }",
arguments = commonNavArgs.plus(patientIdNavArgument()),
) {
TracingProfileScreen(navController = navController, appViewModel = appMainViewModel)
@@ -232,7 +261,12 @@ private fun AppMainNavigationGraph(
MainNavigationScreen.GuardianProfile ->
composable(
route =
- "${it.route}/{${NavigationArg.PATIENT_ID}}${NavigationArg.routePathsOf(includeCommonArgs = true, NavigationArg.ON_ART)}",
+ "${it.route}/{${NavigationArg.PATIENT_ID}}${
+ NavigationArg.routePathsOf(
+ includeCommonArgs = true,
+ NavigationArg.ON_ART,
+ )
+ }",
arguments =
commonNavArgs.plus(
listOf(
@@ -258,7 +292,12 @@ private fun AppMainNavigationGraph(
MainNavigationScreen.FamilyProfile ->
composable(
route =
- "${it.route}${NavigationArg.routePathsOf(includeCommonArgs = true, NavigationArg.PATIENT_ID)}",
+ "${it.route}${
+ NavigationArg.routePathsOf(
+ includeCommonArgs = true,
+ NavigationArg.PATIENT_ID,
+ )
+ }",
arguments = commonNavArgs.plus(patientIdNavArgument()),
) {
FamilyProfileScreen(navController = navController)
@@ -266,7 +305,12 @@ private fun AppMainNavigationGraph(
MainNavigationScreen.ViewChildContacts ->
composable(
route =
- "${it.route}${NavigationArg.routePathsOf(includeCommonArgs = true, NavigationArg.PATIENT_ID)}",
+ "${it.route}${
+ NavigationArg.routePathsOf(
+ includeCommonArgs = true,
+ NavigationArg.PATIENT_ID,
+ )
+ }",
arguments = commonNavArgs.plus(patientIdNavArgument()),
) {
ChildContactsProfileScreen(navController = navController)
@@ -274,7 +318,12 @@ private fun AppMainNavigationGraph(
MainNavigationScreen.TracingHistory ->
composable(
route =
- "${it.route}${NavigationArg.routePathsOf(includeCommonArgs = true, NavigationArg.PATIENT_ID)}",
+ "${it.route}${
+ NavigationArg.routePathsOf(
+ includeCommonArgs = true,
+ NavigationArg.PATIENT_ID,
+ )
+ }",
arguments = commonNavArgs.plus(patientIdNavArgument()),
) {
TracingHistoryScreen(navController = navController)
@@ -282,7 +331,13 @@ private fun AppMainNavigationGraph(
MainNavigationScreen.TracingOutcomes ->
composable(
route =
- "${it.route}${NavigationArg.routePathsOf(includeCommonArgs = true, NavigationArg.PATIENT_ID, NavigationArg.TRACING_ID)}",
+ "${it.route}${
+ NavigationArg.routePathsOf(
+ includeCommonArgs = true,
+ NavigationArg.PATIENT_ID,
+ NavigationArg.TRACING_ID,
+ )
+ }",
arguments =
commonNavArgs.plus(
listOf(
@@ -299,7 +354,15 @@ private fun AppMainNavigationGraph(
MainNavigationScreen.TracingHistoryDetails ->
composable(
route =
- "${it.route}${NavigationArg.routePathsOf(includeCommonArgs = true, NavigationArg.PATIENT_ID, NavigationArg.TRACING_ID, NavigationArg.TRACING_ENCOUNTER_ID, NavigationArg.SCREEN_TITLE)}",
+ "${it.route}${
+ NavigationArg.routePathsOf(
+ includeCommonArgs = true,
+ NavigationArg.PATIENT_ID,
+ NavigationArg.TRACING_ID,
+ NavigationArg.TRACING_ENCOUNTER_ID,
+ NavigationArg.SCREEN_TITLE,
+ )
+ }",
arguments =
commonNavArgs.plus(
listOf(
diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientRegisterScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientRegisterScreen.kt
index dc5a90f9eb..9e49518f7e 100644
--- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientRegisterScreen.kt
+++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientRegisterScreen.kt
@@ -21,56 +21,26 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
-import androidx.compose.material.Card
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.Icon
-import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
-import androidx.compose.material.SnackbarHost
-import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.CheckCircle
-import androidx.compose.material.icons.rounded.Info
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavHostController
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
-import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.QuestionnaireResponse
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.EventCallback
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.Progress
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SyncState
import org.smartregister.fhircore.engine.ui.components.register.LoaderDialog
import org.smartregister.fhircore.engine.ui.components.register.RegisterHeader
import org.smartregister.fhircore.engine.ui.questionnaire.QuestionnaireActivity
@@ -79,7 +49,6 @@ import org.smartregister.fhircore.engine.util.extension.extractId
import org.smartregister.fhircore.quest.ui.components.RegisterFooter
import org.smartregister.fhircore.quest.ui.components.RegisterList
import org.smartregister.fhircore.quest.ui.main.components.TopScreenSection
-import org.smartregister.fhircore.quest.ui.patient.register.components.KeepScreenOn
import org.smartregister.fhircore.quest.ui.shared.models.RegisterViewData
@Composable
@@ -124,175 +93,7 @@ fun PatientRegisterScreen(
.value
.collectAsLazyPagingItems()
- LaunchedEffect(key1 = searchText.trim()) {
- if (searchText.trim().isNotEmpty()) {
- patientRegisterViewModel.searchPatient(searchText.trim())
- }
- }
-
- val lifecycleOwner = LocalLifecycleOwner.current
- val coroutineScope = rememberCoroutineScope()
-
- val snackbarHostState = remember { SnackbarHostState() }
-
- var showProgressDialog by remember { mutableStateOf(false) }
- var showAttentionDialog by remember { mutableStateOf(false) }
- var onSyncListenerDialog by remember { mutableStateOf(false) }
- var keepScreenOn by remember { mutableStateOf(false) }
-
- LaunchedEffect(key1 = patientRegisterViewModel.channelFlow) {
- lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
- patientRegisterViewModel.channelFlow.collect {
- when (it) {
- is EventCallback.InProgress -> {
- showProgressDialog = true
- keepScreenOn = true
- }
- is EventCallback.Finished -> {
- with(patientRegisterViewModel) {
- onEvent(PatientRegisterEvent.SearchRegister(searchText = ""))
- onEvent(PatientRegisterEvent.OpenProfile(it.logicalId, navController))
- showLocalData()
- keepScreenOn = false
- }
- }
- EventCallback.Stated -> {
- coroutineScope.launch {
- snackbarHostState.showSnackbar(message = "Working on it, please wait ...")
- }
- }
- is EventCallback.OnSyncListener -> {
- with(patientRegisterViewModel.getSyncState() == SyncState.SubSequentSync.value) {
- onSyncListenerDialog = this
- keepScreenOn = not()
- }
- }
- EventCallback.ShowAttentionDialog -> {
- showAttentionDialog = true
- }
- }
- }
- }
- }
-
- val state = patientRegisterViewModel.progressStatusUiState.collectAsState().value
- val status = state.progress ?: Progress(0, 0, "")
-
- LaunchedEffect(key1 = status) {
- if (status.totalResources == status.currentPosition.plus(1)) {
- showProgressDialog = false
- }
- }
-
- KeepScreenOn(keepScreenOn)
-
- if (showProgressDialog) {
- Dialog(onDismissRequest = { showProgressDialog = false }) {
- Card(modifier = Modifier.padding(24.dp)) {
- Column(
- modifier = Modifier.fillMaxWidth().padding(24.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Box(contentAlignment = Alignment.Center) {
- CircularProgressIndicator(
- progress = 1f,
- modifier = Modifier.size(90.dp),
- strokeWidth = 16.dp,
- color = Color.LightGray,
- )
-
- CircularProgressIndicator(
- progress = status.currentPosition.toFloat().div(status.totalResources.toFloat()),
- modifier = Modifier.size(90.dp),
- strokeWidth = 16.dp,
- strokeCap = StrokeCap.Round,
- )
- }
-
- Spacer(modifier = Modifier.height(16.dp))
- Text(
- text = "${status.currentPosition} of ${status.totalResources}",
- style = MaterialTheme.typography.h5,
- color = Color.Gray,
- fontWeight = FontWeight.Bold,
- )
- Text(text = "This takes a while, please wait ...")
- }
- }
- }
- }
-
- if (onSyncListenerDialog) {
- Dialog(onDismissRequest = { onSyncListenerDialog = false }) {
- Card(modifier = Modifier.padding(24.dp)) {
- Column(
- modifier = Modifier.fillMaxWidth().padding(24.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Spacer(modifier = Modifier.size(8.dp))
- Icon(
- imageVector = Icons.Rounded.CheckCircle,
- contentDescription = null,
- modifier = Modifier.size(62.dp),
- tint = Color(0xFF1E5220),
- )
-
- Text(
- text = "Sync Completed",
- style = MaterialTheme.typography.h5,
- color = Color.Gray,
- modifier = Modifier.padding(16.dp),
- )
- }
- }
- }
- }
-
- if (showAttentionDialog) {
- Dialog(
- onDismissRequest = { showAttentionDialog = false },
- properties =
- DialogProperties(
- dismissOnClickOutside = false,
- dismissOnBackPress = false,
- ),
- ) {
- Card(modifier = Modifier.padding(24.dp)) {
- Column(
- modifier = Modifier.fillMaxWidth().padding(24.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Spacer(modifier = Modifier.size(8.dp))
- Icon(
- imageVector = Icons.Rounded.Info,
- contentDescription = null,
- modifier = Modifier.size(62.dp),
- tint = Color(0xFFFFA726),
- )
-
- Text(
- text =
- "Please stay on this screen as the initial sync is in progress, we will notify you know when it is completed.",
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(bottom = 16.dp),
- )
-
- Button(
- onClick = {
- showAttentionDialog = false
- keepScreenOn = true
- patientRegisterViewModel.initialSync()
- },
- ) {
- Text(text = "Continue")
- }
- }
- }
- }
- }
-
Scaffold(
- snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
topBar = {
// Top section has toolbar and a results counts view
TopScreenSection(
@@ -376,15 +177,6 @@ fun PatientRegisterScreen(
modifier = iModifier,
pagingItems = pagingItems,
onRowClick = { patientId: String ->
- pagingItems.itemSnapshotList.items
- .find { it.logicalId == patientId }
- ?.let {
- if (it.isLocal.not()) {
- patientRegisterViewModel.fetchAndSaveToDb(it.logicalId)
- return@RegisterList
- }
- }
-
patientRegisterViewModel.onEvent(
PatientRegisterEvent.OpenProfile(patientId, navController),
)
diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientRegisterViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientRegisterViewModel.kt
index ce72085403..aa24e22414 100644
--- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientRegisterViewModel.kt
+++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientRegisterViewModel.kt
@@ -23,9 +23,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
-import androidx.paging.PagingData
-import androidx.paging.PagingSource
-import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.paging.filter
import androidx.paging.map
@@ -34,11 +31,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -46,10 +41,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -58,28 +51,18 @@ import org.smartregister.fhircore.engine.appfeature.AppFeature
import org.smartregister.fhircore.engine.appfeature.AppFeatureManager
import org.smartregister.fhircore.engine.appfeature.model.HealthModule
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
+import org.smartregister.fhircore.engine.data.local.TingatheDatabase
import org.smartregister.fhircore.engine.data.local.register.AppRegisterRepository
-import org.smartregister.fhircore.engine.data.local.register.dao.HivRegisterDao
-import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirApiService
-import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceService
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.ApiRepositoryImpl
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.EventCallback
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.ListResourceExt
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.Progress
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SearchBy
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.logicalIds
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.perOrgSyncConfig
+import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.subListIds
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SyncState
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SyncState.CompletedInitialSync
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SyncState.InitialSync
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SyncState.SubSequentRunSync
-import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SyncState.SubSequentSync
import org.smartregister.fhircore.engine.domain.util.PaginationConstant
import org.smartregister.fhircore.engine.sync.SyncBroadcaster
import org.smartregister.fhircore.engine.ui.questionnaire.QuestionnaireActivity
import org.smartregister.fhircore.engine.ui.questionnaire.QuestionnaireType
-import org.smartregister.fhircore.engine.util.AppDataStore
import org.smartregister.fhircore.engine.util.DispatcherProvider
-import org.smartregister.fhircore.engine.util.SharedPreferenceKey.LAST_SYNC_TIMESTAMP
-import org.smartregister.fhircore.engine.util.SharedPreferenceKey.SYNC_STATUS
+import org.smartregister.fhircore.engine.util.SharedPreferenceKey
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import org.smartregister.fhircore.quest.R
import org.smartregister.fhircore.quest.data.patient.model.PatientPagingSourceState
@@ -89,7 +72,6 @@ import org.smartregister.fhircore.quest.navigation.NavigationArg
import org.smartregister.fhircore.quest.ui.shared.models.RegisterViewData
import org.smartregister.fhircore.quest.util.REGISTER_FORM_ID_KEY
import org.smartregister.fhircore.quest.util.mappers.RegisterViewDataMapper
-import timber.log.Timber
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
@HiltViewModel
@@ -104,33 +86,16 @@ constructor(
val appFeatureManager: AppFeatureManager,
val dispatcherProvider: DispatcherProvider,
val sharedPreferencesHelper: SharedPreferencesHelper,
- fhirResourceService: FhirResourceService,
- fhirApiService: FhirApiService,
- val hivRegisterDao: HivRegisterDao,
- appDataStore: AppDataStore,
+ database: TingatheDatabase,
) : ViewModel() {
private val appFeatureName = savedStateHandle.get(NavigationArg.FEATURE)
private val healthModule =
savedStateHandle.get(NavigationArg.HEALTH_MODULE) ?: HealthModule.DEFAULT
+ private val dao = database.syncStrategyCacheDao
private val _isRefreshing = MutableStateFlow(false)
- private val apiRepository =
- ApiRepositoryImpl(
- fhirApiService = fhirApiService,
- fhirEngine = syncBroadcaster.fhirEngine,
- sharedPreferencesHelper = sharedPreferencesHelper,
- appDataStore = appDataStore,
- fhirResourceService = fhirResourceService,
- )
-
- private val _progressStatusUiState = MutableStateFlow(PatientUiState())
- val progressStatusUiState = _progressStatusUiState.asStateFlow()
- private val _channel = Channel()
- val channelFlow = _channel.receiveAsFlow()
- private var invokeCounter = 0
-
val isRefreshing: StateFlow
get() = _isRefreshing.asStateFlow()
@@ -165,7 +130,7 @@ constructor(
private val _syncProgressMutableStateFlow = MutableStateFlow("")
val syncProgressStateFlow = _syncProgressMutableStateFlow.asStateFlow()
- private val _paginatedRegisterViewData: Flow>> =
+ private val _paginatedRegisterViewData =
combine(
searchedText,
currentPage,
@@ -187,7 +152,7 @@ constructor(
return@mapLatest pagingFlow.cachedIn(viewModelScope).also { _isRefreshing.emit(false) }
}
- var pageRegisterListItemData =
+ val pageRegisterListItemData =
_paginatedRegisterViewData
.map { pagingDataFlow ->
pagingDataFlow.map { pagingData ->
@@ -217,68 +182,8 @@ constructor(
initialValue = emptyFlow(),
)
- data class PatientUiState(val progress: Progress? = null)
-
- fun initialSync() {
- sharedPreferencesHelper.write(SYNC_STATUS.name, InitialSync.value)
- syncBroadcaster.runSync()
- }
-
- fun getSyncState() = sharedPreferencesHelper.read(SYNC_STATUS.name, SyncState.ShowDialog.value)
-
- private suspend fun onSetupState() {
- if (getSyncState() == SyncState.ShowDialog.value) {
- _channel.send(EventCallback.ShowAttentionDialog)
- }
-
- if (getSyncState() == InitialSync.value) {
- sharedPreferencesHelper.write(
- SYNC_STATUS.name,
- CompletedInitialSync.value,
- )
- }
-
- if (getSyncState() == CompletedInitialSync.value || getSyncState() == SubSequentRunSync.value) {
- apiRepository.invoke(
- onCompleteListener = { patientSize ->
- patientSize
- .takeIf { it > 0 }
- ?.let {
- viewModelScope.launch {
- _channel.send(EventCallback.OnSyncListener(true))
- syncBroadcaster.runSync()
- }
- }
- },
- onProgress = { progressUiState ->
- viewModelScope.launch { _channel.send(EventCallback.InProgress) }
- _progressStatusUiState.update { it.copy(progress = progressUiState) }
- },
- )
- }
-
- if (getSyncState() == SubSequentSync.value) {
- ListResourceExt(
- fhirEngine = syncBroadcaster.fhirEngine,
- sharedPreferences = sharedPreferencesHelper,
- apiRepository = apiRepository,
- )
- .onListResourceExt { progressUiState ->
- if (progressUiState.totalResources > 0) {
- viewModelScope.launch {
- _channel.send(EventCallback.InProgress)
- _progressStatusUiState.update { it.copy(progress = progressUiState) }
-
- if (progressUiState.currentPosition.plus(1) == progressUiState.totalResources) {
- sharedPreferencesHelper.write(SYNC_STATUS.name, SubSequentRunSync.value)
- _channel.send(EventCallback.OnSyncListener(true))
- syncBroadcaster.runSync()
- }
- }
- }
- }
- }
- }
+ private fun getSyncState() =
+ sharedPreferencesHelper.read(SharedPreferenceKey.SYNC_STATUS.name, SyncState.InitialSync.value)
init {
syncBroadcaster.registerSyncListener(
@@ -291,18 +196,32 @@ constructor(
is SyncJobStatus.Succeeded -> {
refresh()
_firstTimeSyncState.value = false
- viewModelScope.launch {
- invokeCounter += 1
- if (invokeCounter == 1) {
- onSetupState()
+ viewModelScope.launch(Dispatchers.IO) {
+ val configs =
+ perOrgSyncConfig(configurationRegistry, sharedPreferencesHelper) ?: return@launch
+
+ if (configs.offlineFirst) return@launch
+
+ if (getSyncState() == SyncState.InitialSync.value) {
+ sharedPreferencesHelper.write(
+ SharedPreferenceKey.SYNC_STATUS.name,
+ SyncState.CompletedInitialSync.value,
+ )
+ syncBroadcaster.runSync()
}
- Timber.e("invokeCounter -> $invokeCounter")
+ if (getSyncState() < SyncState.SubSequentSync.value) {
+ if (logicalIds(syncBroadcaster.fhirEngine).subListIds(dao).isNotEmpty()) {
+ syncBroadcaster.runSync()
+ } else {
+ sharedPreferencesHelper.write(
+ SharedPreferenceKey.SYNC_STATUS.name,
+ SyncState.SubSequentSync.value,
+ )
+ }
+ }
}
}
- is SyncJobStatus.Started -> {
- invokeCounter = 0
- }
is SyncJobStatus.InProgress -> {
updateSyncProgress(state)
_firstTimeSyncState.value = isFirstTimeSync()
@@ -343,102 +262,6 @@ constructor(
)
.flow
- private var searchJob: Job? = null
-
- fun searchPatient(searchQuery: String) {
- // searchJob?.cancel()
- viewModelScope.launch {
- // delay(2000)
- val criteria =
- when {
- searchQuery.any { it.isDigit() } -> SearchBy.IDENTIFIER
- else -> SearchBy.HUMAN_NAME
- }
-
- pageRegisterListItemData =
- patientIdentifierPager(searchQuery, criteria)
- .cachedIn(viewModelScope)
- .map { flowOf(it) }
- .stateIn(
- viewModelScope.plus(dispatcherProvider.io()),
- SharingStarted.Lazily,
- initialValue = emptyFlow(),
- )
- }
- }
-
- fun showLocalData() {
- pageRegisterListItemData =
- _paginatedRegisterViewData
- .map { pagingDataFlow ->
- pagingDataFlow.map { pagingData ->
- pagingData
- .filter { it is RegisterViewData.ListItemView }
- .map { it as RegisterViewData.ListItemView }
- }
- }
- .stateIn(
- viewModelScope.plus(dispatcherProvider.io()),
- SharingStarted.Lazily,
- initialValue = emptyFlow(),
- )
- }
-
- fun fetchAndSaveToDb(logicalId: String) =
- viewModelScope.launch {
- _channel.send(EventCallback.Stated)
- apiRepository.fetchAndSaveToDb(
- logicalId,
- onCompleteListener = {
- launch {
- _channel.send(EventCallback.Finished(logicalId))
- syncBroadcaster.runSync()
- }
- },
- )
- }
-
- inner class PatientIdentifierPagingSource(
- private val searchQuery: String,
- private val criteria: SearchBy,
- ) : PagingSource() {
- override suspend fun load(
- params: LoadParams,
- ): LoadResult {
- return try {
- val registerViewData =
- apiRepository
- .search(searchQuery, criteria)
- .map { hivRegisterDao.transformPatientToHivRegisterData(it) }
- .map { registerViewDataMapper.transformInputToOutputModel(it).copy(isLocal = false) }
- LoadResult.Page(
- data = registerViewData,
- prevKey = null,
- nextKey = null,
- )
- } catch (exception: Exception) {
- LoadResult.Error(exception)
- }
- }
-
- override fun getRefreshKey(state: PagingState) = null
- }
-
- private fun patientIdentifierPager(
- searchQuery: String,
- criteria: SearchBy,
- ): Flow> =
- Pager(
- config =
- PagingConfig(
- pageSize = PaginationConstant.DEFAULT_PAGE_SIZE,
- initialLoadSize = PaginationConstant.DEFAULT_INITIAL_LOAD_SIZE,
- enablePlaceholders = false,
- ),
- pagingSourceFactory = { PatientIdentifierPagingSource(searchQuery, criteria) },
- )
- .flow
-
private fun getPager(
appFeatureName: String?,
healthModule: HealthModule,
@@ -516,7 +339,7 @@ constructor(
}
fun isFirstTimeSync() =
- sharedPreferencesHelper.read(LAST_SYNC_TIMESTAMP.name, null).isNullOrBlank()
+ sharedPreferencesHelper.read(SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, null).isNullOrBlank()
fun progressMessage() =
if (searchedText.value.isEmpty()) {
diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/dialog/AttentionDialog.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/dialog/AttentionDialog.kt
new file mode 100644
index 0000000000..9d28eae23b
--- /dev/null
+++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/dialog/AttentionDialog.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2021 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.quest.ui.patient.register.dialog
+
+import androidx.compose.MutableState
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Button
+import androidx.compose.material.Card
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Info
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+
+@Composable
+fun AttentionDialog(
+ show: MutableState,
+ onClick: () -> Unit,
+) {
+ if (show.value) {
+ Dialog(
+ onDismissRequest = { show.value = false },
+ properties =
+ DialogProperties(
+ dismissOnClickOutside = false,
+ dismissOnBackPress = false,
+ ),
+ ) {
+ Card(modifier = Modifier.padding(24.dp)) {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(modifier = Modifier.size(8.dp))
+ Icon(
+ imageVector = Icons.Rounded.Info,
+ contentDescription = null,
+ modifier = Modifier.size(62.dp),
+ tint = Color(0xFFFFA726),
+ )
+ Text(
+ text =
+ "Please stay on this screen as the initial sync is in progress, we will notify you know when it is completed.",
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(bottom = 16.dp),
+ )
+ Button(onClick = onClick) { Text(text = "Continue") }
+ }
+ }
+ }
+ }
+}
diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/dialog/ProgressDialog.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/dialog/ProgressDialog.kt
new file mode 100644
index 0000000000..7c17f8672a
--- /dev/null
+++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/dialog/ProgressDialog.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2021 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.quest.ui.patient.register.dialog
+
+import androidx.compose.MutableState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Card
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+
+@androidx.compose.runtime.Composable
+fun ProgressDialog(
+ show: MutableState,
+ progress: Float,
+ status: String,
+) {
+ if (show.value) {
+ Dialog(onDismissRequest = { show.value = false }) {
+ Card(modifier = Modifier.padding(24.dp)) {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ CircularProgressIndicator(
+ progress = 1f,
+ modifier = Modifier.size(90.dp),
+ strokeWidth = 16.dp,
+ color = Color.LightGray,
+ )
+
+ CircularProgressIndicator(
+ progress = progress,
+ // progress =
+ // status.currentPosition.toFloat().div(status.totalResources.toFloat()),
+ modifier = Modifier.size(90.dp),
+ strokeWidth = 16.dp,
+ strokeCap = StrokeCap.Round,
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = status,
+ // text = "${status.currentPosition} of ${status.totalResources}",
+ style = MaterialTheme.typography.h5,
+ color = Color.Gray,
+ fontWeight = FontWeight.Bold,
+ )
+ Text(text = "This takes a while, please wait ...")
+ }
+ }
+ }
+ }
+}
diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/dialog/SyncCompleteDialog.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/dialog/SyncCompleteDialog.kt
new file mode 100644
index 0000000000..6c550b8799
--- /dev/null
+++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/dialog/SyncCompleteDialog.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2021 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.quest.ui.patient.register.dialog
+
+import androidx.compose.MutableState
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Card
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.CheckCircle
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+
+@Composable
+fun SyncCompleteDialog(show: MutableState) {
+ if (show.value) {
+ Dialog(onDismissRequest = { show.value = false }) {
+ Card(modifier = Modifier.padding(24.dp)) {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(modifier = Modifier.size(8.dp))
+ Icon(
+ imageVector = Icons.Rounded.CheckCircle,
+ contentDescription = null,
+ modifier = Modifier.size(62.dp),
+ tint = Color(0xFF1E5220),
+ )
+
+ Text(
+ text = "Sync Completed",
+ style = MaterialTheme.typography.h5,
+ color = Color.Gray,
+ modifier = Modifier.padding(16.dp),
+ )
+ }
+ }
+ }
+ }
+}