Skip to content

Commit

Permalink
Migrate to Current Sync Job status (#3136)
Browse files Browse the repository at this point in the history
* Migrate to Current Sync Job status

* Code clean up + Fix Build 💚

* Dependabot updates: #3147

* Fix failing Dokka build
  • Loading branch information
ndegwamartin authored Mar 19, 2024
1 parent 73a67d3 commit 25b741a
Show file tree
Hide file tree
Showing 14 changed files with 129 additions and 188 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 17

- name: Add empty local.properties
run: touch local.properties
working-directory: android

- name: Grant execute permission for gradlew
run: chmod +x gradlew
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@

package org.smartregister.fhircore.engine.sync

import com.google.android.fhir.sync.SyncJobStatus
import com.google.android.fhir.sync.CurrentSyncJobStatus

/**
* An interface the exposes a callback method [onSync] which accepts an application level FHIR
* [SyncJobStatus].
* [CurrentSyncJobStatus].
*/
interface OnSyncListener {
/** Callback method invoked to handle sync [SyncJobStatus] */
fun onSync(syncJobStatus: SyncJobStatus)
/** Callback method invoked to handle sync [CurrentSyncJobStatus] */
fun onSync(syncJobStatus: CurrentSyncJobStatus)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@
package org.smartregister.fhircore.engine.sync

import android.content.Context
import androidx.lifecycle.asFlow
import androidx.work.Constraints
import androidx.work.NetworkType
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.hasKeyWithValueOfType
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.sync.CurrentSyncJobStatus
import com.google.android.fhir.sync.PeriodicSyncConfiguration
import com.google.android.fhir.sync.PeriodicSyncJobStatus
import com.google.android.fhir.sync.RepeatInterval
import com.google.android.fhir.sync.Sync
import com.google.android.fhir.sync.SyncJobStatus
Expand All @@ -35,12 +33,10 @@ 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.SharingStarted
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
Expand Down Expand Up @@ -70,9 +66,7 @@ constructor(
*/
suspend fun runOneTimeSync() = coroutineScope {
Timber.i("Running one time sync...")
Sync.oneTimeSync<AppSyncWorker>(context)
val uniqueWorkName = "${AppSyncWorker::class.java.name}-oneTimeSync"
handleSyncJobStatus(uniqueWorkName, this)
Sync.oneTimeSync<AppSyncWorker>(context).handleOneTimeSyncJobStatus(this)
}

/**
Expand All @@ -83,51 +77,37 @@ constructor(
suspend fun schedulePeriodicSync(interval: Long = 15) = coroutineScope {
Timber.i("Scheduling periodic sync...")
Sync.periodicSync<AppSyncWorker>(
context = context,
periodicSyncConfiguration =
PeriodicSyncConfiguration(
syncConstraints =
Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(),
repeat = RepeatInterval(interval = interval, timeUnit = TimeUnit.MINUTES),
),
)
val uniqueWorkName = "${AppSyncWorker::class.java.name}-periodicSync"
handleSyncJobStatus(uniqueWorkName, this)
context = context,
periodicSyncConfiguration =
PeriodicSyncConfiguration(
syncConstraints =
Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(),
repeat = RepeatInterval(interval = interval, timeUnit = TimeUnit.MINUTES),
),
)
.handlePeriodicSyncJobStatus(this)
}

// TODO This serves as a workaround to fix issue with getting SyncJobStatus.Finished
// TODO Refactor once https://github.com/google/android-fhir/pull/2142 is merged
private fun handleSyncJobStatus(uniqueWorkName: String, coroutineScope: CoroutineScope) {
WorkManager.getInstance(context)
.getWorkInfosForUniqueWorkLiveData(uniqueWorkName)
.asFlow()
.flatMapConcat { it.asFlow() }
.mapNotNull { it }
.onEach { workInfo ->
// PeriodSync doesn't return state. It's enqueued instead. Finish the sync as workaround.
if (workInfo.state == WorkInfo.State.ENQUEUED) {
syncListenerManager.onSyncListeners.forEach { onSyncListener ->
onSyncListener.onSync(SyncJobStatus.Succeeded())
}
} else {
val data =
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
workInfo.outputData
} else workInfo.progress
data
.takeIf { it.keyValueMap.isNotEmpty() && it.hasKeyWithValueOfType<String>("StateType") }
?.let {
val state = it.getString("StateType")!!
val stateData = it.getString("State")
val syncJobStatus =
Sync.gson.fromJson(stateData, Class.forName(state)) as SyncJobStatus
syncListenerManager.onSyncListeners.forEach { onSyncListener ->
onSyncListener.onSync(syncJobStatus)
}
}
private fun Flow<PeriodicSyncJobStatus>.handlePeriodicSyncJobStatus(
coroutineScope: CoroutineScope,
) {
this.onEach {
syncListenerManager.onSyncListeners.forEach { onSyncListener ->
onSyncListener.onSync(it.currentSyncJobStatus)
}
}
.catch { throwable -> Timber.e("Encountered an error during sync:", throwable) }
.catch { throwable -> Timber.e("Encountered an error during periodic sync:", throwable) }
.shareIn(coroutineScope, SharingStarted.Eagerly, 1)
.launchIn(coroutineScope)
}

private fun Flow<CurrentSyncJobStatus>.handleOneTimeSyncJobStatus(
coroutineScope: CoroutineScope,
) {
this.onEach {
syncListenerManager.onSyncListeners.forEach { onSyncListener -> onSyncListener.onSync(it) }
}
.catch { throwable -> Timber.e("Encountered an error during one time sync:", throwable) }
.shareIn(coroutineScope, SharingStarted.Eagerly, 1)
.launchIn(coroutineScope)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
package org.smartregister.fhircore.engine.util.test

import androidx.appcompat.app.AppCompatActivity
import com.google.android.fhir.sync.SyncJobStatus
import com.google.android.fhir.sync.CurrentSyncJobStatus
import dagger.hilt.android.AndroidEntryPoint
import org.smartregister.fhircore.engine.sync.OnSyncListener
import org.smartregister.fhircore.engine.util.annotation.ExcludeFromJacocoGeneratedReport

@ExcludeFromJacocoGeneratedReport
@AndroidEntryPoint
class HiltActivityForTest : AppCompatActivity(), OnSyncListener {
override fun onSync(syncJobStatus: SyncJobStatus) {
override fun onSync(syncJobStatus: CurrentSyncJobStatus) {
// DO nothing. This activity implements OnSyncListener for testing purposes
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.NavHostFragment
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.sync.SyncJobStatus
import com.google.android.fhir.sync.CurrentSyncJobStatus
import dagger.hilt.android.AndroidEntryPoint
import io.sentry.android.navigation.SentryNavigationListener
import javax.inject.Inject
Expand Down Expand Up @@ -182,10 +182,19 @@ open class AppMainActivity : BaseMultiLanguageActivity(), QuestionnaireHandler,
}
}

override fun onSync(syncJobStatus: SyncJobStatus) {
override fun onSync(syncJobStatus: CurrentSyncJobStatus) {
when (syncJobStatus) {
is SyncJobStatus.Succeeded,
is SyncJobStatus.Failed, -> {
is CurrentSyncJobStatus.Succeeded -> {
appMainViewModel.run {
onEvent(
AppMainEvent.UpdateSyncState(
state = syncJobStatus,
lastSyncTime = formatLastSyncTimestamp(syncJobStatus.timestamp),
),
)
}
}
is CurrentSyncJobStatus.Failed -> {
appMainViewModel.run {
onEvent(
AppMainEvent.UpdateSyncState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package org.smartregister.fhircore.quest.ui.main

import android.content.Context
import androidx.navigation.NavController
import com.google.android.fhir.sync.SyncJobStatus
import com.google.android.fhir.sync.CurrentSyncJobStatus
import org.smartregister.fhircore.engine.configuration.navigation.NavigationMenuConfig
import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig
import org.smartregister.fhircore.engine.domain.model.Language
Expand All @@ -33,7 +33,8 @@ sealed class AppMainEvent {
val registersList: List<NavigationMenuConfig>?,
) : AppMainEvent()

data class UpdateSyncState(val state: SyncJobStatus, val lastSyncTime: String?) : AppMainEvent()
data class UpdateSyncState(val state: CurrentSyncJobStatus, val lastSyncTime: String?) :
AppMainEvent()

data class TriggerWorkflow(val navController: NavController, val navMenu: NavigationMenuConfig) :
AppMainEvent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.google.android.fhir.sync.SyncJobStatus
import com.google.android.fhir.sync.CurrentSyncJobStatus
import dagger.hilt.android.lifecycle.HiltViewModel
import java.text.SimpleDateFormat
import java.time.OffsetDateTime
Expand Down Expand Up @@ -182,7 +182,7 @@ constructor(
}
is AppMainEvent.OpenRegistersBottomSheet -> displayRegisterBottomSheet(event)
is AppMainEvent.UpdateSyncState -> {
if (event.state is SyncJobStatus.Succeeded) {
if (event.state is CurrentSyncJobStatus.Succeeded) {
sharedPreferencesHelper.write(
SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name,
formatLastSyncTimestamp(event.state.timestamp),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.android.fhir.sync.CurrentSyncJobStatus
import com.google.android.fhir.sync.SyncJobStatus
import com.google.android.fhir.sync.SyncOperation
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -69,8 +70,6 @@ import org.smartregister.fhircore.quest.ui.shared.models.QuestionnaireSubmission
import org.smartregister.fhircore.quest.util.extensions.handleClickEvent
import org.smartregister.fhircore.quest.util.extensions.hookSnackBar
import org.smartregister.fhircore.quest.util.extensions.rememberLifecycleEvent
import retrofit2.HttpException
import timber.log.Timber

@ExperimentalMaterialApi
@AndroidEntryPoint
Expand Down Expand Up @@ -189,17 +188,23 @@ class RegisterFragment : Fragment(), OnSyncListener {
registerViewModel.searchText.value = "" // Clear the search term
}

override fun onSync(syncJobStatus: SyncJobStatus) {
override fun onSync(syncJobStatus: CurrentSyncJobStatus) {
when (syncJobStatus) {
is SyncJobStatus.Started ->
lifecycleScope.launch {
registerViewModel.emitSnackBarState(
SnackBarMessageConfig(message = getString(R.string.syncing)),
is CurrentSyncJobStatus.Running ->
if (syncJobStatus.inProgressSyncJob is SyncJobStatus.Started) {
lifecycleScope.launch {
registerViewModel.emitSnackBarState(
SnackBarMessageConfig(message = getString(R.string.syncing)),
)
}
} else {
emitPercentageProgress(
syncJobStatus.inProgressSyncJob as SyncJobStatus.InProgress,
(syncJobStatus.inProgressSyncJob as SyncJobStatus.InProgress).syncOperation ==
SyncOperation.UPLOAD,
)
}
is SyncJobStatus.InProgress ->
emitPercentageProgress(syncJobStatus, syncJobStatus.syncOperation == SyncOperation.UPLOAD)
is SyncJobStatus.Succeeded -> {
is CurrentSyncJobStatus.Succeeded -> {
refreshRegisterData()
lifecycleScope.launch {
registerViewModel.emitSnackBarState(
Expand All @@ -211,28 +216,14 @@ class RegisterFragment : Fragment(), OnSyncListener {
)
}
}
is SyncJobStatus.Failed -> {
is CurrentSyncJobStatus.Failed -> {
refreshRegisterData()
syncJobStatus.toString()
// Show error message in snackBar message
val hasAuthError =
try {
Timber.e(syncJobStatus.exceptions.joinToString { it.exception.message ?: "" })
syncJobStatus.exceptions.any {
it.exception is HttpException && (it.exception as HttpException).code() == 401
}
} catch (nullPointerException: NullPointerException) {
false
}

lifecycleScope.launch {
registerViewModel.emitSnackBarState(
SnackBarMessageConfig(
message =
getString(
if (hasAuthError) {
R.string.sync_unauthorised
} else R.string.sync_completed_with_errors,
),
message = getString(R.string.sync_completed_with_errors),
duration = SnackbarDuration.Long,
actionLabel = getString(R.string.ok).uppercase(),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import com.google.android.fhir.sync.CurrentSyncJobStatus
import com.google.android.fhir.sync.SyncJobStatus
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
Expand All @@ -48,8 +49,6 @@ import org.smartregister.fhircore.engine.ui.theme.AppTheme
import org.smartregister.fhircore.quest.ui.main.AppMainViewModel
import org.smartregister.fhircore.quest.ui.shared.components.SnackBarMessage
import org.smartregister.fhircore.quest.util.extensions.hookSnackBar
import retrofit2.HttpException
import timber.log.Timber

@AndroidEntryPoint
class UserSettingFragment : Fragment(), OnSyncListener {
Expand Down Expand Up @@ -124,15 +123,17 @@ class UserSettingFragment : Fragment(), OnSyncListener {
syncListenerManager.registerSyncListener(this, lifecycle)
}

override fun onSync(syncJobStatus: SyncJobStatus) {
override fun onSync(syncJobStatus: CurrentSyncJobStatus) {
when (syncJobStatus) {
is SyncJobStatus.Started ->
lifecycleScope.launch {
userSettingViewModel.emitSnackBarState(
SnackBarMessageConfig(message = getString(R.string.syncing)),
)
is CurrentSyncJobStatus.Running ->
if (syncJobStatus.inProgressSyncJob is SyncJobStatus.Started) {
lifecycleScope.launch {
userSettingViewModel.emitSnackBarState(
SnackBarMessageConfig(message = getString(R.string.syncing)),
)
}
}
is SyncJobStatus.Succeeded -> {
is CurrentSyncJobStatus.Succeeded -> {
lifecycleScope.launch {
userSettingViewModel.emitSnackBarState(
SnackBarMessageConfig(
Expand All @@ -143,25 +144,13 @@ class UserSettingFragment : Fragment(), OnSyncListener {
)
}
}
is SyncJobStatus.Failed -> {
val hasAuthError =
try {
Timber.e(syncJobStatus.exceptions.joinToString { it.exception.message ?: "" })
syncJobStatus.exceptions.any {
it.exception is HttpException && (it.exception as HttpException).code() == 401
}
} catch (nullPointerException: NullPointerException) {
false
}

is CurrentSyncJobStatus.Failed -> {
lifecycleScope.launch {
userSettingViewModel.emitSnackBarState(
SnackBarMessageConfig(
message =
getString(
if (hasAuthError) {
R.string.sync_unauthorised
} else R.string.sync_completed_with_errors,
R.string.sync_completed_with_errors,
),
duration = SnackbarDuration.Long,
actionLabel = getString(R.string.ok).uppercase(),
Expand Down
Loading

0 comments on commit 25b741a

Please sign in to comment.