diff --git a/ground/src/main/java/com/google/android/ground/model/job/Job.kt b/ground/src/main/java/com/google/android/ground/model/job/Job.kt index 36b6b43824..1e341bbc4a 100644 --- a/ground/src/main/java/com/google/android/ground/model/job/Job.kt +++ b/ground/src/main/java/com/google/android/ground/model/job/Job.kt @@ -54,6 +54,9 @@ data class Job( /** Returns true if the job has one or more tasks. */ fun hasTasks() = tasks.values.isNotEmpty() + + /** Returns whether the job has non-LOI tasks. */ + fun hasNonLoiTasks() = tasks.values.count { !it.isAddLoiTask } > 0 } fun Job.getDefaultColor(): Int = 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 7759c783c1..c3442ec9f9 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 @@ -312,6 +312,14 @@ internal constructor( return currentIndex to size } + /** Returns the index of the task ID, or -1 if null or not found. */ + private fun getIndexOfTask(taskId: String?) = + if (taskId == null) { + -1 + } else { + tasks.indexOfFirst { it.id == taskId } + } + /** * Retrieves the current task sequence given the inputs and conditions set on the tasks. Setting a * start ID will always generate a sequence with the start ID as the first element, and if @@ -321,17 +329,16 @@ internal constructor( if (tasks.isEmpty()) { error("Can't generate sequence for empty task list") } - - val task = tasks.filter { it.id == (startId ?: tasks[0].id) } - - // TODO(#2539): Cleanup once https://github.com/google/ground-android/issues/2539 is resolved. - if (task.isEmpty()) { - error( - "Unable to find a task with id startId=$startId, firstTaskId=${tasks[0].id}, allTasks=${tasks.map { it.id }}" - ) - } - - val startIndex = tasks.indexOf(task.first()) + val startIndex = + getIndexOfTask(startId).let { + if (it < 0) { + // Default to 0 if startId is not found or is null. + if (startId != null) Timber.w("startId, $startId, was not found. Defaulting to 0") + 0 + } else { + it + } + } return if (reversed) { tasks.subList(0, startIndex + 1).reversed() } else { diff --git a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt index ac3035014e..96351ac877 100644 --- a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt @@ -79,7 +79,7 @@ class HomeScreenMapContainerFragment : AbstractMapContainerFragment() { val canUserSubmitData = userRepository.canUserSubmitData() // Handle collect button clicks - adapter.setCollectDataListener { onCollectData(canUserSubmitData, it) } + adapter.setCollectDataListener { onCollectData(canUserSubmitData, hasValidTasks(it), it) } // Bind data for cards mapContainerViewModel.getMapCardUiData().launchWhenStartedAndCollect { (mapCards, loiCount) -> @@ -90,15 +90,32 @@ class HomeScreenMapContainerFragment : AbstractMapContainerFragment() { map.featureClicks.launchWhenStartedAndCollect { mapContainerViewModel.onFeatureClicked(it) } } + private fun hasValidTasks(cardUiData: MapCardUiData) = + when (cardUiData) { + // LOI tasks are filtered out of the tasks list for pre-defined tasks. + is MapCardUiData.LoiCardUiData -> + cardUiData.loi.job.tasks.values.count { !it.isAddLoiTask } > 0 + is MapCardUiData.AddLoiCardUiData -> cardUiData.job.tasks.values.isNotEmpty() + } + /** Invoked when user clicks on the map cards to collect data. */ - private fun onCollectData(canUserSubmitData: Boolean, cardUiData: MapCardUiData) { - if (canUserSubmitData) { - navigateToDataCollectionFragment(cardUiData) - } else { + private fun onCollectData( + canUserSubmitData: Boolean, + hasTasks: Boolean, + cardUiData: MapCardUiData, + ) { + if (!canUserSubmitData) { // Skip data collection screen if the user can't submit any data // TODO(#1667): Revisit UX for displaying view only mode ephemeralPopups.ErrorPopup().show(getString(R.string.collect_data_viewer_error)) + return + } + if (!hasTasks) { + // NOTE(#2539): The DataCollectionFragment will crash if there are no tasks. + ephemeralPopups.ErrorPopup().show(getString(R.string.no_tasks_error)) + return } + navigateToDataCollectionFragment(cardUiData) } /** Updates the given [TextView] with the submission count for the given [LocationOfInterest]. */ diff --git a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/cards/MapCardAdapter.kt b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/cards/MapCardAdapter.kt index 53254cb806..f7a8479270 100644 --- a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/cards/MapCardAdapter.kt +++ b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/cards/MapCardAdapter.kt @@ -145,8 +145,9 @@ class MapCardAdapter( with(binding) { loiName.text = loiHelper.getDisplayLoiName(loi) jobName.text = loiHelper.getJobName(loi) + // NOTE(#2539): The DataCollectionFragment will crash if there are no non-LOI tasks. collectData.visibility = - if (canUserSubmitData && loi.job.hasTasks()) View.VISIBLE else View.GONE + if (canUserSubmitData && loi.job.hasNonLoiTasks()) View.VISIBLE else View.GONE updateSubmissionCount(loi, submissions) } } diff --git a/ground/src/main/res/values/strings.xml b/ground/src/main/res/values/strings.xml index bbc116f916..bd818d2d5d 100644 --- a/ground/src/main/res/values/strings.xml +++ b/ground/src/main/res/values/strings.xml @@ -90,6 +90,7 @@ Drag your map until the center pin is on the desired location Current location: Loading… + This job has no more tasks to complete Can’t collect data as user is a VIEWER Offline map imagery Hide or show downloaded imagery