From 94c1ea742da5ec5405ca65d9db8ee882f4f9df95 Mon Sep 17 00:00:00 2001 From: Scott Olsen Date: Thu, 19 Dec 2024 18:42:09 -0500 Subject: [PATCH] SyncStatus: Use new mutations repository upload queue flow; renaming. (#2946) This commit uses the newly introduced getUploadQueueFlow() in the MutationRepository to fetch mutations for the sync status UI. In additiion, I have renamed some classes and added some documentation for clarity. In addition, we now handle SubmissionMutations separately from LOI Mutations, allowing us to better inform users. We also no longer check for the authenticated user in the sync view model, since this is done in the mutation repository before the uplaod queue is returned. --- .../ground/repository/MutationRepository.kt | 2 +- .../ground/ui/syncstatus/SyncListItem.kt | 14 +-- ...{MutationDetail.kt => SyncStatusDetail.kt} | 14 ++- .../ui/syncstatus/SyncStatusFragment.kt | 7 +- .../ui/syncstatus/SyncStatusViewModel.kt | 91 +++++++++++-------- 5 files changed, 76 insertions(+), 52 deletions(-) rename ground/src/main/java/com/google/android/ground/ui/syncstatus/{MutationDetail.kt => SyncStatusDetail.kt} (64%) diff --git a/ground/src/main/java/com/google/android/ground/repository/MutationRepository.kt b/ground/src/main/java/com/google/android/ground/repository/MutationRepository.kt index 6dbe60eace..0fa710b767 100644 --- a/ground/src/main/java/com/google/android/ground/repository/MutationRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/MutationRepository.kt @@ -92,7 +92,7 @@ constructor( * Returns a [Flow] which emits the upload queue once and on each change, sorted in chronological * order (FIFO). */ - private fun getUploadQueueFlow(): Flow> = + fun getUploadQueueFlow(): Flow> = localLocationOfInterestStore.getAllMutationsFlow().combine( localSubmissionStore.getAllMutationsFlow() ) { loiMutations, submissionMutations -> diff --git a/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncListItem.kt b/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncListItem.kt index 20096ad930..4883e9d37f 100644 --- a/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncListItem.kt +++ b/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncListItem.kt @@ -50,7 +50,7 @@ import com.google.android.ground.ui.theme.AppTheme import java.util.Date @Composable -fun SyncListItem(modifier: Modifier, detail: MutationDetail) { +fun SyncListItem(modifier: Modifier, detail: SyncStatusDetail) { Column { Row(modifier.fillMaxWidth().padding(top = 8.dp, end = 24.dp, bottom = 8.dp, start = 16.dp)) { Column(modifier.weight(1f)) { @@ -80,8 +80,8 @@ fun SyncListItem(modifier: Modifier, detail: MutationDetail) { fontWeight = FontWeight(400), color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Text(text = detail.loiLabel, style = textStyle) - Text(text = detail.loiSubtitle, style = textStyle) + Text(text = detail.label, style = textStyle) + Text(text = detail.subtitle, style = textStyle) } Column(modifier = modifier.padding(start = 16.dp).align(alignment = CenterVertically)) { Row(verticalAlignment = CenterVertically) { @@ -140,11 +140,11 @@ private fun Mutation.SyncStatus.toIcon(): Int = @Preview(showBackground = true, showSystemUi = true) @ExcludeFromJacocoGeneratedReport fun PreviewSyncListItem( - detail: MutationDetail = - MutationDetail( + detail: SyncStatusDetail = + SyncStatusDetail( user = "Jane Doe", - loiLabel = "Map the farms", - loiSubtitle = "IDX21311", + label = "Map the farms", + subtitle = "IDX21311", mutation = SubmissionMutation( job = Job(id = "123"), diff --git a/ground/src/main/java/com/google/android/ground/ui/syncstatus/MutationDetail.kt b/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusDetail.kt similarity index 64% rename from ground/src/main/java/com/google/android/ground/ui/syncstatus/MutationDetail.kt rename to ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusDetail.kt index 54c2801b8c..f6275d6dda 100644 --- a/ground/src/main/java/com/google/android/ground/ui/syncstatus/MutationDetail.kt +++ b/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusDetail.kt @@ -17,10 +17,16 @@ package com.google.android.ground.ui.syncstatus import com.google.android.ground.model.mutation.Mutation -/** A tiny helper class for bundling mutation history display data. */ -data class MutationDetail( +/** + * Defines the set of data needed to display the human-readable status of a queued local [Mutation]. + */ +data class SyncStatusDetail( + /** The username of the user who made this change. */ val user: String, + /** The underlying [Mutation]. */ val mutation: Mutation, - val loiLabel: String, - val loiSubtitle: String, + /** A human-readable label summarizing what data changed. */ + val label: String, + /** A human-readable label providing further information on the change. */ + val subtitle: String, ) diff --git a/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusFragment.kt b/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusFragment.kt index d6ee215b35..ca56a0279c 100644 --- a/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusFragment.kt @@ -34,7 +34,10 @@ import com.google.android.ground.ui.common.AbstractFragment import com.google.android.ground.ui.theme.AppTheme import dagger.hilt.android.AndroidEntryPoint -/** Fragment containing a list of mutations and their respective upload statuses. */ +/** + * This fragment summarizes the synchronization statuses of local changes that are being uploaded to + * a remote server. + */ @AndroidEntryPoint class SyncStatusFragment : AbstractFragment() { @@ -61,7 +64,7 @@ class SyncStatusFragment : AbstractFragment() { @Composable private fun ShowSyncItems() { - val list by viewModel.mutations.observeAsState() + val list by viewModel.uploadStatus.observeAsState() list?.let { LazyColumn(Modifier.fillMaxSize().testTag("sync list")) { items(it) { diff --git a/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusViewModel.kt index 4faea5bca2..33dcf63258 100644 --- a/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusViewModel.kt @@ -17,8 +17,9 @@ package com.google.android.ground.ui.syncstatus import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData -import com.google.android.ground.model.locationofinterest.LocationOfInterest +import com.google.android.ground.model.mutation.LocationOfInterestMutation import com.google.android.ground.model.mutation.Mutation +import com.google.android.ground.model.mutation.SubmissionMutation import com.google.android.ground.repository.LocationOfInterestRepository import com.google.android.ground.repository.MutationRepository import com.google.android.ground.repository.SurveyRepository @@ -26,58 +27,72 @@ import com.google.android.ground.repository.UserRepository import com.google.android.ground.ui.common.AbstractViewModel import com.google.android.ground.ui.common.LocationOfInterestHelper import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import timber.log.Timber /** - * View model for the offline area manager fragment. Handles the current list of downloaded areas. + * Provides data for the [SyncStatusFragment] UI. + * + * This model retrieves the current upload queue from the mutations repository and converts them to + * [SyncStatusDetail] to prepare the data for display. It assumes that any filtering or + * transformation of the underlying mutation queue is done before fetching at the repository level. */ class SyncStatusViewModel @Inject internal constructor( - private val mutationRepository: MutationRepository, - surveyRepository: SurveyRepository, + mutationRepository: MutationRepository, private val locationOfInterestRepository: LocationOfInterestRepository, private val userRepository: UserRepository, private val locationOfInterestHelper: LocationOfInterestHelper, + private val surveyRepository: SurveyRepository, ) : AbstractViewModel() { - /** [Flow] of latest mutations for the active [Survey]. */ - @OptIn(ExperimentalCoroutinesApi::class) - private val mutationsFlow: Flow> = - surveyRepository.activeSurveyFlow.filterNotNull().flatMapLatest { - mutationRepository.getSurveyMutationsFlow(it) - } - /** - * List of current local [Mutation]s executed by the user, with their corresponding - * [LocationOfInterest]. + * A complete list of [SyncStatusDetail] indicating the current status of local changes being + * synced to remote servers. */ - internal val mutations: LiveData> = - mutationsFlow.map { loadLocationsOfInterestAndPair(it) }.asLiveData() - - private suspend fun loadLocationsOfInterestAndPair( - mutations: List - ): List = mutations.mapNotNull { toMutationDetail(it) } + internal val uploadStatus: LiveData> = + mutationRepository + .getUploadQueueFlow() + .map { uploads -> + val result: MutableList = mutableListOf() + for (upload in uploads) { + val details = upload.mutations().mapNotNull { toSyncStatusDetail(it) } + result.addAll(details) + } + result + } + .asLiveData() - private suspend fun toMutationDetail(mutation: Mutation): MutationDetail? { - val loi = - locationOfInterestRepository.getOfflineLoi(mutation.surveyId, mutation.locationOfInterestId) - if (loi == null) { - // If LOI is null, return null to avoid proceeding - Timber.e("LOI not found for mutation $mutation") - return null + private suspend fun toSyncStatusDetail(mutation: Mutation): SyncStatusDetail? = + when (mutation) { + is LocationOfInterestMutation -> { + val loi = + locationOfInterestRepository.getOfflineLoi( + mutation.surveyId, + mutation.locationOfInterestId, + ) + if (loi == null) { + Timber.e("LOI not found for mutation $mutation") + null + } else { + val user = userRepository.getUser(mutation.userId) + SyncStatusDetail( + user = user.displayName, + mutation = mutation, + label = locationOfInterestHelper.getJobName(loi) ?: "", + subtitle = locationOfInterestHelper.getDisplayLoiName(loi), + ) + } + } + is SubmissionMutation -> { + val user = userRepository.getUser(mutation.userId) + SyncStatusDetail( + user = user.displayName, + mutation = mutation, + label = mutation.job.name ?: "", + subtitle = surveyRepository.getOfflineSurvey(mutation.surveyId)?.title ?: "", + ) + } } - val user = userRepository.getAuthenticatedUser() - return MutationDetail( - user = user.displayName, - mutation = mutation, - loiLabel = locationOfInterestHelper.getJobName(loi) ?: "", - loiSubtitle = locationOfInterestHelper.getDisplayLoiName(loi), - ) - } }