Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ dependencies {
}

// Fragments
implementation libs.androidx.fragment.compose
debugImplementation libs.androidx.fragment.testing.manifest

// TODO: Move protos into shared module and set correct path here.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,22 @@ import org.groundplatform.android.R
import org.groundplatform.android.ui.common.AbstractFragment
import org.groundplatform.android.ui.common.BackPressListener
import org.groundplatform.android.ui.common.EphemeralPopups
import org.groundplatform.android.ui.home.HomeScreenViewModel
import org.groundplatform.android.util.createComposeView
import org.groundplatform.android.util.openAppSettings
import javax.inject.Inject

/** Fragment allowing the user to collect data to complete a task. */
@AndroidEntryPoint
class DataCollectionFragment : AbstractFragment(), BackPressListener {
@Inject lateinit var popups: EphemeralPopups
@Inject lateinit var viewPagerAdapterFactory: DataCollectionViewPagerAdapterFactory

val viewModel: DataCollectionViewModel by hiltNavGraphViewModels(R.id.data_collection)

val homeScreenViewModel: HomeScreenViewModel by lazy {
getViewModel(HomeScreenViewModel::class.java)
}

private var isNavigatingUp = false

override fun onCreateView(
Expand All @@ -46,9 +51,10 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
): View = createComposeView {
DataCollectionScreen(
viewModel = viewModel,
fragment = this,
onValidationError = { resId -> popups.ErrorPopup().show(resId) },
onExitConfirmed = { navigateBack() },
onOpenSettings = { requireActivity().openAppSettings() },
onAwaitingPhotoCapture = { homeScreenViewModel.awaitingPhotoCapture = it },
)
}

Expand Down Expand Up @@ -84,4 +90,8 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
viewModel.clearDraftBlocking()
findNavController().navigateUp()
}

