Skip to content
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 @@ -12,20 +12,22 @@ import com.simprints.feature.troubleshooting.AutoResettingClickCounter
import com.simprints.infra.config.store.ConfigRepository
import com.simprints.infra.config.store.models.SettingsPasswordConfig
import com.simprints.infra.config.store.models.canSyncDataToSimprints
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SYNC
import com.simprints.infra.logging.Simber
import com.simprints.infra.recent.user.activity.RecentUserActivityManager
import com.simprints.infra.recent.user.activity.domain.RecentUserActivity
import com.simprints.infra.sync.OneTime
import com.simprints.infra.sync.SyncOrchestrator
import com.simprints.infra.sync.usecase.ObserveSyncableCountsUseCase
import com.simprints.infra.sync.devicestate.DeviceStateDataTracker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
internal class AboutViewModel @Inject constructor(
private val configRepository: ConfigRepository,
private val observeSyncableCounts: ObserveSyncableCountsUseCase,
private val deviceStateDataTracker: DeviceStateDataTracker,
private val syncOrchestrator: SyncOrchestrator,
private val recentUserActivityManager: RecentUserActivityManager,
) : ViewModel() {
Expand Down Expand Up @@ -79,7 +81,14 @@ internal class AboutViewModel @Inject constructor(
}
}

private suspend fun hasEventsToUpload(): Boolean = observeSyncableCounts().first().eventsToUpload > 0
private suspend fun hasEventsToUpload(): Boolean = try {
deviceStateDataTracker.hasPendingEvents()
} catch (cancellation: CancellationException) {
throw cancellation
} catch (t: Throwable) {
Simber.i("Could not check pending events before logout", t, tag = SYNC)
true
}

