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

Refactor DataCollectionViewModel #2996

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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 @@ -26,7 +26,7 @@
import com.google.android.ground.model.job.Job
import com.google.android.ground.model.submission.TaskData
import com.google.android.ground.model.submission.ValueDelta
import com.google.android.ground.model.submission.isNullOrEmpty
import com.google.android.ground.model.submission.isNotNullOrEmpty
import com.google.android.ground.model.task.Condition
import com.google.android.ground.model.task.Task
import com.google.android.ground.persistence.local.room.converter.SubmissionDeltasConverter
Expand Down Expand Up @@ -150,7 +150,7 @@
lateinit var submissionId: String

fun init() {
_uiState.update { UiState.TaskListAvailable(tasks, getTaskPosition()) }
_uiState.update { UiState.TaskListAvailable(tasks, getTaskPosition(currentTaskId.value)) }
}

fun setLoiName(name: String) {
Expand Down Expand Up @@ -210,16 +210,12 @@
val taskValue = taskViewModel.taskTaskData.value

// Skip validation if the task is empty
if (taskValue.isNullOrEmpty()) {
data[task] = taskValue
moveToPreviousTask()
return
}

val validationError = taskViewModel.validate()
if (validationError != null) {
popups.get().ErrorPopup().show(validationError)
return
if (taskValue.isNotNullOrEmpty()) {
val validationError = taskViewModel.validate()
if (validationError != null) {
popups.get().ErrorPopup().show(validationError)
return

Check warning on line 217 in ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt#L216-L217

Added lines #L216 - L217 were not covered by tests
}
}

data[task] = taskValue
Expand All @@ -237,9 +233,11 @@
return
}

data[taskViewModel.task] = taskViewModel.taskTaskData.value
val task = taskViewModel.task
val taskValue = taskViewModel.taskTaskData
data[task] = taskValue.value

if (!isLastPosition()) {
if (!isLastPosition(task.id)) {
moveToNextTask()
} else {
clearDraft()
Expand All @@ -248,26 +246,28 @@
}
}

/** Persists the current UI state locally which can be resumed whenever the app re-opens. */
fun saveCurrentState() {
getTaskViewModel(currentTaskId.value)?.let {
if (!data.containsKey(it.task)) {
val validationError = it.validate()
if (validationError != null) {
return
}
val taskId = currentTaskId.value
val viewModel = getTaskViewModel(taskId) ?: error("ViewModel not found for task $taskId")

data[it.task] = it.taskTaskData.value
savedStateHandle[TASK_POSITION_ID] = it.task.id
saveDraft()
}
val validationError = viewModel.validate()

Check warning on line 254 in ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt#L254

Added line #L254 was not covered by tests
if (validationError != null) {
Timber.d("Ignoring task $taskId with invalid data: $validationError")
return

Check warning on line 257 in ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt#L256-L257

Added lines #L256 - L257 were not covered by tests
}

data[viewModel.task] = viewModel.taskTaskData.value
savedStateHandle[TASK_POSITION_ID] = taskId
saveDraft(taskId)

Check warning on line 262 in ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt#L260-L262

Added lines #L260 - L262 were not covered by tests
}

/** Retrieves a list of [ValueDelta] for tasks that are part of the current sequence. */
private fun getDeltas(): List<ValueDelta> =
// Filter deltas to valid tasks.
data
.filter { (task) -> task in getTaskSequence() }
.map { (task, value) -> ValueDelta(task.id, task.type, value) }
getTaskSequence()
.mapNotNull { task -> data[task]?.let { ValueDelta(task.id, task.type, it) } }
.toList()

/** Persists the changes locally and enqueues a worker to sync with remote datastore. */
private fun saveChanges(deltas: List<ValueDelta>) {
Expand All @@ -277,23 +277,23 @@
}
}

private fun getAbsolutePosition(): Int {
if (currentTaskId.value == "") {
private fun getAbsolutePosition(taskId: String): Int {
if (taskId == "") {
return 0
}
return tasks.indexOf(tasks.first { it.id == currentTaskId.value })
return tasks.indexOf(tasks.first { it.id == taskId })
}

/** Persists the collected data as draft to local storage. */
private fun saveDraft() {
private fun saveDraft(taskId: String) {
externalScope.launch(ioDispatcher) {
submissionRepository.saveDraftSubmission(
jobId = jobId,
loiId = loiId,
surveyId = surveyId,
deltas = getDeltas(),
loiName = customLoiName,
currentTaskId = currentTaskId.value,
currentTaskId = taskId,
)
}
}
Expand All @@ -307,11 +307,11 @@
* Get the current index within the computed task sequence, and the number of tasks in the
* sequence, e.g (0, 2) means the first task of 2.
*/
private fun getPositionInTaskSequence(): Pair<Int, Int> {
private fun getPositionInTaskSequence(taskId: String): Pair<Int, Int> {
var currentIndex = 0
var size = 0
getTaskSequence().forEachIndexed { index, task ->
if (task.id == currentTaskId.value) {
if (task.id == taskId) {
currentIndex = index
}
size++
Expand Down Expand Up @@ -377,15 +377,15 @@

// Save collected data as draft
clearDraft()
saveDraft()
saveDraft(task.id)

_uiState.update { UiState.TaskUpdated(getTaskPosition()) }
_uiState.update { UiState.TaskUpdated(getTaskPosition(task.id)) }
}

private fun getTaskPosition(): TaskPosition {
val (index, size) = getPositionInTaskSequence()
private fun getTaskPosition(taskId: String): TaskPosition {
val (index, size) = getPositionInTaskSequence(taskId)
return TaskPosition(
absoluteIndex = getAbsolutePosition(),
absoluteIndex = getAbsolutePosition(taskId),
relativeIndex = index,
sequenceSize = size,
)
Expand All @@ -395,16 +395,14 @@
fun isFirstPosition(taskId: String): Boolean = taskId == getTaskSequence().first().id

/**
* Returns true if the given [taskId] with task data would be last in sequence. Defaults to the
* current active task if not set. Useful for handling conditional tasks, see #2394.
* Returns true if the given [taskId] with task data would be last in sequence. Useful for
* handling conditional tasks, see #2394.
*/
fun checkLastPositionWithTaskData(taskId: String? = null, value: TaskData?): Boolean =
(taskId ?: currentTaskId.value) ==
getTaskSequence(taskValueOverride = (taskId ?: currentTaskId.value) to value).last().id
fun checkLastPositionWithTaskData(taskId: String, value: TaskData?): Boolean =
taskId == getTaskSequence(taskValueOverride = taskId to value).last().id

/** Returns true if the given [taskId] is last if set, or the current active task. */
fun isLastPosition(taskId: String? = null): Boolean =
(taskId ?: currentTaskId.value) == getTaskSequence().last().id
/** Returns true if the given [taskId] is last task in sequence. */
fun isLastPosition(taskId: String): Boolean = taskId == getTaskSequence().last().id

/** Evaluates the task condition against the current inputs. */
private fun evaluateCondition(
Expand Down
Loading