companion object {
const val TASK_ID: String = "taskId"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
Expand All @@ -41,20 +42,24 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.groundplatform.android.R
import org.groundplatform.android.ui.components.ConfirmationDialog
import org.groundplatform.android.ui.datacollection.tasks.TaskScreenContainer

/**
* The main screen for data collection, coordinating the task sequence and host UI.
*
* @param viewModel The view model for data collection.
* @param fragment The fragment hosting this screen (retained for ViewPager2 adapter creation).
* @param onValidationError Callback when a validation error occurs.
* @param onExitConfirmed Callback when the user confirms exiting the data collection flow.
* @param onOpenSettings Callback to open the app settings.
* @param onAwaitingPhotoCapture Callback to set whether the app is awaiting a photo capture.
*/
@Composable
fun DataCollectionScreen(
viewModel: DataCollectionViewModel,
fragment: DataCollectionFragment,
onValidationError: (resId: Int) -> Unit,
onExitConfirmed: () -> Unit,
onOpenSettings: () -> Unit,
onAwaitingPhotoCapture: (Boolean) -> Unit,
) {
val showExitWarningDialog by viewModel.showExitWarning.collectAsStateWithLifecycle()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Expand All @@ -63,13 +68,40 @@ fun DataCollectionScreen(
viewModel.uiEffects.collect { effect ->
when (effect) {
is DataCollectionUiEffect.Exit -> onExitConfirmed()
is DataCollectionUiEffect.OpenSettings -> onOpenSettings()
is DataCollectionUiEffect.SetAwaitingPhotoCapture -> onAwaitingPhotoCapture(effect.awaiting)
is DataCollectionUiEffect.ShowValidationError -> onValidationError(effect.errorResId)
}
}
}

DataCollectionContent(uiState = uiState, onCloseClicked = { viewModel.onCloseClicked() }) {
DataCollectionViewPager(uiState, fragment)
readyState ->
val tasks = readyState.tasks
if (tasks.isNotEmpty()) {
val position = readyState.position
val currentTask = tasks[position.absoluteIndex]

key(currentTask.id) {
viewModel.getTaskViewModel(currentTask.id)?.let { taskViewModel ->
val loiName by viewModel.loiNameDraft.collectAsStateWithLifecycle()
val showLoiNameDialog by viewModel.loiNameDialogOpen.collectAsStateWithLifecycle()

val onLoiNameAction = { action: LoiNameAction ->
viewModel.handleLoiNameAction(action, currentTask.id)
}

TaskScreenContainer(
task = currentTask,
taskViewModel = taskViewModel,
taskPosition = position,
loiName = loiName,
shouldShowLoiNameDialog = showLoiNameDialog,
onLoiNameAction = { onLoiNameAction(it) },
)
}
}
}
}

if (showExitWarningDialog) {
Expand Down Expand Up @@ -102,7 +134,7 @@ object DataCollectionScreenTestTags {
fun DataCollectionContent(
uiState: DataCollectionUiState,
onCloseClicked: () -> Unit,
pagerContent: @Composable () -> Unit,
pagerContent: @Composable (DataCollectionUiState.Ready) -> Unit,
) {
Scaffold(topBar = { DataCollectionToolbar(uiState, onCloseClicked) }) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
Expand All @@ -118,7 +150,7 @@ fun DataCollectionContent(
ErrorContent()
}
is DataCollectionUiState.Ready -> {
ReadyContent(pagerContent = pagerContent)
ReadyContent { pagerContent(uiState) }
}
is DataCollectionUiState.TaskSubmitted -> {
DataSubmissionConfirmationScreen(loiReport = uiState.loiReport) { onCloseClicked() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -65,6 +64,10 @@ import javax.inject.Inject
sealed interface DataCollectionUiEffect {
data object Exit : DataCollectionUiEffect

data object OpenSettings : DataCollectionUiEffect

data class SetAwaitingPhotoCapture(val awaiting: Boolean) : DataCollectionUiEffect

data class ShowValidationError(val errorResId: Int) : DataCollectionUiEffect
}

Expand All @@ -88,9 +91,6 @@ internal constructor(
private val _uiEffects = Channel<DataCollectionUiEffect>(Channel.BUFFERED)
val uiEffects = _uiEffects.receiveAsFlow()

private val _dataCollectionEvents =
MutableSharedFlow<DataCollectionEvent>(extraBufferCapacity = 1)

private val _uiState = MutableStateFlow<DataCollectionUiState>(DataCollectionUiState.Loading)
val uiState: StateFlow<DataCollectionUiState> = _uiState

Expand Down Expand Up @@ -136,19 +136,6 @@ internal constructor(
}
_uiState.value = initResult
}

viewModelScope.launch {
_dataCollectionEvents.collect { event ->
withReadyOrNull { it.currentTaskId }
?.let { taskId ->
when (event) {
DataCollectionEvent.NavigatePrevious -> onPreviousClicked(taskId)
DataCollectionEvent.NavigateNext -> onNextClicked(taskId)
DataCollectionEvent.ShowLoiDialog -> openLoiNameDialog()
}
}
}
}
}

private fun setLoiName(name: String) {
Expand Down Expand Up @@ -203,6 +190,16 @@ internal constructor(
viewModelScope.launch { _uiEffects.send(DataCollectionUiEffect.Exit) }
}

private fun openSettings() {
viewModelScope.launch { _uiEffects.send(DataCollectionUiEffect.OpenSettings) }
}

private fun setAwaitingPhotoCapture(awaiting: Boolean) {
viewModelScope.launch {
_uiEffects.send(DataCollectionUiEffect.SetAwaitingPhotoCapture(awaiting))
}
}

fun handleLoiNameAction(action: LoiNameAction, taskId: String) {
when (action) {
is LoiNameAction.Confirmed -> {
Expand Down Expand Up @@ -332,7 +329,19 @@ internal constructor(
isLastPositionWithValue(task, taskData)
},
surveyId = state.surveyId,
eventReporter = { _dataCollectionEvents.tryEmit(it) },
eventReporter = { event ->
withReadyOrNull { it.currentTaskId }
?.let { taskId ->
when (event) {
is DataCollectionEvent.NavigatePrevious -> onPreviousClicked(taskId)
is DataCollectionEvent.NavigateNext -> onNextClicked(taskId)
is DataCollectionEvent.ShowLoiDialog -> openLoiNameDialog()
is DataCollectionEvent.OpenSettings -> openSettings()
is DataCollectionEvent.SetAwaitingPhotoCapture ->
setAwaitingPhotoCapture(event.awaiting)
}
}
},
)
updateDataAndInvalidateTasks(task, taskData)
taskViewModels.value[task.id] = created
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading