Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ internal constructor(
val uiState: StateFlow<DataCollectionUiState> = _uiState

val loiNameDialogOpen = mutableStateOf(false)

private val _loiNameDraft = MutableStateFlow("")
val loiNameDraft: StateFlow<String> = _loiNameDraft

private var shouldLoadFromDraft: Boolean = savedStateHandle[TASK_SHOULD_LOAD_FROM_DRAFT] ?: false

private val jobId: String = requireNotNull(savedStateHandle[TASK_JOB_ID_KEY])
Expand Down Expand Up @@ -126,6 +130,30 @@ internal constructor(
}
}

fun setLoiNameDraft(name: String) {
_loiNameDraft.value = name
}

fun getLoiName(): String {
val state = uiState.value
return (state as? DataCollectionUiState.Ready)?.loiName ?: getTypedLoiNameOrEmpty()
}

fun openLoiNameDialog() {
setLoiNameDraft(getLoiName())
loiNameDialogOpen.value = true
}

fun confirmLoiName(name: String) {
loiNameDialogOpen.value = false
setLoiName(name)
}

fun dismissLoiNameDialog(initialName: String) {
loiNameDialogOpen.value = false
setLoiNameDraft(initialName)
}

private fun isFirstPosition(taskId: String): Boolean =
withReadyOrNull { taskSequenceHandler.isFirstPosition(taskId) } ?: false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ abstract class AbstractTaskFragment<T : AbstractTaskViewModel> : AbstractFragmen

private fun handleNext() {
if (getTask().isAddLoiTask) {
dataCollectionViewModel.loiNameDialogOpen.value = true
dataCollectionViewModel.openLoiNameDialog()
} else {
moveToNext()
}
Expand Down Expand Up @@ -187,11 +187,23 @@ abstract class AbstractTaskFragment<T : AbstractTaskViewModel> : AbstractFragmen

/** Handles actions triggered from the task screen UI. */
fun handleTaskScreenAction(screenAction: TaskScreenAction) {
if (screenAction is TaskScreenAction.OnButtonClicked) {
handleButtonClick(screenAction.action)
} else {
// TODO: Handle other actions
// https://github.com/google/ground-android/issues/3630
when (screenAction) {
is TaskScreenAction.OnButtonClicked -> {
handleButtonClick(screenAction.action)
}
is TaskScreenAction.OnLoiNameChanged -> {
dataCollectionViewModel.setLoiNameDraft(screenAction.name)
}
is TaskScreenAction.OnLoiNameDismiss -> {
dataCollectionViewModel.dismissLoiNameDialog(dataCollectionViewModel.getLoiName())
}
is TaskScreenAction.OnLoiNameConfirm -> {
dataCollectionViewModel.confirmLoiName(screenAction.name)
moveToNext()
}
else -> {
// Remaining actions are task specific and are handled within the task screen.
}
}
}

Expand All @@ -215,7 +227,7 @@ abstract class AbstractTaskFragment<T : AbstractTaskViewModel> : AbstractFragmen
private fun getTask(): Task = viewModel.task

@Composable
private fun LoiNameDialog() {
protected fun LoiNameDialog() {
var openAlertDialog by dataCollectionViewModel.loiNameDialogOpen

if (openAlertDialog) {
Expand All @@ -242,13 +254,13 @@ abstract class AbstractTaskFragment<T : AbstractTaskViewModel> : AbstractFragmen

@Composable
private fun InstructionsDialog(instructionData: InstructionData) {
var showInstructionsDialog by viewModel.showInstructionsDialog
val showInstructionsDialog by viewModel.showInstructionsDialog.collectAsStateWithLifecycle()

if (showInstructionsDialog) {
InstructionsDialog(
data = instructionData,
onDismissed = {
showInstructionsDialog = false
viewModel.dismissInstructions()
onInstructionDialogDismissed()
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package org.groundplatform.android.ui.datacollection.tasks

import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
Expand Down Expand Up @@ -43,7 +42,8 @@ abstract class AbstractTaskViewModel internal constructor() : AbstractViewModel(
private val _taskDataFlow: MutableStateFlow<TaskData?> = MutableStateFlow(null)
val taskTaskData: StateFlow<TaskData?> = _taskDataFlow.asStateFlow()

val showInstructionsDialog = mutableStateOf(false)
private val _showInstructionsDialog = MutableStateFlow(false)
val showInstructionsDialog = _showInstructionsDialog.asStateFlow()

open val taskActionButtonStates: StateFlow<List<ButtonActionState>> by lazy {
taskTaskData
Expand All @@ -56,6 +56,14 @@ abstract class AbstractTaskViewModel internal constructor() : AbstractViewModel(
lateinit var task: Task
private lateinit var taskPositionInterface: TaskPositionInterface

fun dismissInstructions() {
_showInstructionsDialog.value = false
}

fun showInstructions() {
_showInstructionsDialog.value = true
}

open fun initialize(
job: Job,
task: Task,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,43 +15,42 @@
*/
package org.groundplatform.android.ui.datacollection.tasks.point

import androidx.compose.runtime.Composable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import javax.inject.Provider
import org.groundplatform.android.R
import org.groundplatform.android.ui.datacollection.components.InstructionData
import org.groundplatform.android.ui.datacollection.components.TaskHeader
import org.groundplatform.android.ui.datacollection.components.TaskMapFragmentContainer
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskFragment
import org.groundplatform.android.util.createComposeView

@AndroidEntryPoint
class DropPinTaskFragment @Inject constructor() : AbstractTaskFragment<DropPinTaskViewModel>() {
@Inject lateinit var dropPinTaskMapFragmentProvider: Provider<DropPinTaskMapFragment>

override val taskHeader: TaskHeader by lazy {
TaskHeader(viewModel.task.label, R.drawable.outline_pin_drop)
}

override val instructionData =
InstructionData(iconId = R.drawable.swipe_24, stringId = R.string.drop_a_pin_tooltip_text)

@Composable
override fun TaskBody() {
TaskMapFragmentContainer(
taskId = viewModel.task.id,
fragmentManager = childFragmentManager,
fragmentProvider = dropPinTaskMapFragmentProvider,
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
) = createComposeView {
val shouldShowLoiNameDialog by dataCollectionViewModel.loiNameDialogOpen
val loiName by dataCollectionViewModel.loiNameDraft.collectAsStateWithLifecycle()

override fun onTaskResume() {
if (isVisible && viewModel.shouldShowInstructionsDialog()) {
viewModel.showInstructionsDialog.value = true
DropPinTaskScreen(
viewModel = viewModel,
onFooterPositionUpdated = { saveFooterPosition(it) },
shouldShowLoiNameDialog = shouldShowLoiNameDialog,
loiName = loiName,
onAction = { action -> handleTaskScreenAction(action) },
) {
TaskMapFragmentContainer(
taskId = viewModel.task.id,
fragmentManager = childFragmentManager,
fragmentProvider = dropPinTaskMapFragmentProvider,
)
}
}

override fun onInstructionDialogDismissed() {
viewModel.instructionsDialogShown = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package org.groundplatform.android.ui.datacollection.tasks.point

import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.groundplatform.android.model.map.CameraPosition
Expand All @@ -34,14 +34,12 @@ class DropPinTaskMapFragment @Inject constructor() :

// Disable pan/zoom gestures if a marker has been placed on the map.
launchWhenTaskVisible(dataCollectionViewModel, taskId) {
taskViewModel.features.asFlow().collect { features ->
updateGestures(features, taskViewModel.captureLocation)
}
taskViewModel.features.collect { features -> updateGestures(features) }
}
}

private fun updateGestures(features: Set<Feature>, captureLocation: Boolean) {
if (features.isNotEmpty() || captureLocation) {
private fun updateGestures(features: Set<Feature>) {
if (features.isNotEmpty()) {
map.disableGestures()
} else {
map.enableGestures()
Expand All @@ -53,7 +51,7 @@ class DropPinTaskMapFragment @Inject constructor() :
taskViewModel.updateCameraPosition(position)
}

override fun renderFeatures(): LiveData<Set<Feature>> = taskViewModel.features
override fun renderFeatures(): LiveData<Set<Feature>> = taskViewModel.features.asLiveData()
Comment thread
shobhitagarwal1612 marked this conversation as resolved.
Outdated

override fun setDefaultViewPort() {
val feature = taskViewModel.features.value?.firstOrNull() ?: return
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.groundplatform.android.ui.datacollection.tasks.point

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.groundplatform.android.R
import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport
import org.groundplatform.android.ui.datacollection.components.ButtonAction
import org.groundplatform.android.ui.datacollection.components.ButtonActionState
import org.groundplatform.android.ui.datacollection.components.InstructionData
import org.groundplatform.android.ui.datacollection.components.TaskHeader
import org.groundplatform.android.ui.datacollection.tasks.TaskScreen
import org.groundplatform.android.ui.datacollection.tasks.TaskScreenAction
import org.groundplatform.ui.theme.AppTheme

/**
* A screen for dropping a pin on the map.
*
* This is the stateful wrapper that collects state from [DropPinTaskViewModel] and handles event
* routing.
*
* @param viewModel The view model for this task.
* @param onFooterPositionUpdated Callback when the footer position changes.
* @param onAction Callback for screen actions (e.g., navigation).
* @param mapContent Composable for rendering the map.
*/
@Composable
fun DropPinTaskScreen(
viewModel: DropPinTaskViewModel,
onFooterPositionUpdated: (Float) -> Unit,
shouldShowLoiNameDialog: Boolean,
loiName: String,
onAction: (TaskScreenAction) -> Unit,
mapContent: @Composable () -> Unit,
) {
val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle()
val showInstructionsDialog by viewModel.showInstructionsDialog.collectAsStateWithLifecycle()

DropPinTaskContent(
taskLabel = viewModel.task.label,
taskActionButtonsStates = taskActionButtonsStates,
showInstructionsDialog = showInstructionsDialog,
shouldShowLoiNameDialog = shouldShowLoiNameDialog,
loiName = loiName,
onFooterPositionUpdated = onFooterPositionUpdated,
onAction = { action ->
if (action is TaskScreenAction.OnInstructionsDismiss) {
viewModel.dismissDropPinInstructions()
} else {
onAction(action)
}
},
mapContent = mapContent,
)
}

/**
* The stateless content of the drop pin task screen.
*
* @param taskLabel The label of the task.
* @param taskActionButtonsStates The states of the action buttons.
* @param showInstructionsDialog Whether to show the instructions' dialog.
* @param onFooterPositionUpdated Callback when the footer position changes.
* @param onAction Callback for screen actions.
* @param mapContent Composable for rendering the map.
*/
@Composable
private fun DropPinTaskContent(
taskLabel: String,
taskActionButtonsStates: List<ButtonActionState>,
showInstructionsDialog: Boolean,
shouldShowLoiNameDialog: Boolean,
loiName: String,
onFooterPositionUpdated: (Float) -> Unit,
onAction: (TaskScreenAction) -> Unit,
mapContent: @Composable () -> Unit,
) {
TaskScreen(
taskHeader = TaskHeader(taskLabel, R.drawable.outline_pin_drop),
instructionData =
InstructionData(iconId = R.drawable.swipe_24, stringId = R.string.drop_a_pin_tooltip_text),
taskActionButtonsStates = taskActionButtonsStates,
showInstructionsDialog = showInstructionsDialog,
shouldShowLoiNameDialog = shouldShowLoiNameDialog,
loiName = loiName,
onFooterPositionUpdated = onFooterPositionUpdated,
onAction = onAction,
taskBody = mapContent,
)
}

@Preview(showBackground = true)
@Composable
@ExcludeFromJacocoGeneratedReport
private fun DropPinTaskScreenPreview() {
AppTheme {
DropPinTaskContent(
taskLabel = "Task for dropping a pin",
taskActionButtonsStates =
listOf(
ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true),
ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true),
ButtonActionState(ButtonAction.UNDO, isEnabled = false, isVisible = false),
ButtonActionState(ButtonAction.DROP_PIN, isEnabled = true, isVisible = true),
ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = false),
),
showInstructionsDialog = true,
shouldShowLoiNameDialog = false,
loiName = "",
onFooterPositionUpdated = {},
onAction = {},
mapContent = {},
)
}
}
Loading
Loading