Skip to content
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
val fhirAuthArray = arrayOf(
"FHIR_BASE_URL", "OAUTH_BASE_URL", "OAUTH_CIENT_ID", "OAUTH_CLIENT_SECRET", "OAUTH_SCOPE", "APP_ID", "FHIR_HELPER_SERVICE"
"FHIR_BASE_URL", "OAUTH_BASE_URL", "OAUTH_CIENT_ID", "OAUTH_CLIENT_SECRET", "OAUTH_SCOPE", "APP_ID", "FHIR_HELPER_SERVICE", "FHIR_API_SERVICE"
)
//KEYSTORE CREDENTIALS
val keystoreAuthArray = arrayOf(
Expand Down
1 change: 1 addition & 0 deletions android/dataclerk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ android {
""""${project.extra["OAUTH_CLIENT_SECRET"]}"""",
)
buildConfigField("String", "OAUTH_SCOPE", """"${project.extra["OAUTH_SCOPE"]}"""")
buildConfigField("String", "FHIR_API_SERVICE", """"${project.extra["FHIR_API_SERVICE"]}"""")
buildConfigField(
"String",
"FHIR_HELPER_SERVICE",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class DataClerkConfigService @Inject constructor(@ApplicationContext val context
clientSecret = BuildConfig.OAUTH_CLIENT_SECRET,
accountType = BuildConfig.APPLICATION_ID,
fhirHelperServiceBaseUrl = BuildConfig.FHIR_HELPER_SERVICE,
fhirApiBaseUrl = BuildConfig.FHIR_BASE_URL,
)

override fun defineResourceTags() =
Expand Down
2 changes: 2 additions & 0 deletions android/engine/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,7 @@
tools:node="remove" />
</provider>

<receiver android:name=".data.remote.resource.syncStrategy.broadcast.SyncStatusBroadcastReceiver" />

</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ data class AppConfiguration(
val appFeatures: AppFeatureConfig,
val syncConfig: SyncConfig,
val formConfigs: List<QuestionnaireConfig> = listOf(),
val organizationSyncConfig: OrganizationSyncConfig,
)

@Serializable
Expand All @@ -66,3 +67,12 @@ data class Resource(
data class Parameter(
@SerializedName("resource") var resource: Resource,
)

@Serializable data class OrganizationSyncConfig(@SerializedName("items") val items: List<Item>)

@Serializable
data class Item(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("offline_first") val offlineFirst: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ constructor(

fun getFormConfigs(): List<QuestionnaireConfig>? = applicationConfiguration.value?.formConfigs

fun getPerOrgSyncConfigs(): OrganizationSyncConfig? =
applicationConfiguration.value?.organizationSyncConfig

private suspend fun getBinary(id: String): Binary = fhirEngine.get(id)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ data class AuthConfiguration(
var clientId: String,
var clientSecret: String,
var fhirHelperServiceBaseUrl: String,
var fhirApiBaseUrl: String,
var accountType: String,
var scope: String = "openid",
)
Original file line number Diff line number Diff line change
Expand Up @@ -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<TingatheDatabase> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ constructor(
currentCarePlan = null,
)

private fun transformPatientToHivRegisterData(
fun transformPatientToHivRegisterData(
patient: Patient,
pregnancyStatus: PregnancyStatus = PregnancyStatus.None,
): RegisterData.HivRegisterData {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
abstract class SyncStrategyCacheDao {

suspend fun upsert(logicalId: String) =
with(get(logicalId)) {
if (this == null) {
insert(logicalId.toEntity())
} else {
update(this.logicalId)
}
}

suspend fun upsert(logicalIds: List<String>) =
logicalIds.onEach { logicalId ->
with(get(logicalId)) {
if (this == null) {
insert(logicalId.toEntity())
} else {
update(this.logicalId)
}
}
}

@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insert(syncStrategyCacheEntity: List<SyncStrategyCacheEntity>)

@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insert(syncStrategyCacheEntity: SyncStrategyCacheEntity)

@Query("DELETE FROM syncstrategycacheentity") abstract suspend fun deleteAll()

@Query("SELECT * FROM syncstrategycacheentity WHERE shouldSync = 0")
abstract suspend fun query(): List<SyncStrategyCacheEntity>

@Query("SELECT * FROM syncstrategycacheentity WHERE logicalId = :logicalId")
abstract suspend fun get(logicalId: String): SyncStrategyCacheEntity?

@Query("UPDATE syncstrategycacheentity SET shouldSync = 1 WHERE logicalId = :logicalId")
abstract suspend fun update(logicalId: String)

@Query("UPDATE syncstrategycacheentity SET shouldSync = 0") abstract suspend fun resetAll()

@Delete abstract suspend fun delete(syncStrategyCacheEntity: SyncStrategyCacheEntity)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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<String>.toEntity() = map { SyncStrategyCacheEntity(logicalId = it) }

fun String.toEntity() = SyncStrategyCacheEntity(logicalId = this)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.fhir.resource

import org.hl7.fhir.r4.model.Bundle
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.Path

/** [Retrofit] Service for communication with HAPI FHIR server. Used for querying FHIR Resources */
interface FhirApiService {

@GET("{logicalId}") suspend fun getPatient(@Path("logicalId") logicalId: String): Bundle
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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 java.text.SimpleDateFormat
import java.time.OffsetDateTime
import java.time.ZoneId
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import org.hl7.fhir.r4.model.Patient
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.R
import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceService
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.repository.ApiRepository
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SearchBy
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SearchBy.HUMAN_NAME
import org.smartregister.fhircore.engine.data.remote.resource.syncStrategy.utils.SearchBy.IDENTIFIER
import org.smartregister.fhircore.engine.util.AppDataStore
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper

class ApiRepositoryImpl(
private val fhirResourceService: FhirResourceService,
sharedPreferencesHelper: SharedPreferencesHelper,
) : ApiRepository() {

private val context = sharedPreferencesHelper.context
private val system = context.getString(R.string.sync_strategy_organization_system)
private val tag = "$system%7C${sharedPreferencesHelper.organisationCode()}"

override suspend fun search(searchQuery: String, criteria: SearchBy): List<Patient> =
fhirResourceService
.getResource(
when (criteria) {
IDENTIFIER -> "${ResourceType.Patient.name}?identifier=$searchQuery&_tag=$tag"
HUMAN_NAME ->
"${ResourceType.Patient.name}?given=$searchQuery,family=$searchQuery&_tag=$tag"
},
)
.entry
.map { it.resource as Patient }
}

private const val SYNC_TIMESTAMP_INPUT_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"

enum class OfType {
Patient,
Encounter,
Observation,
Condition,
CarePlan,
List,
Task,
Practitioner,
RelatedPerson,
Appointment,
}

fun simpleDateFormat() = SimpleDateFormat(SYNC_TIMESTAMP_INPUT_FORMAT, Locale.getDefault())

fun Date.toOffsetDateTime(): OffsetDateTime {
return OffsetDateTime.ofInstant(toInstant(), ZoneId.systemDefault())
}

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)
}

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)
}
}
Original file line number Diff line number Diff line change
@@ -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.remote.resource.syncStrategy

sealed interface EventCallback {

data object ShowAttentionDialog : EventCallback

data object InProgress : EventCallback

data object Stated : EventCallback

/**
* @param logicalId [String] The ID is passed when opening `QuestionnaireActivity`
* @see [org.smartregister.fhircore.engine.ui.questionnaire.QuestionnaireActivity]
*/
data class Finished(val logicalId: String) : EventCallback

data class OnSyncListener(val completed: Boolean) : EventCallback
}
Loading