diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt index 8e69cb4f5c..64c2319e0a 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt @@ -129,7 +129,7 @@ class DataCollectionFragment : Hilt_DataCollectionFragment(), BackPressListener false } else { // Otherwise, select the previous step. - viewModel.setCurrentPosition(viewModel.currentPosition.value!! - 1) + viewModel.updateCurrentPosition(viewModel.getVisibleTaskPosition() - 1) true } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt index b0ac5a769a..76c9fa250f 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt @@ -116,10 +116,9 @@ internal constructor( private val responses: MutableMap = LinkedHashMap() - private val currentPositionKey = "currentPosition" - // Tracks the user's current position in the list of tasks for the current Job + // Tracks the task's current position in the list of tasks for the current job var currentPosition: @Hot(replays = true) MutableLiveData = - savedStateHandle.getLiveData(currentPositionKey, 0) + savedStateHandle.getLiveData(TASK_POSITION_KEY, 0) var currentTaskData: TaskData? = null @@ -162,7 +161,7 @@ internal constructor( * Validates the user's input and displays an error if the user input was invalid. Progresses to * the next Data Collection screen if the user input was valid. */ - fun onNextClicked() { + fun onNextClicked(position: Int) { val currentTask = currentTaskViewModel ?: return val validationError = currentTask.validate() @@ -171,16 +170,10 @@ internal constructor( return } - val currentTaskPosition = currentPosition.value!! - val finalTaskPosition = tasks.size - 1 - - assert(finalTaskPosition >= 0) - assert(currentTaskPosition in 0..finalTaskPosition) - responses[currentTask.task] = currentTaskData - if (currentTaskPosition != finalTaskPosition) { - setCurrentPosition(currentPosition.value!! + 1) + if (!isLastPosition(position)) { + updateCurrentPosition(position + 1) } else { val taskDataDeltas = responses.map { (task, taskData) -> TaskDataDelta(task.id, task.type, taskData) } @@ -202,14 +195,30 @@ internal constructor( } } - fun setCurrentPosition(position: Int) { - savedStateHandle[currentPositionKey] = position + /** Returns the position of the task fragment visible to the user. */ + fun getVisibleTaskPosition() = currentPosition.value!! + + /** Displays the task at the given position to the user. */ + fun updateCurrentPosition(position: Int) { + savedStateHandle[TASK_POSITION_KEY] = position + } + + /** Returns true if the given task position is last. */ + fun isLastPosition(taskPosition: Int): Boolean { + val finalTaskPosition = tasks.size - 1 + + assert(finalTaskPosition >= 0) + assert(taskPosition in 0..finalTaskPosition) + + return taskPosition == finalTaskPosition } private fun createSuggestLoiTask(taskType: Task.Type): Task = Task(id = "-1", index = -1, taskType, resources.getString(R.string.new_site), isRequired = true) companion object { + private const val TASK_POSITION_KEY = "currentPosition" + fun getViewModelClass(taskType: Task.Type): Class = when (taskType) { Task.Type.TEXT -> TextTaskViewModel::class.java diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/components/ButtonAction.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/components/ButtonAction.kt index 0b113d5240..a95bf60da7 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/components/ButtonAction.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/components/ButtonAction.kt @@ -28,6 +28,7 @@ enum class ButtonAction( ) { // All tasks + DONE(Type.TEXT, Theme.DARK_GREEN, textId = R.string.done), NEXT(Type.TEXT, Theme.DARK_GREEN, textId = R.string.next), SKIP(Type.TEXT, Theme.TRANSPARENT, textId = R.string.skip), UNDO(Type.ICON, Theme.LIGHT_GREEN, drawableId = R.drawable.ic_undo_black), diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskFragment.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskFragment.kt index 8dc2c90ae2..4adcc01e76 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskFragment.kt @@ -120,7 +120,7 @@ abstract class AbstractTaskFragment : AbstractFragmen protected fun addNextButton() = addButton(ButtonAction.NEXT) - .setOnClickListener { dataCollectionViewModel.onNextClicked() } + .setOnClickListener { moveToNext() } .setOnTaskUpdated { button, taskData -> button.enableIfTrue(taskData.isNotNullOrEmpty()) } .disable() @@ -135,7 +135,11 @@ abstract class AbstractTaskFragment : AbstractFragmen private fun onSkip() { check(viewModel.hasNoData()) { "User should not be able to skip a task with data." } - dataCollectionViewModel.onNextClicked() + moveToNext() + } + + fun moveToNext() { + dataCollectionViewModel.onNextClicked(position) } fun addUndoButton() = @@ -144,7 +148,8 @@ abstract class AbstractTaskFragment : AbstractFragmen .setOnTaskUpdated { button, taskData -> button.showIfTrue(taskData.isNotNullOrEmpty()) } .hide() - protected fun addButton(action: ButtonAction): TaskButton { + protected fun addButton(buttonAction: ButtonAction): TaskButton { + val action = if (buttonAction.shouldReplaceWithDoneButton()) ButtonAction.DONE else buttonAction check(!buttons.contains(action)) { "Button $action already bound" } val button = TaskButtonFactory.createAndAttachButton( @@ -157,6 +162,10 @@ abstract class AbstractTaskFragment : AbstractFragmen return button } + /** Returns true if the given [ButtonAction] should be replace with "Done" button. */ + private fun ButtonAction.shouldReplaceWithDoneButton() = + this == ButtonAction.NEXT && dataCollectionViewModel.isLastPosition(position) + @TestOnly fun getButtons() = buttons @TestOnly fun getButtonsIndex() = buttonsIndex diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragment.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragment.kt index 6c31f728cc..94c61cbc35 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragment.kt @@ -58,7 +58,7 @@ class CaptureLocationTaskFragment : .setOnClickListener { viewModel.updateResponse() } .setOnTaskUpdated { button, taskData -> button.showIfTrue(taskData.isNullOrEmpty()) } addButton(ButtonAction.NEXT) - .setOnClickListener { dataCollectionViewModel.onNextClicked() } + .setOnClickListener { moveToNext() } .setOnTaskUpdated { button, taskData -> button.showIfTrue(taskData.isNotNullOrEmpty()) } .hide() } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragment.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragment.kt index e441822cd1..913edadaf4 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragment.kt @@ -55,7 +55,7 @@ class DropAPinTaskFragment : Hilt_DropAPinTaskFragment() .setOnClickListener { viewModel.dropPin() } .setOnTaskUpdated { button, taskData -> button.showIfTrue(taskData.isNullOrEmpty()) } addButton(ButtonAction.NEXT) - .setOnClickListener { dataCollectionViewModel.onNextClicked() } + .setOnClickListener { moveToNext() } .setOnTaskUpdated { button, taskData -> button.showIfTrue(taskData.isNotNullOrEmpty()) } .hide() } diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt index d00fb5cb7e..13fa77e8b5 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt @@ -146,9 +146,9 @@ class DataCollectionFragmentTest : BaseHiltTest() { onView(withText(TASK_1_NAME)).check(matches(not(isDisplayed()))) onView(withText(TASK_2_NAME)).check(matches(isDisplayed())) - // Click "next" on final task + // Click "done" on final task onView(allOf(withId(R.id.user_response_text), isDisplayed())).perform(typeText(task2Response)) - onView(allOf(withText("Next"), isDisplayed())).perform(click()) + onView(allOf(withText("Done"), isDisplayed())).perform(click()) advanceUntilIdle() verify(submissionRepository) diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt index 30b601a9fa..cba9a61d02 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt @@ -45,6 +45,8 @@ import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.shadows.ShadowAlertDialog @@ -146,6 +148,14 @@ class MultipleChoiceTaskFragmentTest : assertFragmentHasButtons(ButtonAction.SKIP, ButtonAction.NEXT) } + @Test + fun testActionButtons_whenLastTask() { + whenever(dataCollectionViewModel.isLastPosition(any())).thenReturn(true) + setupTaskFragment(job, task) + + hasButtons(ButtonAction.SKIP, ButtonAction.DONE) + } + @Test fun testActionButtons_whenTaskIsOptional() { setupTaskFragment(job, task.copy(isRequired = false))