private suspend fun canSyncDataToSimprints(): Boolean = configRepository.getProjectConfiguration().canSyncDataToSimprints()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.simprints.infra.config.store.models.isModuleSelectionAvailable
import com.simprints.infra.recent.user.activity.RecentUserActivityManager
import com.simprints.infra.sync.OneTime
import com.simprints.infra.sync.SyncOrchestrator
import com.simprints.infra.sync.devicestate.DeviceStateDataTracker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
Expand Down Expand Up @@ -44,6 +45,7 @@ internal class SyncInfoViewModel @Inject constructor(
private val timeHelper: TimeHelper,
observeSyncInfo: ObserveSyncInfoUseCase,
private val syncOrchestrator: SyncOrchestrator,
private val deviceStateDataTracker: DeviceStateDataTracker,
@param:DispatcherIO private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
var isPreLogoutUpSync = false
Expand Down Expand Up @@ -134,6 +136,8 @@ internal class SyncInfoViewModel @Inject constructor(
}

fun forceEventSync(canEmitSyncButtonClick: Boolean = true) {
// Sync is the user's "give me fresh data" gesture, so it also recovers any failed device state source
deviceStateDataTracker.refresh()
viewModelScope.launch {
if (canEmitSyncButtonClick) {
val isEventSyncing = eventSyncStateFlow.firstOrNull()?.isSyncInProgress() == true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ import com.simprints.feature.dashboard.settings.syncinfo.usecase.internal.GetSyn
import com.simprints.infra.authstore.AuthStore
import com.simprints.infra.config.store.models.isModuleSelectionAvailable
import com.simprints.infra.config.store.models.isSampleUploadEnabledInProject
import com.simprints.infra.eventsync.status.models.DownSyncCounts
import com.simprints.infra.eventsync.sync.down.EventDownSyncPeriodicCountUseCase
import com.simprints.infra.network.ConnectivityTracker
import com.simprints.infra.sync.SyncOrchestrator
import com.simprints.infra.sync.usecase.ObserveSyncableCountsUseCase
import com.simprints.infra.sync.devicestate.DeviceDataState
import com.simprints.infra.sync.devicestate.DeviceStateDataTracker
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
Expand All @@ -32,7 +35,8 @@ internal class ObserveSyncInfoUseCase @Inject constructor(
private val getSyncInfoSectionImages: GetSyncInfoSectionImagesUseCase,
private val getSyncInfoSectionRecords: GetSyncInfoSectionRecordsUseCase,
private val observeConfigurationFlow: ObserveConfigurationChangesUseCase,
private val observeSyncableCounts: ObserveSyncableCountsUseCase,
private val deviceStateDataTracker: DeviceStateDataTracker,
private val eventDownSyncCount: EventDownSyncPeriodicCountUseCase,
private val syncOrchestrator: SyncOrchestrator,
@param:DispatcherBG private val dispatcher: CoroutineDispatcher,
) {
Expand All @@ -43,17 +47,22 @@ internal class ObserveSyncInfoUseCase @Inject constructor(
ticker.observeTicks(1.minutes),
) { isOnline, _, _ -> isOnline }

private fun combinedDataState(): Flow<Pair<DeviceDataState, DownSyncCounts>> = combine(
deviceStateDataTracker.observeDeviceDataState(),
eventDownSyncCount(),
) { deviceDataState, downSyncCounts -> deviceDataState to downSyncCounts }

operator fun invoke(isPreLogoutUpSync: Boolean = false): Flow<SyncInfo> = combine(
combinedRefreshSignals(),
authStore.observeSignedInProjectId(),
syncOrchestrator.observeSyncState(),
observeSyncableCounts(),
combinedDataState(),
observeConfigurationFlow(),
) {
isOnline,
projectId,
(eventSyncState, imageSyncStatus),
syncableCounts,
(deviceDataState, downSyncCounts),
(isRefreshing, isProjectRunning, moduleCounts, projectConfig),
->
val isReLoginRequired = eventSyncState.isSyncFailedBecauseReloginRequired()
Expand All @@ -67,7 +76,8 @@ internal class ObserveSyncInfoUseCase @Inject constructor(
projectId,
eventSyncState,
imageSyncStatus,
syncableCounts,
deviceDataState,
downSyncCounts,
isProjectRunning,
moduleCounts,
projectConfig,
Expand All @@ -76,7 +86,7 @@ internal class ObserveSyncInfoUseCase @Inject constructor(
isOnline,
eventSyncState,
imageSyncStatus,
syncableCounts,
deviceDataState,
)
return@combine SyncInfo(
isLoggedIn = projectId.isNotEmpty(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.simprints.feature.dashboard.settings.syncinfo.SyncProgressInfo
import com.simprints.feature.dashboard.settings.syncinfo.SyncProgressInfoPart
import com.simprints.infra.eventsync.status.models.EventSyncState
import com.simprints.infra.sync.ImageSyncStatus
import com.simprints.infra.sync.SyncableCounts
import com.simprints.infra.sync.devicestate.DeviceDataState
import javax.inject.Inject
import kotlin.math.roundToInt

Expand All @@ -18,12 +18,12 @@ internal class GetSyncInfoSectionImagesUseCase @Inject constructor(
isOnline: Boolean,
eventSyncState: EventSyncState,
imageSyncStatus: ImageSyncStatus,
syncableCounts: SyncableCounts,
deviceDataState: DeviceDataState,
): SyncInfoSectionImages {
val imagesToUploadOrNull = if (imageSyncStatus.isSyncing) {
null
} else {
syncableCounts.samplesToUpload // internal term is sample, user-facing (within sync info) term is image
deviceDataState.pendingSamples
}
val progress = getImageSyncProgress(imageSyncStatus)
val imageLastSyncTimestamp = Timestamp(imageSyncStatus.lastUpdateTimeMillis ?: -1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import com.simprints.infra.eventsync.permission.CommCarePermissionChecker
import com.simprints.infra.eventsync.status.models.DownSyncCounts
import com.simprints.infra.eventsync.status.models.EventSyncState
import com.simprints.infra.sync.ImageSyncStatus
import com.simprints.infra.sync.SyncableCounts
import com.simprints.infra.sync.devicestate.DeviceDataState
import javax.inject.Inject
import kotlin.math.roundToInt

Expand All @@ -31,20 +31,21 @@ internal class GetSyncInfoSectionRecordsUseCase @Inject constructor(
projectId: String,
eventSyncState: EventSyncState,
imageSyncStatus: ImageSyncStatus,
syncableCounts: SyncableCounts,
deviceDataState: DeviceDataState,
downSyncCounts: DownSyncCounts,
isProjectRunning: Boolean,
moduleCounts: List<ModuleCount>,
projectConfig: ProjectConfiguration,
): SyncInfoSectionRecords {
val isSyncInProgress = eventSyncState.isSyncInProgress() ||
(isPreLogoutUpSync && imageSyncStatus.isSyncing) // also in progress if combined with image sync

val counterTotalRecords = getRecordsTotal(isSyncInProgress, projectId, syncableCounts)
val counterRecordsToDownload = getRecordsToDownload(isSyncInProgress, isPreLogoutUpSync, projectConfig, syncableCounts)
val counterRecordsToUpload = getRecordsToUpload(isSyncInProgress, syncableCounts)
val counterImagesToUpload = getImagesToUpload(imageSyncStatus, syncableCounts)
val counterTotalRecords = getRecordsTotal(isSyncInProgress, projectId, deviceDataState)
val counterRecordsToDownload = getRecordsToDownload(isSyncInProgress, isPreLogoutUpSync, projectConfig, downSyncCounts)
val counterRecordsToUpload = getRecordsToUpload(isSyncInProgress, deviceDataState)
val counterImagesToUpload = getImagesToUpload(imageSyncStatus, deviceDataState)

val progress = getSyncProgressInfo(isSyncInProgress, isPreLogoutUpSync, eventSyncState, imageSyncStatus, syncableCounts)
val progress = getSyncProgressInfo(isSyncInProgress, isPreLogoutUpSync, eventSyncState, imageSyncStatus, deviceDataState)

val recordSyncVisibleState =
getSyncVisibleState(isSyncInProgress, isOnline, isPreLogoutUpSync, projectConfig, moduleCounts, eventSyncState)
Expand Down Expand Up @@ -82,49 +83,47 @@ internal class GetSyncInfoSectionRecordsUseCase @Inject constructor(
isSyncInProgress: Boolean,
isPreLogoutUpSync: Boolean,
projectConfig: ProjectConfiguration,
syncableCounts: SyncableCounts,
downSyncCounts: DownSyncCounts,
): String = when {
isSyncInProgress || isPreLogoutUpSync -> null
projectConfig.isSimprintsEventDownSyncAllowed() -> with(syncableCounts) {
DownSyncCounts(recordEventsToDownload, isRecordEventsToDownloadLowerBound)
}
projectConfig.isSimprintsEventDownSyncAllowed() -> downSyncCounts
else -> DownSyncCounts(0, isLowerBound = false)
}?.let { "${it.count}${if (it.isLowerBound) "+" else ""}" }.orEmpty()

private fun getRecordsTotal(
isSyncInProgress: Boolean,
projectId: String,
syncableCounts: SyncableCounts,
deviceDataState: DeviceDataState,
): String = if (isSyncInProgress || projectId.isBlank()) {
""
} else {
syncableCounts.totalRecords.toString()
deviceDataState.recordCount?.toString().orEmpty()
Comment thread
meladRaouf marked this conversation as resolved.
}

private fun getRecordsToUpload(
isSyncInProgress: Boolean,
syncableCounts: SyncableCounts,
deviceDataState: DeviceDataState,
): String = if (isSyncInProgress) {
""
} else {
syncableCounts.enrolmentsToUpload.toString()
deviceDataState.pendingEnrolments?.toString().orEmpty()
}

private fun getImagesToUpload(
imageSyncStatus: ImageSyncStatus,
syncableCounts: SyncableCounts,
deviceDataState: DeviceDataState,
): String = if (imageSyncStatus.isSyncing) {
""
} else {
syncableCounts.samplesToUpload.toString()
deviceDataState.pendingSamples?.toString().orEmpty()
}

private fun getSyncProgressInfo(
isSyncInProgress: Boolean,
isPreLogoutUpSync: Boolean,
eventSyncState: EventSyncState,
imageSyncStatus: ImageSyncStatus,
syncableCounts: SyncableCounts,
deviceDataState: DeviceDataState,
): SyncProgressInfo {
if (!isSyncInProgress) {
return SyncProgressInfo()
Expand All @@ -142,7 +141,8 @@ internal class GetSyncInfoSectionRecordsUseCase @Inject constructor(
)
val imageSyncProgressPart = SyncProgressInfoPart(
isPending = eventSyncState.isSyncInProgress() && !imageSyncStatus.isSyncing,
isDone = !eventSyncState.isSyncInProgress() && !imageSyncStatus.isSyncing && syncableCounts.samplesToUpload == 0,
// An unreadable sample count (null) is deliberately not "done".
isDone = !eventSyncState.isSyncInProgress() && !imageSyncStatus.isSyncing && deviceDataState.pendingSamples == 0,
areNumbersVisible = imageSyncStatus.isSyncing && totalImages > 0,
currentNumber = currentImages,
totalNumber = totalImages,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@ import com.simprints.infra.recent.user.activity.RecentUserActivityManager
import com.simprints.infra.recent.user.activity.domain.RecentUserActivity
import com.simprints.infra.sync.OneTime
import com.simprints.infra.sync.SyncOrchestrator
import com.simprints.infra.sync.SyncableCounts
import com.simprints.infra.sync.usecase.ObserveSyncableCountsUseCase
import com.simprints.infra.sync.devicestate.DeviceStateDataTracker
import com.simprints.testtools.common.coroutines.TestCoroutineRule
import com.simprints.testtools.common.livedata.getOrAwaitValue
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
Expand Down Expand Up @@ -56,7 +54,7 @@ class AboutViewModelTest {
)

@MockK
lateinit var observeSyncableCounts: ObserveSyncableCountsUseCase
lateinit var deviceStateDataTracker: DeviceStateDataTracker

@MockK
lateinit var configRepository: ConfigRepository
Expand All @@ -79,7 +77,7 @@ class AboutViewModelTest {
fun `should initialize the live data correctly`() = runTest(testDispatcher) {
val viewModel = AboutViewModel(
configRepository = configRepository,
observeSyncableCounts = observeSyncableCounts,
deviceStateDataTracker = deviceStateDataTracker,
recentUserActivityManager = recentUserActivityManager,
syncOrchestrator = syncOrchestrator,
)
Expand Down Expand Up @@ -196,6 +194,20 @@ class AboutViewModelTest {
}
}

@Test
fun `should read pending events freshly rather than reusing an earlier value`() = runTest(testDispatcher) {
val viewModel = buildAboutViewModel(canSyncDataToSimprints = true, hasEventsToUpload = false)
viewModel.processLogoutRequest()
assertThat(viewModel.logoutDestinationEvent.getOrAwaitValue().peekContent())
.isEqualTo(LogoutDestination.LoginScreen)

coEvery { deviceStateDataTracker.hasPendingEvents() } returns true
viewModel.processLogoutRequest()

assertThat(viewModel.logoutDestinationEvent.getOrAwaitValue().peekContent())
.isEqualTo(LogoutDestination.LogoutDataSyncScreen)
}

private fun buildAboutViewModel(
canSyncDataToSimprints: Boolean,
hasEventsToUpload: Boolean,
Expand All @@ -204,26 +216,13 @@ class AboutViewModelTest {
true -> UpSynchronizationConfiguration.UpSynchronizationKind.ALL
false -> UpSynchronizationConfiguration.UpSynchronizationKind.NONE
}
val countEventsToUpload = when (hasEventsToUpload) {
true -> 1
false -> 0
}
every { observeSyncableCounts.invoke() } returns flowOf(
SyncableCounts(
totalRecords = 0,
recordEventsToDownload = 0,
isRecordEventsToDownloadLowerBound = false,
eventsToUpload = countEventsToUpload,
enrolmentsToUpload = 0,
samplesToUpload = 0,
),
)
coEvery { deviceStateDataTracker.hasPendingEvents() } returns hasEventsToUpload
coEvery { configRepository.getProjectConfiguration() } returns buildProjectConfigurationMock(
upSyncKind,
)
return AboutViewModel(
configRepository = configRepository,
observeSyncableCounts = observeSyncableCounts,
deviceStateDataTracker = deviceStateDataTracker,
recentUserActivityManager = recentUserActivityManager,
syncOrchestrator = syncOrchestrator,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.simprints.infra.sync.ImageSyncStatus
import com.simprints.infra.sync.OneTime
import com.simprints.infra.sync.SyncOrchestrator
import com.simprints.infra.sync.SyncStatus
import com.simprints.infra.sync.devicestate.DeviceStateDataTracker
import com.simprints.testtools.common.coroutines.TestCoroutineRule
import com.simprints.testtools.common.livedata.getOrAwaitValue
import com.simprints.testtools.common.livedata.getOrAwaitValues
Expand Down Expand Up @@ -67,6 +68,9 @@ class SyncInfoViewModelTest {
@MockK
private lateinit var syncOrchestrator: SyncOrchestrator

@MockK
private lateinit var deviceStateDataTracker: DeviceStateDataTracker

@MockK
private lateinit var mockProjectConfiguration: ProjectConfiguration

Expand Down Expand Up @@ -162,6 +166,7 @@ class SyncInfoViewModelTest {
timeHelper = timeHelper,
observeSyncInfo = observeSyncInfo,
syncOrchestrator = syncOrchestrator,
deviceStateDataTracker = deviceStateDataTracker,
ioDispatcher = testCoroutineRule.testCoroutineDispatcher,
)
}
Expand Down Expand Up @@ -390,6 +395,15 @@ class SyncInfoViewModelTest {
verify { syncOrchestrator.execute(OneTime.Events.restart(isDownSyncAllowed = false)) }
}

@Test
fun `should refresh device data state when forcing event sync`() = runTest {
createViewModel()

viewModel.forceEventSync()

verify(exactly = 1) { deviceStateDataTracker.refresh() }
}

@Test
fun `should stop current event sync before starting new one`() = runTest {
createViewModel()
Expand Down
Loading
Loading