Skip to content

Commit

Permalink
SyncStatus: Use new mutations repository upload queue flow; renaming. (
Browse files Browse the repository at this point in the history
…#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.
  • Loading branch information
scolsen authored Dec 19, 2024
1 parent f819674 commit 94c1ea7
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<UploadQueueEntry>> =
fun getUploadQueueFlow(): Flow<List<UploadQueueEntry>> =
localLocationOfInterestStore.getAllMutationsFlow().combine(
localSubmissionStore.getAllMutationsFlow()
) { loiMutations, submissionMutations ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,67 +17,82 @@ 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
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<List<Mutation>> =
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<List<MutationDetail>> =
mutationsFlow.map { loadLocationsOfInterestAndPair(it) }.asLiveData()

private suspend fun loadLocationsOfInterestAndPair(
mutations: List<Mutation>
): List<MutationDetail> = mutations.mapNotNull { toMutationDetail(it) }
internal val uploadStatus: LiveData<List<SyncStatusDetail>> =
mutationRepository
.getUploadQueueFlow()
.map { uploads ->
val result: MutableList<SyncStatusDetail> = 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),
)
}
}

0 comments on commit 94c1ea7

Please sign in to comment.