Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix sync timestamp not updating #2514

Merged
merged 8 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.google.android.fhir.FhirEngine
import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.get
import com.google.android.fhir.logicalId
import java.io.FileNotFoundException
import java.net.UnknownHostException
import java.util.LinkedList
import java.util.Locale
Expand All @@ -36,6 +37,7 @@ import org.hl7.fhir.r4.model.Composition
import org.hl7.fhir.r4.model.ListResource
import org.hl7.fhir.r4.model.Resource
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.fhir.resource.FhirResourceDataSource
import org.smartregister.fhircore.engine.util.DispatcherProvider
Expand Down Expand Up @@ -194,35 +196,40 @@ constructor(
// For appId that ends with suffix /debug e.g. app/debug, we load configurations from assets
// extract appId by removing the suffix e.g. app from above example
val loadFromAssets = appId.endsWith(DEBUG_SUFFIX, ignoreCase = true)
val parsedAppId = appId.substringBefore("/").trim()
if (loadFromAssets) {
val parsedAppId = appId.substringBefore("/").trim()
context
.assets
.open(String.format(COMPOSITION_CONFIG_PATH, parsedAppId))
.bufferedReader()
.readText()
.decodeResourceFromString<Composition>()
.run {
val iconConfigs =
retrieveCompositionSections().filter {
it.focus.hasIdentifier() && isIconConfig(it.focus.identifier.value)
try {
context
.assets
.open(String.format(COMPOSITION_CONFIG_PATH, parsedAppId))
.bufferedReader()
.readText()
.decodeResourceFromString<Composition>()
.run {
val iconConfigs =
retrieveCompositionSections().filter {
it.focus.hasIdentifier() && isIconConfig(it.focus.identifier.value)
}
if (iconConfigs.isNotEmpty()) {
val ids = iconConfigs.joinToString(",") { it.focus.extractId() }
fhirResourceDataSource.getResource(
"${ResourceType.Binary.name}?${Composition.SP_RES_ID}=$ids"
)
.entry
.forEach { addOrUpdate(it.resource) }
}
if (iconConfigs.isNotEmpty()) {
val ids = iconConfigs.joinToString(",") { it.focus.extractId() }
fhirResourceDataSource.getResource(
"${ResourceType.Binary.name}?${Composition.SP_RES_ID}=$ids"
)
.entry
.forEach { addOrUpdate(it.resource) }
populateConfigurationsMap(
composition = this,
loadFromAssets = true,
appId = parsedAppId,
configsLoadedCallback = configsLoadedCallback,
context = context
)
}
populateConfigurationsMap(
composition = this,
loadFromAssets = true,
appId = parsedAppId,
configsLoadedCallback = configsLoadedCallback,
context = context
)
}
} catch (fileNotFoundException: FileNotFoundException) {
Timber.e("Missing app configs for app ID: $parsedAppId", fileNotFoundException)
withContext(dispatcherProvider.main()) { configsLoadedCallback(false) }
}
} else {
fhirEngine.searchCompositionByIdentifier(appId)?.run {
populateConfigurationsMap(context, this, false, appId, configsLoadedCallback)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ data class ApplicationConfiguration(
val remoteSyncPageSize: Int = 100,
val languages: List<String> = listOf("en"),
val useDarkTheme: Boolean = false,
val syncInterval: Long = 30,
val syncInterval: Long = 15,
val syncStrategies: List<String> = listOf(),
val loginConfig: LoginConfig = LoginConfig(),
val deviceToDeviceSync: DeviceToDeviceSyncConfig? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ package org.smartregister.fhircore.engine.di

import android.accounts.AccountManager
import android.content.Context
import androidx.work.WorkManager
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.knowledge.KnowledgeManager
import com.google.android.fhir.sync.Sync
import com.google.android.fhir.workflow.FhirOperator
import dagger.Module
import dagger.Provides
Expand All @@ -35,7 +37,7 @@ import org.hl7.fhir.r4.utils.FHIRPathEngine
import org.smartregister.fhircore.engine.util.helper.TransformSupportServices

@InstallIn(SingletonComponent::class)
@Module(includes = [NetworkModule::class, DispatcherModule::class])
@Module(includes = [NetworkModule::class, DispatcherModule::class, WorkManagerModule::class])
class CoreModule {

@Singleton
Expand Down Expand Up @@ -67,4 +69,6 @@ class CoreModule {
@Provides
fun provideFhirOperator(fhirEngine: FhirEngine): FhirOperator =
FhirOperator(fhirContext = FhirContext.forCached(FhirVersionEnum.R4), fhirEngine = fhirEngine)

@Singleton @Provides fun provideSync(workManager: WorkManager) = Sync(workManager)
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import com.google.android.fhir.sync.FhirSyncWorker
import com.google.android.fhir.sync.UploadConfiguration
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.smartregister.fhircore.engine.data.local.DefaultRepository

@HiltWorker
class AppSyncWorker
Expand All @@ -36,7 +35,7 @@ constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
val syncListenerManager: SyncListenerManager,
val defaultRepository: DefaultRepository,
val openSrpFhirEngine: FhirEngine,
val appTimeStampContext: AppTimeStampContext,
) : FhirSyncWorker(appContext, workerParams) {

Expand All @@ -45,13 +44,12 @@ constructor(
override fun getDownloadWorkManager(): DownloadWorkManager =
OpenSrpDownloadManager(
syncParams = syncListenerManager.loadSyncParams(),
context = appTimeStampContext,
defaultRepository
context = appTimeStampContext
)

/** Disable ETag for upload */
override fun getUploadConfiguration(): UploadConfiguration =
UploadConfiguration(useETagForUpload = false)

override fun getFhirEngine(): FhirEngine = defaultRepository.fhirEngine
override fun getFhirEngine(): FhirEngine = openSrpFhirEngine
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,21 @@ import com.google.android.fhir.sync.download.ResourceParamsBasedDownloadWorkMana
import com.google.android.fhir.sync.download.ResourceSearchParams
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.util.extension.updateLastUpdated

/** Created by Ephraim Kigamba - nek.eam@gmail.com on 13-06-2023. */
class OpenSrpDownloadManager(
syncParams: ResourceSearchParams,
val context: ResourceParamsBasedDownloadWorkManager.TimestampContext,
val defaultRepository: DefaultRepository
) : DownloadWorkManager {

val downloadWorkManager = ResourceParamsBasedDownloadWorkManager(syncParams, context)
private val downloadWorkManager = ResourceParamsBasedDownloadWorkManager(syncParams, context)

override suspend fun getNextRequest(): Request? = downloadWorkManager.getNextRequest()

override suspend fun getSummaryRequestUrls(): Map<ResourceType, String> =
downloadWorkManager.getSummaryRequestUrls()

override suspend fun processResponse(response: Resource): Collection<Resource> {
return downloadWorkManager.processResponse(response).apply {
forEach { it.updateLastUpdated() }
}
return downloadWorkManager.processResponse(response).onEach { it.updateLastUpdated() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,21 @@ import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.shareIn
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.util.DispatcherProvider
import timber.log.Timber

/**
* This class is used to trigger one time and periodic syncs. A new instance of this class is
* created each time because a new instance of [ResourceParamsBasedDownloadWorkManager] is needed
* everytime sync is triggered. This class should not be provided as a singleton. The
* everytime sync is triggered; this class SHOULD NOT be provided as a singleton. The
* [SyncJobStatus] events are sent to the registered [OnSyncListener] maintained by the
* [SyncListenerManager]
*/
Expand All @@ -55,62 +55,43 @@ constructor(
val fhirEngine: FhirEngine,
val syncListenerManager: SyncListenerManager,
val dispatcherProvider: DispatcherProvider,
val sync: Sync,
@ApplicationContext val context: Context,
) {

fun runSync(syncSharedFlow: MutableSharedFlow<SyncJobStatus>) {
val coroutineScope = CoroutineScope(dispatcherProvider.main())
/**
* Run one time sync. The [SyncJobStatus] will be broadcast to all the registered [OnSyncListener]
* 's
*/
suspend fun runOneTimeSync() = coroutineScope {
Timber.i("Running one time sync...")
coroutineScope.launch {
syncSharedFlow
.onEach {
syncListenerManager.onSyncListeners.forEach { onSyncListener ->
onSyncListener.onSync(it)
}
}
.handleErrors()
.launchIn(this)
}

coroutineScope.launch(dispatcherProvider.main()) {
Sync.oneTimeSync<AppSyncWorker>(context).collect { syncSharedFlow.emit(it) }
}
sync.oneTimeSync<AppSyncWorker>().handleSyncJobStatus(this)
}

private fun <T> Flow<T>.handleErrors(): Flow<T> = catch { throwable -> Timber.e(throwable) }

/**
* Schedule periodic sync periodically as defined in the application config interval. The
* [SyncJobStatus] will be broadcast to the listeners
* [SyncJobStatus] will be broadcast to all the registered [OnSyncListener]'s
*/
@OptIn(ExperimentalCoroutinesApi::class)
fun schedulePeriodicSync(periodicSyncSharedFlow: MutableSharedFlow<SyncJobStatus>) {
suspend fun schedulePeriodicSync(interval: Long = 15) = coroutineScope {
Timber.i("Scheduling periodic sync...")
sync
.periodicSync<AppSyncWorker>(
PeriodicSyncConfiguration(
syncConstraints =
Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(),
repeat = RepeatInterval(interval = interval, timeUnit = TimeUnit.MINUTES)
)
)
.handleSyncJobStatus(this)
}

// Launch in main to observer UI updates that should ONLY happen on main thread
val coroutineScope = CoroutineScope(dispatcherProvider.main())
coroutineScope.launch {
periodicSyncSharedFlow
.onEach {
syncListenerManager.onSyncListeners.forEach { onSyncListener ->
onSyncListener.onSync(it)
}
}
.handleErrors()
.launchIn(this)

// Switch to io thread when triggering periodic sync
withContext(dispatcherProvider.io()) {
Sync.periodicSync<AppSyncWorker>(
context,
PeriodicSyncConfiguration(
syncConstraints =
Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(),
repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES)
)
)
.collect { periodicSyncSharedFlow.emit(it) }
private fun Flow<SyncJobStatus>.handleSyncJobStatus(coroutineScope: CoroutineScope) {
this.onEach {
syncListenerManager.onSyncListeners.forEach { onSyncListener -> onSyncListener.onSync(it) }
}
}
.catch { throwable -> Timber.e("Encountered an error during sync:", throwable) }
.shareIn(coroutineScope, SharingStarted.Eagerly, 1)
.launchIn(coroutineScope)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import io.mockk.verify
import org.hl7.fhir.r4.model.ResourceType
import org.junit.Assert
import org.junit.Test
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.robolectric.RobolectricTest

class AppSyncWorkerTest : RobolectricTest() {
Expand All @@ -37,17 +36,15 @@ class AppSyncWorkerTest : RobolectricTest() {
val syncParams = emptyMap<ResourceType, ParamMap>()
val syncListenerManager = mockk<SyncListenerManager>()
val fhirEngine = mockk<FhirEngine>()
val defaultRepository = mockk<DefaultRepository>()
val taskExecutor = mockk<TaskExecutor>()
val timeContext = mockk<AppTimeStampContext>()

every { defaultRepository.fhirEngine } returns fhirEngine
every { taskExecutor.backgroundExecutor } returns mockk()
every { workerParams.taskExecutor } returns taskExecutor
every { syncListenerManager.loadSyncParams() } returns syncParams

val appSyncWorker =
AppSyncWorker(mockk(), workerParams, syncListenerManager, defaultRepository, timeContext)
AppSyncWorker(mockk(), workerParams, syncListenerManager, fhirEngine, timeContext)

appSyncWorker.getDownloadWorkManager()
verify { syncListenerManager.loadSyncParams() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,13 @@ import org.smartregister.fhircore.engine.util.extension.isIn
class SyncBroadcasterTest : RobolectricTest() {

@get:Rule(order = 0) val hiltAndroidRule = HiltAndroidRule(this)

@get:Rule(order = 1) val coroutineTestRule = CoroutineTestRule()

@Inject lateinit var sharedPreferencesHelper: SharedPreferencesHelper

@Inject lateinit var configService: ConfigService

private val configurationRegistry: ConfigurationRegistry = Faker.buildTestConfigurationRegistry()

private val fhirEngine = mockk<FhirEngine>()

private lateinit var syncListenerManager: SyncListenerManager

private lateinit var syncBroadcaster: SyncBroadcaster

private val context = ApplicationProvider.getApplicationContext<HiltTestApplication>()

@Before
Expand All @@ -81,6 +73,7 @@ class SyncBroadcasterTest : RobolectricTest() {
fhirEngine = fhirEngine,
dispatcherProvider = coroutineTestRule.testDispatcherProvider,
syncListenerManager = syncListenerManager,
sync = mockk(relaxed = true),
context = context
)
)
Expand Down
2 changes: 1 addition & 1 deletion android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ desugar-jdk-libs = "1.1.5"
easy-rules-jexl = "4.1.0"
espresso-core = "3.5.0"
fhir-common-utils = "0.0.2-SNAPSHOT"
fhir-engine = "0.1.0-beta03-preview9-SNAPSHOT"
fhir-engine = "0.1.0-beta03-preview9.1-SNAPSHOT"
foundation = "1.3.1"
fragment-ktx = "1.5.5"
fragment-testing = "1.5.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,16 @@ constructor(
}

fun loadConfigurations(context: Context) {
viewModelScope.launch(dispatcherProvider.io()) {
appId.value?.let { thisAppId ->
configurationRegistry.loadConfigurations(thisAppId, context) {
appId.value?.let { thisAppId ->
viewModelScope.launch(dispatcherProvider.io()) {
configurationRegistry.loadConfigurations(thisAppId, context) { loadConfigSuccessful ->
showProgressBar.postValue(false)
sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, thisAppId)
context.getActivity()?.launchActivityWithNoBackStackHistory<LoginActivity>()
if (loadConfigSuccessful) {
sharedPreferencesHelper.write(SharedPreferenceKey.APP_ID.name, thisAppId)
context.getActivity()?.launchActivityWithNoBackStackHistory<LoginActivity>()
} else {
_error.postValue(context.getString(R.string.application_not_supported, appId.value))
}
}
}
}
Expand Down
Loading