From 92daef82ffdb03bf0f37385aa45c1caef11c08f5 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Wed, 13 May 2026 09:48:19 +0530 Subject: [PATCH 01/12] Replace ViewPager2 and task fragments with Compose-based TaskScreenContainer --- .../datacollection/DataCollectionFragment.kt | 19 +- .../ui/datacollection/DataCollectionScreen.kt | 14 +- .../datacollection/DataCollectionViewPager.kt | 51 ----- .../DataCollectionViewPagerAdapter.kt | 64 ------ .../DataCollectionViewPagerAdapterFactory.kt | 25 -- .../ui/datacollection/TaskScreenContainer.kt | 166 ++++++++++++++ .../components/TaskMapFragmentContainer.kt | 4 +- .../tasks/AbstractTaskMapFragment.kt | 3 +- .../tasks/DataCollectionTaskFragment.kt | 213 ------------------ .../DataCollectionScreenTest.kt | 7 +- 10 files changed, 199 insertions(+), 367 deletions(-) delete mode 100644 app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPager.kt delete mode 100644 app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPagerAdapter.kt delete mode 100644 app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPagerAdapterFactory.kt create mode 100644 app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt delete mode 100644 app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/DataCollectionTaskFragment.kt diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt index 3be0d7ba1f..0b9f83a177 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt @@ -26,17 +26,30 @@ 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.datacollection.tasks.location.CaptureLocationTaskMapFragment +import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskMapFragment +import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskMapFragment +import org.groundplatform.android.ui.home.HomeScreenViewModel import org.groundplatform.android.util.createComposeView import javax.inject.Inject +import javax.inject.Provider /** 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 + + @Inject + lateinit var captureLocationTaskMapFragmentProvider: Provider + @Inject lateinit var dropPinTaskMapFragmentProvider: Provider + @Inject lateinit var drawAreaTaskMapFragmentProvider: Provider 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( @@ -84,4 +97,8 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { viewModel.clearDraftBlocking() findNavController().navigateUp() } + + companion object { + const val TASK_ID: String = "taskId" + } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt index ba3a1cfa0d..cad19a820b 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt @@ -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 @@ -46,7 +47,7 @@ import org.groundplatform.android.ui.components.ConfirmationDialog * 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 fragment The fragment hosting this screen. * @param onExitConfirmed Callback when the user confirms exiting the data collection flow. */ @Composable @@ -69,7 +70,12 @@ fun DataCollectionScreen( } DataCollectionContent(uiState = uiState, onCloseClicked = { viewModel.onCloseClicked() }) { - DataCollectionViewPager(uiState, fragment) + readyState -> + val tasks = readyState.tasks + val position = readyState.position + val currentTask = tasks[position.relativeIndex] + + key(currentTask.id) { TaskScreenContainer(currentTask, position, fragment) } } if (showExitWarningDialog) { @@ -102,7 +108,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()) { @@ -118,7 +124,7 @@ fun DataCollectionContent( ErrorContent() } is DataCollectionUiState.Ready -> { - ReadyContent(pagerContent = pagerContent) + ReadyContent { pagerContent(uiState) } } is DataCollectionUiState.TaskSubmitted -> { DataSubmissionConfirmationScreen(loiReport = uiState.loiReport) { onCloseClicked() } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPager.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPager.kt deleted file mode 100644 index 477fa35f1e..0000000000 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPager.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.view.doOnLayout -import androidx.viewpager2.widget.ViewPager2 - -/** - * Hosts the ViewPager2 that renders individual task screens. - * - * @param uiState The current UI state. - * @param fragment The fragment hosting this screen. - */ -@Composable -fun DataCollectionViewPager(uiState: DataCollectionUiState, fragment: DataCollectionFragment) { - AndroidView( - factory = { context -> - ViewPager2(context).apply { - isUserInputEnabled = false - offscreenPageLimit = 1 - } - }, - update = { viewPager -> - if (uiState is DataCollectionUiState.Ready) { - val currentAdapter = viewPager.adapter as? DataCollectionViewPagerAdapter - if (currentAdapter == null || currentAdapter.tasks != uiState.tasks) { - viewPager.adapter = fragment.viewPagerAdapterFactory.create(fragment, uiState.tasks) - } - viewPager.doOnLayout { viewPager.setCurrentItem(uiState.position.absoluteIndex, false) } - } - }, - modifier = Modifier.fillMaxSize(), - ) -} diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPagerAdapter.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPagerAdapter.kt deleted file mode 100644 index e91a152823..0000000000 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPagerAdapter.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2022 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 - -import android.os.Bundle -import androidx.fragment.app.Fragment -import androidx.viewpager2.adapter.FragmentStateAdapter -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import org.groundplatform.android.ui.datacollection.tasks.DataCollectionTaskFragment -import org.groundplatform.domain.model.task.Task -import javax.inject.Provider - -/** - * A simple pager adapter that presents the [Task]s associated with a given Submission, in sequence. - */ -class DataCollectionViewPagerAdapter -@AssistedInject -constructor( - private val taskFragmentProvider: Provider, - @Assisted fragment: Fragment, - @Assisted val tasks: List, -) : FragmentStateAdapter(fragment) { - override fun getItemCount(): Int = tasks.size - - override fun createFragment(position: Int): Fragment { - val task = tasks[position] - - val taskFragment = - when (task.type) { - Task.Type.TEXT, - Task.Type.NUMBER, - Task.Type.DATE, - Task.Type.TIME, - Task.Type.INSTRUCTIONS, - Task.Type.MULTIPLE_CHOICE, - Task.Type.PHOTO, - Task.Type.DROP_PIN, - Task.Type.DRAW_AREA, - Task.Type.CAPTURE_LOCATION -> taskFragmentProvider.get() - Task.Type.UNKNOWN -> - throw UnsupportedOperationException("Unsupported task type: ${task.type}") - } - - return taskFragment.also { - val bundle = it.arguments ?: Bundle() - bundle.putString(DataCollectionTaskFragment.TASK_ID, task.id) - it.arguments = bundle - } - } -} diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPagerAdapterFactory.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPagerAdapterFactory.kt deleted file mode 100644 index 6919f8a846..0000000000 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewPagerAdapterFactory.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2022 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 - -import androidx.fragment.app.Fragment -import dagger.assisted.AssistedFactory -import org.groundplatform.domain.model.task.Task - -@AssistedFactory -interface DataCollectionViewPagerAdapterFactory { - fun create(fragment: Fragment, tasks: List): DataCollectionViewPagerAdapter -} diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt new file mode 100644 index 0000000000..73ec17ec9e --- /dev/null +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt @@ -0,0 +1,166 @@ +/* + * 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 + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.TaskMapFragmentContainer +import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskScreen +import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskScreen +import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskScreen +import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.multiplechoice.MultipleChoiceTaskScreen +import org.groundplatform.android.ui.datacollection.tasks.multiplechoice.MultipleChoiceTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.number.NumberTaskScreen +import org.groundplatform.android.ui.datacollection.tasks.number.NumberTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.photo.PhotoTaskScreen +import org.groundplatform.android.ui.datacollection.tasks.photo.PhotoTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskScreen +import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskScreen +import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.text.TextTaskScreen +import org.groundplatform.android.ui.datacollection.tasks.text.TextTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.time.TimeTaskScreen +import org.groundplatform.android.ui.datacollection.tasks.time.TimeTaskViewModel +import org.groundplatform.android.util.openAppSettings +import org.groundplatform.domain.model.task.Task + +/** + * A container that renders the appropriate task screen based on the provided [task] type. + * + * @param task the task to be displayed. + * @param taskPosition the position of this task within the data collection flow. + * @param fragment the [DataCollectionFragment] hosting the task screens. + */ +@Composable +fun TaskScreenContainer(task: Task, taskPosition: TaskPosition, fragment: DataCollectionFragment) { + val dataCollectionViewModel = fragment.viewModel + val taskViewModel = dataCollectionViewModel.getTaskViewModel(task.id) ?: return + + val loiName by dataCollectionViewModel.loiNameDraft.collectAsStateWithLifecycle() + val showLoiNameDialog by dataCollectionViewModel.loiNameDialogOpen.collectAsStateWithLifecycle() + + val onButtonClicked = { action: ButtonAction -> taskViewModel.onButtonClick(action) } + + val onLoiNameAction = { action: LoiNameAction -> + dataCollectionViewModel.handleLoiNameAction(action, task.id) + } + + when (taskViewModel) { + is CaptureLocationTaskViewModel -> + CaptureLocationTaskScreen( + viewModel = taskViewModel, + taskPosition = taskPosition, + onButtonClicked = onButtonClicked, + onOpenSettings = { fragment.requireActivity().openAppSettings() }, + ) { + TaskMapFragmentContainer( + taskId = task.id, + fragmentManager = fragment.childFragmentManager, + fragmentProvider = fragment.captureLocationTaskMapFragmentProvider, + ) + } + + is DateTaskViewModel -> + DateTaskScreen( + viewModel = taskViewModel, + taskPosition = taskPosition, + onButtonClicked = onButtonClicked, + ) + + is DrawAreaTaskViewModel -> + DrawAreaTaskScreen( + viewModel = taskViewModel, + taskPosition = taskPosition, + onButtonClicked = onButtonClicked, + onLoiNameAction = onLoiNameAction, + loiName = loiName, + shouldShowLoiNameDialog = showLoiNameDialog, + ) { + TaskMapFragmentContainer( + taskId = task.id, + fragmentManager = fragment.childFragmentManager, + fragmentProvider = fragment.drawAreaTaskMapFragmentProvider, + ) + } + + is DropPinTaskViewModel -> + DropPinTaskScreen( + viewModel = taskViewModel, + taskPosition = taskPosition, + onButtonClicked = onButtonClicked, + onLoiNameAction = onLoiNameAction, + loiName = loiName, + shouldShowLoiNameDialog = showLoiNameDialog, + ) { + TaskMapFragmentContainer( + taskId = task.id, + fragmentManager = fragment.childFragmentManager, + fragmentProvider = fragment.dropPinTaskMapFragmentProvider, + ) + } + + is InstructionTaskViewModel -> + InstructionTaskScreen( + viewModel = taskViewModel, + taskPosition = taskPosition, + onButtonClicked = onButtonClicked, + ) + + is MultipleChoiceTaskViewModel -> + MultipleChoiceTaskScreen( + viewModel = taskViewModel, + taskPosition = taskPosition, + onButtonClicked = onButtonClicked, + ) + + is PhotoTaskViewModel -> + PhotoTaskScreen( + viewModel = taskViewModel, + taskPosition = taskPosition, + onButtonClicked = onButtonClicked, + onAwaitingPhotoCapture = { fragment.homeScreenViewModel.awaitingPhotoCapture = it }, + ) + + is NumberTaskViewModel -> + NumberTaskScreen( + viewModel = taskViewModel, + taskPosition = taskPosition, + onButtonClicked = onButtonClicked, + ) + + is TextTaskViewModel -> + TextTaskScreen( + viewModel = taskViewModel, + taskPosition = taskPosition, + onButtonClicked = onButtonClicked, + ) + + is TimeTaskViewModel -> + TimeTaskScreen( + viewModel = taskViewModel, + taskPosition = taskPosition, + onButtonClicked = onButtonClicked, + ) + + else -> error("Unsupported task type: ${task.type}") + } +} diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskMapFragmentContainer.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskMapFragmentContainer.kt index 6e2bb66bae..2ca5fece94 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskMapFragmentContainer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskMapFragmentContainer.kt @@ -22,7 +22,7 @@ import androidx.core.os.bundleOf import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentManager import org.groundplatform.android.ui.common.AbstractMapContainerFragment -import org.groundplatform.android.ui.datacollection.tasks.DataCollectionTaskFragment +import org.groundplatform.android.ui.datacollection.DataCollectionFragment import javax.inject.Provider /** @@ -41,7 +41,7 @@ fun TaskMapFragmentContainer( factory = { context -> FragmentContainerView(context).apply { id = View.generateViewId() } }, update = { view -> with(fragmentProvider.get()) { - arguments = bundleOf(Pair(DataCollectionTaskFragment.TASK_ID, taskId)) + arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, taskId)) fragmentManager.beginTransaction().replace(view.id, this).commit() } }, diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskMapFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskMapFragment.kt index d2006cef11..a78395d4b0 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskMapFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskMapFragment.kt @@ -41,6 +41,7 @@ import org.groundplatform.android.ui.common.BaseMapViewModel import org.groundplatform.android.ui.components.MapFloatingActionButton import org.groundplatform.android.ui.components.MapFloatingActionButtonType import org.groundplatform.android.ui.components.RecenterButton +import org.groundplatform.android.ui.datacollection.DataCollectionFragment import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.map.Feature import org.groundplatform.android.ui.map.MapFragment @@ -71,7 +72,7 @@ abstract class AbstractTaskMapFragment : private lateinit var viewModel: BaseMapViewModel protected val taskId: String by lazy { - arguments?.getString(DataCollectionTaskFragment.TASK_ID) ?: error("null taskId fragment arg") + arguments?.getString(DataCollectionFragment.TASK_ID) ?: error("null taskId fragment arg") } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/DataCollectionTaskFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/DataCollectionTaskFragment.kt deleted file mode 100644 index e2d31a016f..0000000000 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/DataCollectionTaskFragment.kt +++ /dev/null @@ -1,213 +0,0 @@ -/* - * 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 - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.compose.runtime.getValue -import androidx.hilt.navigation.fragment.hiltNavGraphViewModels -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import dagger.hilt.android.AndroidEntryPoint -import org.groundplatform.android.R -import org.groundplatform.android.ui.common.AbstractFragment -import org.groundplatform.android.ui.datacollection.DataCollectionUiState -import org.groundplatform.android.ui.datacollection.DataCollectionViewModel -import org.groundplatform.android.ui.datacollection.LoiNameAction -import org.groundplatform.android.ui.datacollection.components.ButtonAction -import org.groundplatform.android.ui.datacollection.components.TaskMapFragmentContainer -import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskScreen -import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskScreen -import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskMapFragment -import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskScreen -import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.multiplechoice.MultipleChoiceTaskScreen -import org.groundplatform.android.ui.datacollection.tasks.multiplechoice.MultipleChoiceTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.number.NumberTaskScreen -import org.groundplatform.android.ui.datacollection.tasks.number.NumberTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.photo.PhotoTaskScreen -import org.groundplatform.android.ui.datacollection.tasks.photo.PhotoTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskMapFragment -import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskScreen -import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskMapFragment -import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskScreen -import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.text.TextTaskScreen -import org.groundplatform.android.ui.datacollection.tasks.text.TextTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.time.TimeTaskScreen -import org.groundplatform.android.ui.datacollection.tasks.time.TimeTaskViewModel -import org.groundplatform.android.ui.home.HomeScreenViewModel -import org.groundplatform.android.util.createComposeView -import org.groundplatform.android.util.openAppSettings -import javax.inject.Inject -import javax.inject.Provider - -/** - * A generic fragment that hosts the Compose view for data collection tasks. - * - * This fragment dynamically renders the appropriate task screen (e.g., text input, multiple choice, - * photo, location capture) based on the provided task ID. It also manages the lifecycle of - * task-specific map fragments when required by the task type. - */ -@AndroidEntryPoint -class DataCollectionTaskFragment @Inject constructor() : AbstractFragment() { - @Inject - lateinit var captureLocationTaskMapFragmentProvider: Provider - - @Inject lateinit var dropPinTaskMapFragmentProvider: Provider - - @Inject lateinit var drawAreaTaskMapFragmentProvider: Provider - - private val dataCollectionViewModel: DataCollectionViewModel by - hiltNavGraphViewModels(R.id.data_collection) - - private val homeScreenViewModel: HomeScreenViewModel by lazy { - getViewModel(HomeScreenViewModel::class.java) - } - - private val taskId: String by lazy { - arguments?.getString(TASK_ID) ?: error("taskId not found in arguments") - } - - private val viewModel: AbstractTaskViewModel by lazy { - dataCollectionViewModel.getTaskViewModel(taskId) - ?: error("ViewModel for taskId:$taskId not found.") - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ) = createComposeView { - val loiName by dataCollectionViewModel.loiNameDraft.collectAsStateWithLifecycle() - val showLoiNameDialog by dataCollectionViewModel.loiNameDialogOpen.collectAsStateWithLifecycle() - val uiState by dataCollectionViewModel.uiState.collectAsStateWithLifecycle() - val taskPosition = (uiState as? DataCollectionUiState.Ready)?.position - - val onButtonClicked = { action: ButtonAction -> viewModel.onButtonClick(action) } - - val onLoiNameAction = { action: LoiNameAction -> - dataCollectionViewModel.handleLoiNameAction(action, taskId) - } - - when (val vm = viewModel) { - is CaptureLocationTaskViewModel -> - CaptureLocationTaskScreen( - viewModel = vm, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - onOpenSettings = { requireActivity().openAppSettings() }, - ) { - TaskMapFragmentContainer( - taskId = taskId, - fragmentManager = childFragmentManager, - fragmentProvider = captureLocationTaskMapFragmentProvider, - ) - } - - is DateTaskViewModel -> - DateTaskScreen( - viewModel = vm, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - ) - - is DrawAreaTaskViewModel -> - DrawAreaTaskScreen( - viewModel = vm, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - onLoiNameAction = onLoiNameAction, - loiName = loiName, - shouldShowLoiNameDialog = showLoiNameDialog, - ) { - TaskMapFragmentContainer( - taskId = taskId, - fragmentManager = childFragmentManager, - fragmentProvider = drawAreaTaskMapFragmentProvider, - ) - } - - is DropPinTaskViewModel -> - DropPinTaskScreen( - viewModel = vm, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - onLoiNameAction = onLoiNameAction, - loiName = loiName, - shouldShowLoiNameDialog = showLoiNameDialog, - ) { - TaskMapFragmentContainer( - taskId = taskId, - fragmentManager = childFragmentManager, - fragmentProvider = dropPinTaskMapFragmentProvider, - ) - } - - is InstructionTaskViewModel -> - InstructionTaskScreen( - viewModel = vm, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - ) - - is MultipleChoiceTaskViewModel -> - MultipleChoiceTaskScreen( - viewModel = vm, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - ) - - is PhotoTaskViewModel -> - PhotoTaskScreen( - viewModel = vm, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - onAwaitingPhotoCapture = { homeScreenViewModel.awaitingPhotoCapture = it }, - ) - - is NumberTaskViewModel -> - NumberTaskScreen( - viewModel = vm, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - ) - - is TextTaskViewModel -> - TextTaskScreen( - viewModel = vm, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - ) - - is TimeTaskViewModel -> - TimeTaskScreen( - viewModel = vm, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - ) - - else -> error("Unsupported task type: ${viewModel.task.type}") - } - } - - companion object { - const val TASK_ID = "taskId" - } -} diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt index fa822f1284..7cc7931816 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt @@ -34,7 +34,6 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule -import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @@ -47,8 +46,6 @@ class DataCollectionScreenTest { @Mock private lateinit var mockViewModel: DataCollectionViewModel @Mock private lateinit var mockFragment: DataCollectionFragment - @Mock private lateinit var mockAdapterFactory: DataCollectionViewPagerAdapterFactory - @Mock private lateinit var mockAdapter: DataCollectionViewPagerAdapter private val uiState = MutableStateFlow(DataCollectionUiState.Loading) private val showExitWarning = MutableStateFlow(false) @@ -59,9 +56,7 @@ class DataCollectionScreenTest { whenever(mockViewModel.uiState).doReturn(uiState) whenever(mockViewModel.showExitWarning).doReturn(showExitWarning) whenever(mockViewModel.uiEffects).doReturn(uiEffects) - - whenever(mockAdapterFactory.create(any(), any())).doReturn(mockAdapter) - whenever(mockFragment.viewPagerAdapterFactory).doReturn(mockAdapterFactory) + whenever(mockFragment.viewModel).doReturn(mockViewModel) } private fun setContent(onValidationError: (Int) -> Unit = {}, onExitConfirmed: () -> Unit = {}) { From ea14afeddf7c1a89b1a8fede9100b83a14478a35 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Wed, 13 May 2026 09:58:14 +0530 Subject: [PATCH 02/12] Replace custom TaskMapFragmentContainer with official AndroidFragment --- app/build.gradle | 1 + .../datacollection/DataCollectionFragment.kt | 9 ---- .../ui/datacollection/TaskScreenContainer.kt | 27 +++++----- .../components/TaskMapFragmentContainer.kt | 49 ------------------- gradle/libs.versions.toml | 1 + 5 files changed, 16 insertions(+), 71 deletions(-) delete mode 100644 app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskMapFragmentContainer.kt diff --git a/app/build.gradle b/app/build.gradle index 7066a495d3..62690c34d6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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. diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt index 0b9f83a177..454119e7dc 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt @@ -26,24 +26,15 @@ 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.datacollection.tasks.location.CaptureLocationTaskMapFragment -import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskMapFragment -import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskMapFragment import org.groundplatform.android.ui.home.HomeScreenViewModel import org.groundplatform.android.util.createComposeView import javax.inject.Inject -import javax.inject.Provider /** Fragment allowing the user to collect data to complete a task. */ @AndroidEntryPoint class DataCollectionFragment : AbstractFragment(), BackPressListener { @Inject lateinit var popups: EphemeralPopups - @Inject - lateinit var captureLocationTaskMapFragmentProvider: Provider - @Inject lateinit var dropPinTaskMapFragmentProvider: Provider - @Inject lateinit var drawAreaTaskMapFragmentProvider: Provider - val viewModel: DataCollectionViewModel by hiltNavGraphViewModels(R.id.data_collection) val homeScreenViewModel: HomeScreenViewModel by lazy { diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt index 73ec17ec9e..d67bd8a1b0 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt @@ -17,13 +17,15 @@ package org.groundplatform.android.ui.datacollection import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.core.os.bundleOf +import androidx.fragment.compose.AndroidFragment import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.groundplatform.android.ui.datacollection.components.ButtonAction -import org.groundplatform.android.ui.datacollection.components.TaskMapFragmentContainer import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskScreen import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskScreen import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskMapFragment import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskScreen import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.multiplechoice.MultipleChoiceTaskScreen @@ -32,8 +34,10 @@ import org.groundplatform.android.ui.datacollection.tasks.number.NumberTaskScree import org.groundplatform.android.ui.datacollection.tasks.number.NumberTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.photo.PhotoTaskScreen import org.groundplatform.android.ui.datacollection.tasks.photo.PhotoTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskMapFragment import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskScreen import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskMapFragment import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskScreen import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.text.TextTaskScreen @@ -72,10 +76,9 @@ fun TaskScreenContainer(task: Task, taskPosition: TaskPosition, fragment: DataCo onButtonClicked = onButtonClicked, onOpenSettings = { fragment.requireActivity().openAppSettings() }, ) { - TaskMapFragmentContainer( - taskId = task.id, - fragmentManager = fragment.childFragmentManager, - fragmentProvider = fragment.captureLocationTaskMapFragmentProvider, + AndroidFragment( + clazz = CaptureLocationTaskMapFragment::class.java, + arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, task.id)), ) } @@ -95,10 +98,9 @@ fun TaskScreenContainer(task: Task, taskPosition: TaskPosition, fragment: DataCo loiName = loiName, shouldShowLoiNameDialog = showLoiNameDialog, ) { - TaskMapFragmentContainer( - taskId = task.id, - fragmentManager = fragment.childFragmentManager, - fragmentProvider = fragment.drawAreaTaskMapFragmentProvider, + AndroidFragment( + clazz = DrawAreaTaskMapFragment::class.java, + arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, task.id)), ) } @@ -111,10 +113,9 @@ fun TaskScreenContainer(task: Task, taskPosition: TaskPosition, fragment: DataCo loiName = loiName, shouldShowLoiNameDialog = showLoiNameDialog, ) { - TaskMapFragmentContainer( - taskId = task.id, - fragmentManager = fragment.childFragmentManager, - fragmentProvider = fragment.dropPinTaskMapFragmentProvider, + AndroidFragment( + clazz = DropPinTaskMapFragment::class.java, + arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, task.id)), ) } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskMapFragmentContainer.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskMapFragmentContainer.kt deleted file mode 100644 index 2ca5fece94..0000000000 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskMapFragmentContainer.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.components - -import android.view.View -import androidx.compose.runtime.Composable -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.os.bundleOf -import androidx.fragment.app.FragmentContainerView -import androidx.fragment.app.FragmentManager -import org.groundplatform.android.ui.common.AbstractMapContainerFragment -import org.groundplatform.android.ui.datacollection.DataCollectionFragment -import javax.inject.Provider - -/** - * A Composable that hosts a fragment-based map container for a specific task. - * - * This function bridges the gap between Jetpack Compose and the Fragment-based map implementation - * by using [AndroidView] to embed a [FragmentContainerView]. - */ -@Composable -fun TaskMapFragmentContainer( - taskId: String, - fragmentManager: FragmentManager, - fragmentProvider: Provider, -) { - AndroidView( - factory = { context -> FragmentContainerView(context).apply { id = View.generateViewId() } }, - update = { view -> - with(fragmentProvider.get()) { - arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, taskId)) - fragmentManager.beginTransaction().replace(view.id, this).commit() - } - }, - ) -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5cc009b0a0..ebf44b6476 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -93,6 +93,7 @@ androidx-core-testing = { module = "androidx.arch.core:core-testing", version.re androidx-espresso-contrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "espressoContribVersion" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoContribVersion" } androidx-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espressoContribVersion" } +androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version.ref = "fragmentVersion" } androidx-fragment-testing-manifest = { module = "androidx.fragment:fragment-testing-manifest", version.ref = "fragmentVersion" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltJetpackVersion" } androidx-hilt-navigation-fragment = { module = "androidx.hilt:hilt-navigation-fragment", version.ref = "hiltJetpackVersion" } From 9bb7b732c5aedc15cec91c59626228ad9a2abf19 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Wed, 13 May 2026 10:12:03 +0530 Subject: [PATCH 03/12] Move map fragment instantiation into individual task screens --- .../ui/datacollection/DataCollectionScreen.kt | 8 +++--- .../ui/datacollection/TaskScreenContainer.kt | 26 +++---------------- .../location/CaptureLocationTaskScreen.kt | 16 +++++++----- .../tasks/point/DropPinTaskScreen.kt | 19 +++++++++----- .../tasks/polygon/DrawAreaTaskScreen.kt | 20 +++++++++----- .../location/CaptureLocationTaskScreenTest.kt | 1 - .../tasks/point/DropPinTaskScreenTest.kt | 1 - .../tasks/polygon/DrawAreaTaskScreenTest.kt | 1 - 8 files changed, 43 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt index cad19a820b..ecaf6ed32d 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt @@ -72,10 +72,12 @@ fun DataCollectionScreen( DataCollectionContent(uiState = uiState, onCloseClicked = { viewModel.onCloseClicked() }) { readyState -> val tasks = readyState.tasks - val position = readyState.position - val currentTask = tasks[position.relativeIndex] + if (tasks.isNotEmpty()) { + val position = readyState.position + val currentTask = tasks[position.relativeIndex] - key(currentTask.id) { TaskScreenContainer(currentTask, position, fragment) } + key(currentTask.id) { TaskScreenContainer(currentTask, position, fragment) } + } } if (showExitWarningDialog) { diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt index d67bd8a1b0..47734bd6a3 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt @@ -17,15 +17,12 @@ package org.groundplatform.android.ui.datacollection import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.core.os.bundleOf -import androidx.fragment.compose.AndroidFragment import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskScreen import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskScreen import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskMapFragment import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskScreen import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.multiplechoice.MultipleChoiceTaskScreen @@ -34,10 +31,8 @@ import org.groundplatform.android.ui.datacollection.tasks.number.NumberTaskScree import org.groundplatform.android.ui.datacollection.tasks.number.NumberTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.photo.PhotoTaskScreen import org.groundplatform.android.ui.datacollection.tasks.photo.PhotoTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskMapFragment import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskScreen import org.groundplatform.android.ui.datacollection.tasks.point.DropPinTaskViewModel -import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskMapFragment import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskScreen import org.groundplatform.android.ui.datacollection.tasks.polygon.DrawAreaTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.text.TextTaskScreen @@ -75,12 +70,7 @@ fun TaskScreenContainer(task: Task, taskPosition: TaskPosition, fragment: DataCo taskPosition = taskPosition, onButtonClicked = onButtonClicked, onOpenSettings = { fragment.requireActivity().openAppSettings() }, - ) { - AndroidFragment( - clazz = CaptureLocationTaskMapFragment::class.java, - arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, task.id)), - ) - } + ) is DateTaskViewModel -> DateTaskScreen( @@ -97,12 +87,7 @@ fun TaskScreenContainer(task: Task, taskPosition: TaskPosition, fragment: DataCo onLoiNameAction = onLoiNameAction, loiName = loiName, shouldShowLoiNameDialog = showLoiNameDialog, - ) { - AndroidFragment( - clazz = DrawAreaTaskMapFragment::class.java, - arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, task.id)), - ) - } + ) is DropPinTaskViewModel -> DropPinTaskScreen( @@ -112,12 +97,7 @@ fun TaskScreenContainer(task: Task, taskPosition: TaskPosition, fragment: DataCo onLoiNameAction = onLoiNameAction, loiName = loiName, shouldShowLoiNameDialog = showLoiNameDialog, - ) { - AndroidFragment( - clazz = DropPinTaskMapFragment::class.java, - arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, task.id)), - ) - } + ) is InstructionTaskViewModel -> InstructionTaskScreen( diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt index f6a0f94c92..508c836156 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt @@ -22,10 +22,13 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf +import androidx.fragment.compose.AndroidFragment import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.groundplatform.android.R import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport import org.groundplatform.android.ui.components.ConfirmationDialog +import org.groundplatform.android.ui.datacollection.DataCollectionFragment import org.groundplatform.android.ui.datacollection.TaskPosition import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.ButtonActionState @@ -41,7 +44,6 @@ import org.groundplatform.ui.theme.AppTheme * * @param viewModel The view model for this task. * @param onOpenSettings Callback to open app settings when permission is denied. - * @param mapContent Composable for rendering the map. */ @Composable fun CaptureLocationTaskScreen( @@ -49,7 +51,6 @@ fun CaptureLocationTaskScreen( taskPosition: TaskPosition? = null, onButtonClicked: (ButtonAction) -> Unit, onOpenSettings: () -> Unit, - mapContent: @Composable () -> Unit, ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val showAccuracyCard by viewModel.showAccuracyCard.collectAsStateWithLifecycle() @@ -59,6 +60,7 @@ fun CaptureLocationTaskScreen( LaunchedEffect(Unit) { viewModel.enableLocationLock() } CaptureLocationTaskContent( + taskId = viewModel.task.id, taskLabel = viewModel.task.label, taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, @@ -68,7 +70,6 @@ fun CaptureLocationTaskScreen( onAllowLocationClicked = { viewModel.onAllowLocationClicked() }, onOpenSettings = onOpenSettings, onButtonClicked = onButtonClicked, - mapContent = mapContent, ) } @@ -88,6 +89,7 @@ fun CaptureLocationTaskScreen( */ @Composable private fun CaptureLocationTaskContent( + taskId: String, taskLabel: String, taskPosition: TaskPosition? = null, taskActionButtonsStates: List, @@ -97,7 +99,6 @@ private fun CaptureLocationTaskContent( onAllowLocationClicked: () -> Unit, onOpenSettings: () -> Unit, onButtonClicked: (ButtonAction) -> Unit, - mapContent: @Composable () -> Unit, ) { TaskScreen( taskHeader = TaskHeader(taskLabel, R.drawable.outline_pin_drop), @@ -113,7 +114,10 @@ private fun CaptureLocationTaskContent( } }, taskBody = { - mapContent() + AndroidFragment( + clazz = CaptureLocationTaskMapFragment::class.java, + arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, taskId)), + ) if (showPermissionDeniedDialog) { ConfirmationDialog( @@ -136,6 +140,7 @@ private fun CaptureLocationTaskContent( private fun CaptureLocationTaskScreenPreview() { AppTheme { CaptureLocationTaskContent( + taskId = "task_id", taskLabel = "Task for capturing current location", taskActionButtonsStates = listOf( @@ -151,7 +156,6 @@ private fun CaptureLocationTaskScreenPreview() { onAllowLocationClicked = {}, onOpenSettings = {}, onButtonClicked = {}, - mapContent = {}, ) } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt index 61e5606ee5..3afb6b2ac1 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt @@ -19,12 +19,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.tooling.preview.Preview +import androidx.core.os.bundleOf +import androidx.fragment.compose.AndroidFragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.repeatOnLifecycle import org.groundplatform.android.R import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport +import org.groundplatform.android.ui.datacollection.DataCollectionFragment import org.groundplatform.android.ui.datacollection.LoiNameAction import org.groundplatform.android.ui.datacollection.TaskPosition import org.groundplatform.android.ui.datacollection.components.ButtonAction @@ -41,7 +44,6 @@ import org.groundplatform.ui.theme.AppTheme * routing. * * @param viewModel The view model for this task. - * @param mapContent Composable for rendering the map. */ @Composable fun DropPinTaskScreen( @@ -51,7 +53,6 @@ fun DropPinTaskScreen( loiName: String, onButtonClicked: (ButtonAction) -> Unit, onLoiNameAction: (LoiNameAction) -> Unit, - mapContent: @Composable () -> Unit, ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val showInstructionsDialog by viewModel.showInstructionsDialog.collectAsStateWithLifecycle() @@ -62,6 +63,7 @@ fun DropPinTaskScreen( } DropPinTaskContent( + taskId = viewModel.task.id, taskLabel = viewModel.task.label, taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, @@ -71,7 +73,6 @@ fun DropPinTaskScreen( onButtonClicked = onButtonClicked, onLoiNameAction = onLoiNameAction, onInstructionsDismiss = { viewModel.dismissDropPinInstructions() }, - mapContent = mapContent, ) } @@ -82,10 +83,10 @@ fun DropPinTaskScreen( * @param taskPosition The position of the task in the sequence. * @param taskActionButtonsStates The states of the action buttons. * @param showInstructionsDialog Whether to show the instructions' dialog. - * @param mapContent Composable for rendering the map. */ @Composable private fun DropPinTaskContent( + taskId: String, taskLabel: String, taskPosition: TaskPosition? = null, taskActionButtonsStates: List, @@ -95,7 +96,6 @@ private fun DropPinTaskContent( onButtonClicked: (ButtonAction) -> Unit, onLoiNameAction: (LoiNameAction) -> Unit, onInstructionsDismiss: () -> Unit, - mapContent: @Composable () -> Unit, ) { TaskScreen( taskHeader = TaskHeader(taskLabel, R.drawable.outline_pin_drop), @@ -109,7 +109,12 @@ private fun DropPinTaskContent( onButtonClicked = onButtonClicked, onLoiNameAction = onLoiNameAction, onInstructionsDismiss = onInstructionsDismiss, - taskBody = mapContent, + taskBody = { + AndroidFragment( + clazz = DropPinTaskMapFragment::class.java, + arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, taskId)), + ) + }, ) } @@ -119,6 +124,7 @@ private fun DropPinTaskContent( private fun DropPinTaskScreenPreview() { AppTheme { DropPinTaskContent( + taskId = "task_id", taskLabel = "Task for dropping a pin", taskActionButtonsStates = listOf( @@ -134,7 +140,6 @@ private fun DropPinTaskScreenPreview() { onButtonClicked = {}, onLoiNameAction = {}, onInstructionsDismiss = {}, - mapContent = {}, ) } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt index 7077122488..6fd3b05706 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt @@ -23,6 +23,8 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.core.os.bundleOf +import androidx.fragment.compose.AndroidFragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -30,6 +32,7 @@ import androidx.lifecycle.repeatOnLifecycle import org.groundplatform.android.R import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport import org.groundplatform.android.ui.components.ConfirmationDialog +import org.groundplatform.android.ui.datacollection.DataCollectionFragment import org.groundplatform.android.ui.datacollection.LoiNameAction import org.groundplatform.android.ui.datacollection.TaskPosition import org.groundplatform.android.ui.datacollection.components.ButtonAction @@ -47,7 +50,6 @@ fun DrawAreaTaskScreen( loiName: String, onButtonClicked: (ButtonAction) -> Unit, onLoiNameAction: (LoiNameAction) -> Unit, - mapContent: @Composable () -> Unit, ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val showSelfIntersectionDialog by viewModel.showSelfIntersectionDialog @@ -66,6 +68,7 @@ fun DrawAreaTaskScreen( } DrawAreaTaskContent( + taskId = viewModel.task.id, taskLabel = viewModel.task.label, taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, @@ -77,7 +80,6 @@ fun DrawAreaTaskScreen( onLoiNameAction = onLoiNameAction, onInstructionsDismiss = { viewModel.dismissDrawAreaInstructions() }, onDismissSelfIntersectionDialog = { viewModel.showSelfIntersectionDialog.value = false }, - mapContent = mapContent, ) } @@ -95,10 +97,10 @@ fun DrawAreaTaskScreen( * @param onLoiNameAction Callback when user interacts with the LOI name dialog. * @param onInstructionsDismiss Callback when user dismisses the instructions dialog. * @param onDismissSelfIntersectionDialog Callback when the self-intersection dialog is dismissed. - * @param mapContent Composable for rendering the map. */ @Composable private fun DrawAreaTaskContent( + taskId: String, taskLabel: String, taskPosition: TaskPosition? = null, taskActionButtonsStates: List, @@ -110,7 +112,6 @@ private fun DrawAreaTaskContent( onLoiNameAction: (LoiNameAction) -> Unit, onInstructionsDismiss: () -> Unit, onDismissSelfIntersectionDialog: () -> Unit, - mapContent: @Composable () -> Unit, ) { TaskScreen( taskHeader = TaskHeader(taskLabel, R.drawable.outline_draw), @@ -127,7 +128,12 @@ private fun DrawAreaTaskContent( onButtonClicked = onButtonClicked, onLoiNameAction = onLoiNameAction, onInstructionsDismiss = onInstructionsDismiss, - taskBody = { mapContent() }, + taskBody = { + AndroidFragment( + clazz = DrawAreaTaskMapFragment::class.java, + arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, taskId)), + ) + }, ) if (showSelfIntersectionDialog) { @@ -147,6 +153,7 @@ private fun DrawAreaTaskContent( private fun DrawAreaTaskScreenInstructionsPreview() { AppTheme { DrawAreaTaskContent( + taskId = "task_id", taskLabel = "Task for drawing a polygon", taskActionButtonsStates = listOf( @@ -165,7 +172,6 @@ private fun DrawAreaTaskScreenInstructionsPreview() { onLoiNameAction = {}, onInstructionsDismiss = {}, onDismissSelfIntersectionDialog = {}, - mapContent = {}, ) } } @@ -176,6 +182,7 @@ private fun DrawAreaTaskScreenInstructionsPreview() { private fun DrawAreaTaskScreenSelfIntersectionPreview() { AppTheme { DrawAreaTaskContent( + taskId = "task_id", taskLabel = "Task for drawing a polygon", taskActionButtonsStates = listOf( @@ -194,7 +201,6 @@ private fun DrawAreaTaskScreenSelfIntersectionPreview() { onLoiNameAction = {}, onInstructionsDismiss = {}, onDismissSelfIntersectionDialog = {}, - mapContent = {}, ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt index 95d94716e0..6f62cf5ac1 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt @@ -248,7 +248,6 @@ class CaptureLocationTaskScreenTest { viewModel.onButtonClick(action) }, onOpenSettings = { openSettingsCalled = true }, - mapContent = { /* Dummy content */ }, ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt index 8017c3b8a2..2545bc4e36 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt @@ -186,7 +186,6 @@ class DropPinTaskScreenTest : BaseHiltTest() { viewModelToUse.onButtonClick(action) }, onLoiNameAction = {}, - mapContent = { /* Dummy content */ }, ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt index 3ffaa48be6..d9532b1ded 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt @@ -265,7 +265,6 @@ class DrawAreaTaskScreenTest : BaseHiltTest() { viewModelToUse.onButtonClick(action) }, onLoiNameAction = {}, - mapContent = { /* Dummy content */ }, ) } } From 527696d906a0aaa1f3c65ea2892a8accfa24cbe4 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Wed, 13 May 2026 10:27:27 +0530 Subject: [PATCH 04/12] Replace Fragment parameter with explicit callbacks in DataCollectionScreen and TaskScreenContainer --- .../datacollection/DataCollectionFragment.kt | 4 +++- .../ui/datacollection/DataCollectionScreen.kt | 17 ++++++++++++++--- .../ui/datacollection/TaskScreenContainer.kt | 18 ++++++++++++------ .../location/CaptureLocationTaskScreen.kt | 1 - .../datacollection/DataCollectionScreenTest.kt | 5 ++--- 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt index 454119e7dc..adfc69de01 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt @@ -28,6 +28,7 @@ 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. */ @@ -50,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 }, ) } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt index ecaf6ed32d..a847a0528c 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt @@ -47,15 +47,18 @@ import org.groundplatform.android.ui.components.ConfirmationDialog * 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. + * @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() @@ -76,7 +79,15 @@ fun DataCollectionScreen( val position = readyState.position val currentTask = tasks[position.relativeIndex] - key(currentTask.id) { TaskScreenContainer(currentTask, position, fragment) } + key(currentTask.id) { + TaskScreenContainer( + task = currentTask, + taskPosition = position, + dataCollectionViewModel = viewModel, + onOpenSettings = onOpenSettings, + onAwaitingPhotoCapture = onAwaitingPhotoCapture, + ) + } } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt index 47734bd6a3..b909df576b 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt @@ -39,7 +39,6 @@ import org.groundplatform.android.ui.datacollection.tasks.text.TextTaskScreen import org.groundplatform.android.ui.datacollection.tasks.text.TextTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.time.TimeTaskScreen import org.groundplatform.android.ui.datacollection.tasks.time.TimeTaskViewModel -import org.groundplatform.android.util.openAppSettings import org.groundplatform.domain.model.task.Task /** @@ -47,11 +46,18 @@ import org.groundplatform.domain.model.task.Task * * @param task the task to be displayed. * @param taskPosition the position of this task within the data collection flow. - * @param fragment the [DataCollectionFragment] hosting the task screens. + * @param dataCollectionViewModel the [DataCollectionViewModel] for 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 TaskScreenContainer(task: Task, taskPosition: TaskPosition, fragment: DataCollectionFragment) { - val dataCollectionViewModel = fragment.viewModel +fun TaskScreenContainer( + task: Task, + taskPosition: TaskPosition, + dataCollectionViewModel: DataCollectionViewModel, + onOpenSettings: () -> Unit, + onAwaitingPhotoCapture: (Boolean) -> Unit, +) { val taskViewModel = dataCollectionViewModel.getTaskViewModel(task.id) ?: return val loiName by dataCollectionViewModel.loiNameDraft.collectAsStateWithLifecycle() @@ -69,7 +75,7 @@ fun TaskScreenContainer(task: Task, taskPosition: TaskPosition, fragment: DataCo viewModel = taskViewModel, taskPosition = taskPosition, onButtonClicked = onButtonClicked, - onOpenSettings = { fragment.requireActivity().openAppSettings() }, + onOpenSettings = onOpenSettings, ) is DateTaskViewModel -> @@ -118,7 +124,7 @@ fun TaskScreenContainer(task: Task, taskPosition: TaskPosition, fragment: DataCo viewModel = taskViewModel, taskPosition = taskPosition, onButtonClicked = onButtonClicked, - onAwaitingPhotoCapture = { fragment.homeScreenViewModel.awaitingPhotoCapture = it }, + onAwaitingPhotoCapture = onAwaitingPhotoCapture, ) is NumberTaskViewModel -> diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt index 508c836156..fe11c6be42 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt @@ -85,7 +85,6 @@ fun CaptureLocationTaskScreen( * @param onAllowLocationClicked Callback when the allow location button is clicked in the dialog. * @param onOpenSettings Callback to open app settings. * @param onButtonClicked Callback when a button is clicked. - * @param mapContent Composable for rendering the map. */ @Composable private fun CaptureLocationTaskContent( diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt index 7cc7931816..36efe7787e 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt @@ -45,7 +45,6 @@ class DataCollectionScreenTest { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var mockViewModel: DataCollectionViewModel - @Mock private lateinit var mockFragment: DataCollectionFragment private val uiState = MutableStateFlow(DataCollectionUiState.Loading) private val showExitWarning = MutableStateFlow(false) @@ -56,16 +55,16 @@ class DataCollectionScreenTest { whenever(mockViewModel.uiState).doReturn(uiState) whenever(mockViewModel.showExitWarning).doReturn(showExitWarning) whenever(mockViewModel.uiEffects).doReturn(uiEffects) - whenever(mockFragment.viewModel).doReturn(mockViewModel) } private fun setContent(onValidationError: (Int) -> Unit = {}, onExitConfirmed: () -> Unit = {}) { composeTestRule.setContent { DataCollectionScreen( viewModel = mockViewModel, - fragment = mockFragment, onValidationError = onValidationError, onExitConfirmed = onExitConfirmed, + onOpenSettings = {}, + onAwaitingPhotoCapture = {}, ) } } From bb1edf4620eb2c847dbd5e0ca2815e4a86416419 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Wed, 13 May 2026 10:55:38 +0530 Subject: [PATCH 05/12] Refactor OpenSettings and SetAwaitingPhotoCapture to use DataCollectionViewModel UI effects --- .../ui/datacollection/DataCollectionScreen.kt | 4 +-- .../datacollection/DataCollectionViewModel.kt | 10 +++++++ .../ui/datacollection/TaskScreenContainer.kt | 6 ----- .../tasks/AbstractTaskViewModel.kt | 2 +- .../tasks/DataCollectionEvent.kt | 6 +++++ .../location/CaptureLocationTaskScreen.kt | 13 +--------- .../location/CaptureLocationTaskViewModel.kt | 2 ++ .../tasks/photo/PhotoTaskScreen.kt | 6 ----- .../tasks/photo/PhotoTaskViewModel.kt | 26 +++++++++---------- .../location/CaptureLocationTaskScreenTest.kt | 1 - .../tasks/photo/PhotoTaskScreenTest.kt | 6 +---- .../tasks/photo/PhotoTaskViewModelTest.kt | 1 - 12 files changed, 35 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt index a847a0528c..468c9cc7e7 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt @@ -67,6 +67,8 @@ 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) } } @@ -84,8 +86,6 @@ fun DataCollectionScreen( task = currentTask, taskPosition = position, dataCollectionViewModel = viewModel, - onOpenSettings = onOpenSettings, - onAwaitingPhotoCapture = onAwaitingPhotoCapture, ) } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt index db5eac8eda..b73f3c581e 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt @@ -65,6 +65,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 } @@ -145,6 +149,12 @@ internal constructor( DataCollectionEvent.NavigatePrevious -> onPreviousClicked(taskId) DataCollectionEvent.NavigateNext -> onNextClicked(taskId) DataCollectionEvent.ShowLoiDialog -> openLoiNameDialog() + DataCollectionEvent.OpenSettings -> + viewModelScope.launch { _uiEffects.send(DataCollectionUiEffect.OpenSettings) } + is DataCollectionEvent.SetAwaitingPhotoCapture -> + viewModelScope.launch { + _uiEffects.send(DataCollectionUiEffect.SetAwaitingPhotoCapture(event.awaiting)) + } } } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt index b909df576b..60253feb17 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt @@ -47,16 +47,12 @@ import org.groundplatform.domain.model.task.Task * @param task the task to be displayed. * @param taskPosition the position of this task within the data collection flow. * @param dataCollectionViewModel the [DataCollectionViewModel] for 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 TaskScreenContainer( task: Task, taskPosition: TaskPosition, dataCollectionViewModel: DataCollectionViewModel, - onOpenSettings: () -> Unit, - onAwaitingPhotoCapture: (Boolean) -> Unit, ) { val taskViewModel = dataCollectionViewModel.getTaskViewModel(task.id) ?: return @@ -75,7 +71,6 @@ fun TaskScreenContainer( viewModel = taskViewModel, taskPosition = taskPosition, onButtonClicked = onButtonClicked, - onOpenSettings = onOpenSettings, ) is DateTaskViewModel -> @@ -124,7 +119,6 @@ fun TaskScreenContainer( viewModel = taskViewModel, taskPosition = taskPosition, onButtonClicked = onButtonClicked, - onAwaitingPhotoCapture = onAwaitingPhotoCapture, ) is NumberTaskViewModel -> diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskViewModel.kt index 9373f4d3c6..a44834d0d9 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskViewModel.kt @@ -55,7 +55,7 @@ abstract class AbstractTaskViewModel internal constructor() : AbstractViewModel( lateinit var surveyId: String lateinit var task: Task private lateinit var taskPositionInterface: TaskPositionInterface - private lateinit var eventReporter: (DataCollectionEvent) -> Unit + protected lateinit var eventReporter: (DataCollectionEvent) -> Unit fun dismissInstructions() { _showInstructionsDialog.value = false diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/DataCollectionEvent.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/DataCollectionEvent.kt index 676c966fdd..2eda48a731 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/DataCollectionEvent.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/DataCollectionEvent.kt @@ -23,6 +23,12 @@ sealed interface DataCollectionEvent { /** Navigate to the next task (includes validation and submission check). */ data object NavigateNext : DataCollectionEvent + /** Open the app settings. */ + data object OpenSettings : DataCollectionEvent + + /** Notify whether the app is awaiting a photo capture. */ + data class SetAwaitingPhotoCapture(val awaiting: Boolean) : DataCollectionEvent + /** Show the LOI Name Dialog before proceeding. */ data object ShowLoiDialog : DataCollectionEvent } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt index fe11c6be42..a2cdb7c361 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt @@ -41,16 +41,12 @@ import org.groundplatform.ui.theme.AppTheme * * This is the stateful wrapper that collects state from [CaptureLocationTaskViewModel] and handles * event routing. - * - * @param viewModel The view model for this task. - * @param onOpenSettings Callback to open app settings when permission is denied. */ @Composable fun CaptureLocationTaskScreen( viewModel: CaptureLocationTaskViewModel, taskPosition: TaskPosition? = null, onButtonClicked: (ButtonAction) -> Unit, - onOpenSettings: () -> Unit, ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val showAccuracyCard by viewModel.showAccuracyCard.collectAsStateWithLifecycle() @@ -68,7 +64,6 @@ fun CaptureLocationTaskScreen( showPermissionDeniedDialog = showPermissionDeniedDialog, onDismissAccuracyCard = { viewModel.dismissAccuracyCard() }, onAllowLocationClicked = { viewModel.onAllowLocationClicked() }, - onOpenSettings = onOpenSettings, onButtonClicked = onButtonClicked, ) } @@ -83,7 +78,6 @@ fun CaptureLocationTaskScreen( * @param showPermissionDeniedDialog Whether to show the permission denied dialog. * @param onDismissAccuracyCard Callback when the accuracy card is dismissed. * @param onAllowLocationClicked Callback when the allow location button is clicked in the dialog. - * @param onOpenSettings Callback to open app settings. * @param onButtonClicked Callback when a button is clicked. */ @Composable @@ -96,7 +90,6 @@ private fun CaptureLocationTaskContent( showPermissionDeniedDialog: Boolean, onDismissAccuracyCard: () -> Unit, onAllowLocationClicked: () -> Unit, - onOpenSettings: () -> Unit, onButtonClicked: (ButtonAction) -> Unit, ) { TaskScreen( @@ -123,10 +116,7 @@ private fun CaptureLocationTaskContent( title = R.string.allow_location_title, description = R.string.allow_location_description, confirmButtonText = R.string.allow_location_confirmation, - onConfirmClicked = { - onAllowLocationClicked() - onOpenSettings() - }, + onConfirmClicked = onAllowLocationClicked, ) } }, @@ -153,7 +143,6 @@ private fun CaptureLocationTaskScreenPreview() { showPermissionDeniedDialog = true, onDismissAccuracyCard = {}, onAllowLocationClicked = {}, - onOpenSettings = {}, onButtonClicked = {}, ) } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt index 51185823ff..d80b009378 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt @@ -31,6 +31,7 @@ import org.groundplatform.android.common.Constants.ACCURACY_THRESHOLD_IN_M import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.AbstractMapTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.DataCollectionEvent import org.groundplatform.android.ui.datacollection.tasks.LocationLockEnabledState import org.groundplatform.android.ui.map.gms.getAccuracyOrNull import org.groundplatform.android.ui.map.gms.getAltitudeOrNull @@ -103,6 +104,7 @@ class CaptureLocationTaskViewModel @Inject constructor() : AbstractMapTaskViewMo fun onAllowLocationClicked() { dismissPermissionDeniedDialog() + eventReporter(DataCollectionEvent.OpenSettings) } fun updateLocation(location: Location) { diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreen.kt index c5a62942e9..06fb63a5fb 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreen.kt @@ -32,7 +32,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -60,17 +59,12 @@ fun PhotoTaskScreen( viewModel: PhotoTaskViewModel, taskPosition: TaskPosition? = null, onButtonClicked: (ButtonAction) -> Unit, - onAwaitingPhotoCapture: (Boolean) -> Unit, ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val uri by viewModel.uri.collectAsStateWithLifecycle(Uri.EMPTY) - val isAwaiting by viewModel.isAwaitingPhotoCapture.collectAsStateWithLifecycle() var activeError by rememberSaveable { mutableStateOf(null) } - val currentOnAwaiting by rememberUpdatedState(onAwaitingPhotoCapture) - LaunchedEffect(isAwaiting) { currentOnAwaiting(isAwaiting) } - val capturePhotoLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { result -> viewModel.onCaptureResult(result) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt index c9ab206321..e5413a2b75 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt @@ -22,14 +22,10 @@ import android.os.Build import android.os.Build.VERSION_CODES import androidx.core.net.toUri import androidx.lifecycle.viewModelScope -import java.io.File -import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn @@ -39,11 +35,14 @@ import org.groundplatform.android.system.PermissionDeniedException import org.groundplatform.android.system.PermissionsManager import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel +import org.groundplatform.android.ui.datacollection.tasks.DataCollectionEvent import org.groundplatform.domain.model.submission.TaskData import org.groundplatform.domain.model.submission.isNotNullOrEmpty import org.groundplatform.domain.model.task.PhotoTaskData import org.groundplatform.domain.repository.UserMediaRepositoryInterface import timber.log.Timber +import java.io.File +import javax.inject.Inject class PhotoTaskViewModel @Inject @@ -54,9 +53,6 @@ constructor( private var tempPhotoFilePath: String? = null - private val _isAwaitingPhotoCapture = MutableStateFlow(false) - val isAwaitingPhotoCapture: StateFlow = _isAwaitingPhotoCapture.asStateFlow() - private val _events = Channel() val events: Flow = _events.receiveAsFlow() @@ -72,8 +68,7 @@ constructor( .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Uri.EMPTY) fun onTakePhoto() { - if (_isAwaitingPhotoCapture.value) return - _isAwaitingPhotoCapture.value = true + eventReporter(DataCollectionEvent.SetAwaitingPhotoCapture(true)) viewModelScope.launch { obtainCapturePhotoPermissions { launchPhotoCapture() } } } @@ -85,8 +80,7 @@ constructor( permissionsManager.obtainPermission(CAMERA) onPermissionsGranted() } catch (_: PermissionDeniedException) { - _isAwaitingPhotoCapture.value = false - _events.send(PhotoTaskEvent.ShowError(PhotoTaskError.PERMISSION_DENIED)) + onLaunchPhotoCaptureError(PhotoTaskError.PERMISSION_DENIED) } } @@ -98,12 +92,16 @@ constructor( _events.send(PhotoTaskEvent.LaunchCamera(mediaUri.toUri())) Timber.d("Capture photo intent sent") } catch (e: IllegalArgumentException) { - _isAwaitingPhotoCapture.value = false - _events.send(PhotoTaskEvent.ShowError(PhotoTaskError.CAMERA_LAUNCH_FAILED)) Timber.e(e, "Error launching photo capture") + onLaunchPhotoCaptureError(PhotoTaskError.CAMERA_LAUNCH_FAILED) } } + private suspend fun onLaunchPhotoCaptureError(errorType: PhotoTaskError) { + eventReporter(DataCollectionEvent.SetAwaitingPhotoCapture(false)) + _events.send(PhotoTaskEvent.ShowError(errorType)) + } + override fun getButtonStates(taskData: TaskData?): List = listOf( getPreviousButton(), @@ -113,13 +111,13 @@ constructor( ) fun onCaptureResult(result: Boolean) { + eventReporter(DataCollectionEvent.SetAwaitingPhotoCapture(false)) val filePath = tempPhotoFilePath tempPhotoFilePath = null // Clear to avoid reusing stale path viewModelScope.launch { if (result && filePath != null) { finalizePhotoCapture(File(filePath)) } - _isAwaitingPhotoCapture.value = false } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt index 6f62cf5ac1..5a5ae8a961 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt @@ -247,7 +247,6 @@ class CaptureLocationTaskScreenTest { lastScreenAction = action viewModel.onButtonClick(action) }, - onOpenSettings = { openSettingsCalled = true }, ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt index d958f290ac..78b67f2656 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt @@ -89,11 +89,7 @@ class PhotoTaskScreenTest { ) composeTestRule.setContent { - PhotoTaskScreen( - viewModel = viewModel, - onButtonClicked = { lastButtonAction = it }, - onAwaitingPhotoCapture = {}, - ) + PhotoTaskScreen(viewModel = viewModel, onButtonClicked = { lastButtonAction = it }) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModelTest.kt index 66b390cb81..a1ceec9099 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModelTest.kt @@ -97,7 +97,6 @@ class PhotoTaskViewModelTest : BaseHiltTest() { } verify(userMediaRepository).addImageToGallery("/path/to/file.jpg", "file.jpg") - assertThat(viewModel.isAwaitingPhotoCapture.value).isFalse() } @Test From e187aa931c51037efbb29935682b3fe5fec42ca7 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Wed, 13 May 2026 11:03:15 +0530 Subject: [PATCH 06/12] Remove onButtonClicked parameter from TaskScreens and handle clicks via ViewModels --- .../ui/datacollection/TaskScreenContainer.kt | 67 ++++++------------- .../tasks/date/DateTaskScreen.kt | 9 +-- .../instruction/InstructionTaskScreen.kt | 9 +-- .../location/CaptureLocationTaskScreen.kt | 3 +- .../MultipleChoiceTaskScreen.kt | 4 +- .../tasks/number/NumberTaskScreen.kt | 9 +-- .../tasks/photo/PhotoTaskScreen.kt | 9 +-- .../tasks/point/DropPinTaskScreen.kt | 3 +- .../tasks/polygon/DrawAreaTaskScreen.kt | 3 +- .../tasks/text/TextTaskScreen.kt | 9 +-- .../tasks/time/TimeTaskScreen.kt | 15 ++--- .../tasks/date/DateTaskScreenTest.kt | 2 +- .../instruction/InstructionTaskScreenTest.kt | 1 - .../location/CaptureLocationTaskScreenTest.kt | 4 -- .../MultipleChoiceTaskScreenTest.kt | 1 - .../tasks/number/NumberTaskScreenTest.kt | 1 - .../tasks/photo/PhotoTaskScreenTest.kt | 2 +- .../tasks/point/DropPinTaskScreenTest.kt | 4 -- .../tasks/polygon/DrawAreaTaskScreenTest.kt | 4 -- .../tasks/text/TextTaskScreenTest.kt | 1 - .../tasks/time/TimeTaskScreenTest.kt | 1 - 21 files changed, 42 insertions(+), 119 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt index 60253feb17..b0a99293a9 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt @@ -18,7 +18,6 @@ package org.groundplatform.android.ui.datacollection import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskScreen import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskScreen @@ -59,88 +58,64 @@ fun TaskScreenContainer( val loiName by dataCollectionViewModel.loiNameDraft.collectAsStateWithLifecycle() val showLoiNameDialog by dataCollectionViewModel.loiNameDialogOpen.collectAsStateWithLifecycle() - val onButtonClicked = { action: ButtonAction -> taskViewModel.onButtonClick(action) } - val onLoiNameAction = { action: LoiNameAction -> dataCollectionViewModel.handleLoiNameAction(action, task.id) } - when (taskViewModel) { - is CaptureLocationTaskViewModel -> + when (task.type) { + Task.Type.CAPTURE_LOCATION -> CaptureLocationTaskScreen( - viewModel = taskViewModel, + viewModel = taskViewModel as CaptureLocationTaskViewModel, taskPosition = taskPosition, - onButtonClicked = onButtonClicked, ) - is DateTaskViewModel -> - DateTaskScreen( - viewModel = taskViewModel, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - ) + Task.Type.DATE -> + DateTaskScreen(viewModel = taskViewModel as DateTaskViewModel, taskPosition = taskPosition) - is DrawAreaTaskViewModel -> + Task.Type.DRAW_AREA -> DrawAreaTaskScreen( - viewModel = taskViewModel, + viewModel = taskViewModel as DrawAreaTaskViewModel, taskPosition = taskPosition, - onButtonClicked = onButtonClicked, onLoiNameAction = onLoiNameAction, loiName = loiName, shouldShowLoiNameDialog = showLoiNameDialog, ) - is DropPinTaskViewModel -> + Task.Type.DROP_PIN -> DropPinTaskScreen( - viewModel = taskViewModel, + viewModel = taskViewModel as DropPinTaskViewModel, taskPosition = taskPosition, - onButtonClicked = onButtonClicked, onLoiNameAction = onLoiNameAction, loiName = loiName, shouldShowLoiNameDialog = showLoiNameDialog, ) - is InstructionTaskViewModel -> + Task.Type.INSTRUCTIONS -> InstructionTaskScreen( - viewModel = taskViewModel, + viewModel = taskViewModel as InstructionTaskViewModel, taskPosition = taskPosition, - onButtonClicked = onButtonClicked, ) - is MultipleChoiceTaskViewModel -> + Task.Type.MULTIPLE_CHOICE -> MultipleChoiceTaskScreen( - viewModel = taskViewModel, + viewModel = taskViewModel as MultipleChoiceTaskViewModel, taskPosition = taskPosition, - onButtonClicked = onButtonClicked, ) - is PhotoTaskViewModel -> - PhotoTaskScreen( - viewModel = taskViewModel, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - ) + Task.Type.PHOTO -> + PhotoTaskScreen(viewModel = taskViewModel as PhotoTaskViewModel, taskPosition = taskPosition) - is NumberTaskViewModel -> + Task.Type.NUMBER -> NumberTaskScreen( - viewModel = taskViewModel, + viewModel = taskViewModel as NumberTaskViewModel, taskPosition = taskPosition, - onButtonClicked = onButtonClicked, ) - is TextTaskViewModel -> - TextTaskScreen( - viewModel = taskViewModel, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - ) + Task.Type.TEXT -> + TextTaskScreen(viewModel = taskViewModel as TextTaskViewModel, taskPosition = taskPosition) - is TimeTaskViewModel -> - TimeTaskScreen( - viewModel = taskViewModel, - taskPosition = taskPosition, - onButtonClicked = onButtonClicked, - ) + Task.Type.TIME -> + TimeTaskScreen(viewModel = taskViewModel as TimeTaskViewModel, taskPosition = taskPosition) else -> error("Unsupported task type: ${task.type}") } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreen.kt index cbf36ed141..5205ce0726 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreen.kt @@ -39,7 +39,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.groundplatform.android.R import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport import org.groundplatform.android.ui.datacollection.TaskPosition -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.TaskHeader import org.groundplatform.android.ui.datacollection.tasks.TaskScreen import org.groundplatform.domain.model.submission.DateTimeTaskData @@ -51,11 +50,7 @@ import java.util.Date const val DATE_PICKER_TEST_TAG: String = "date picker test tag" @Composable -fun DateTaskScreen( - viewModel: DateTaskViewModel, - taskPosition: TaskPosition? = null, - onButtonClicked: (ButtonAction) -> Unit, -) { +fun DateTaskScreen(viewModel: DateTaskViewModel, taskPosition: TaskPosition? = null) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val taskData by viewModel.taskTaskData.collectAsStateWithLifecycle() @@ -64,7 +59,7 @@ fun DateTaskScreen( TaskHeader(label = viewModel.task.label, iconResId = R.drawable.ic_question_answer), taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, - onButtonClicked = onButtonClicked, + onButtonClicked = { viewModel.onButtonClick(it) }, taskBody = { DateTaskContent( taskData as? DateTimeTaskData, diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreen.kt index 00986ad743..9082cf74f9 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreen.kt @@ -32,23 +32,18 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport import org.groundplatform.android.ui.datacollection.TaskPosition -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.tasks.TaskScreen import org.groundplatform.ui.theme.sizes @Composable -fun InstructionTaskScreen( - viewModel: InstructionTaskViewModel, - taskPosition: TaskPosition? = null, - onButtonClicked: (ButtonAction) -> Unit, -) { +fun InstructionTaskScreen(viewModel: InstructionTaskViewModel, taskPosition: TaskPosition? = null) { val taskActionButtonStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() TaskScreen( taskHeader = null, taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonStates, - onButtonClicked = onButtonClicked, + onButtonClicked = { viewModel.onButtonClick(it) }, taskBody = { InstructionTaskContent(viewModel.task.label) }, ) } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt index a2cdb7c361..edba336e5c 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt @@ -46,7 +46,6 @@ import org.groundplatform.ui.theme.AppTheme fun CaptureLocationTaskScreen( viewModel: CaptureLocationTaskViewModel, taskPosition: TaskPosition? = null, - onButtonClicked: (ButtonAction) -> Unit, ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val showAccuracyCard by viewModel.showAccuracyCard.collectAsStateWithLifecycle() @@ -64,7 +63,7 @@ fun CaptureLocationTaskScreen( showPermissionDeniedDialog = showPermissionDeniedDialog, onDismissAccuracyCard = { viewModel.dismissAccuracyCard() }, onAllowLocationClicked = { viewModel.onAllowLocationClicked() }, - onButtonClicked = onButtonClicked, + onButtonClicked = { viewModel.onButtonClick(it) }, ) } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskScreen.kt index 3fdf8a4880..5050f62a38 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskScreen.kt @@ -32,7 +32,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.groundplatform.android.R import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport import org.groundplatform.android.ui.datacollection.TaskPosition -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.TaskHeader import org.groundplatform.android.ui.datacollection.tasks.TaskScreen import org.groundplatform.domain.model.task.MultipleChoice.Cardinality @@ -46,7 +45,6 @@ import org.groundplatform.ui.theme.sizes fun MultipleChoiceTaskScreen( viewModel: MultipleChoiceTaskViewModel, taskPosition: TaskPosition? = null, - onButtonClicked: (ButtonAction) -> Unit, ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val list by viewModel.items.collectAsStateWithLifecycle() @@ -56,7 +54,7 @@ fun MultipleChoiceTaskScreen( TaskHeader(label = viewModel.task.label, iconResId = R.drawable.ic_question_answer), taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, - onButtonClicked = onButtonClicked, + onButtonClicked = { viewModel.onButtonClick(it) }, taskBody = { MultipleChoiceTaskContent( list = list, diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskScreen.kt index 7c2fe22eeb..d882a51e16 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskScreen.kt @@ -28,7 +28,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.groundplatform.android.R import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport import org.groundplatform.android.ui.datacollection.TaskPosition -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.TaskHeader import org.groundplatform.android.ui.datacollection.components.TextTaskInput import org.groundplatform.android.ui.datacollection.tasks.TaskScreen @@ -39,11 +38,7 @@ import org.groundplatform.ui.theme.sizes const val INPUT_NUMBER_TEST_TAG: String = "number task input test tag" @Composable -fun NumberTaskScreen( - viewModel: NumberTaskViewModel, - taskPosition: TaskPosition? = null, - onButtonClicked: (ButtonAction) -> Unit, -) { +fun NumberTaskScreen(viewModel: NumberTaskViewModel, taskPosition: TaskPosition? = null) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val userResponse by viewModel.responseText.observeAsState("") @@ -52,7 +47,7 @@ fun NumberTaskScreen( TaskHeader(label = viewModel.task.label, iconResId = R.drawable.ic_question_answer), taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, - onButtonClicked = onButtonClicked, + onButtonClicked = { viewModel.onButtonClick(it) }, taskBody = { NumberTaskContent( userResponse = userResponse, diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreen.kt index 06fb63a5fb..51ec3d30cd 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreen.kt @@ -48,18 +48,13 @@ import androidx.lifecycle.flowWithLifecycle import org.groundplatform.android.R import org.groundplatform.android.ui.components.ConfirmationDialog import org.groundplatform.android.ui.datacollection.TaskPosition -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.TaskHeader import org.groundplatform.android.ui.datacollection.components.UriImage import org.groundplatform.android.ui.datacollection.tasks.TaskScreen import org.groundplatform.ui.theme.sizes @Composable -fun PhotoTaskScreen( - viewModel: PhotoTaskViewModel, - taskPosition: TaskPosition? = null, - onButtonClicked: (ButtonAction) -> Unit, -) { +fun PhotoTaskScreen(viewModel: PhotoTaskViewModel, taskPosition: TaskPosition? = null) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val uri by viewModel.uri.collectAsStateWithLifecycle(Uri.EMPTY) @@ -86,7 +81,7 @@ fun PhotoTaskScreen( TaskHeader(label = viewModel.task.label, iconResId = R.drawable.ic_question_answer), taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, - onButtonClicked = onButtonClicked, + onButtonClicked = { viewModel.onButtonClick(it) }, taskBody = { PhotoTaskContent( uri = uri, diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt index 3afb6b2ac1..ac409b3cfd 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt @@ -51,7 +51,6 @@ fun DropPinTaskScreen( taskPosition: TaskPosition? = null, shouldShowLoiNameDialog: Boolean, loiName: String, - onButtonClicked: (ButtonAction) -> Unit, onLoiNameAction: (LoiNameAction) -> Unit, ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() @@ -70,7 +69,7 @@ fun DropPinTaskScreen( showInstructionsDialog = showInstructionsDialog, shouldShowLoiNameDialog = shouldShowLoiNameDialog, loiName = loiName, - onButtonClicked = onButtonClicked, + onButtonClicked = { viewModel.onButtonClick(it) }, onLoiNameAction = onLoiNameAction, onInstructionsDismiss = { viewModel.dismissDropPinInstructions() }, ) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt index 6fd3b05706..72606a5a01 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt @@ -48,7 +48,6 @@ fun DrawAreaTaskScreen( taskPosition: TaskPosition? = null, shouldShowLoiNameDialog: Boolean, loiName: String, - onButtonClicked: (ButtonAction) -> Unit, onLoiNameAction: (LoiNameAction) -> Unit, ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() @@ -76,7 +75,7 @@ fun DrawAreaTaskScreen( showSelfIntersectionDialog = showSelfIntersectionDialog, shouldShowLoiNameDialog = shouldShowLoiNameDialog, loiName = loiName, - onButtonClicked = onButtonClicked, + onButtonClicked = { viewModel.onButtonClick(it) }, onLoiNameAction = onLoiNameAction, onInstructionsDismiss = { viewModel.dismissDrawAreaInstructions() }, onDismissSelfIntersectionDialog = { viewModel.showSelfIntersectionDialog.value = false }, diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreen.kt index 88618285d1..462d1e1031 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreen.kt @@ -27,7 +27,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.groundplatform.android.R import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport import org.groundplatform.android.ui.datacollection.TaskPosition -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.TaskHeader import org.groundplatform.android.ui.datacollection.components.TextTaskInput import org.groundplatform.android.ui.datacollection.tasks.TaskScreen @@ -38,11 +37,7 @@ import org.groundplatform.ui.theme.sizes const val INPUT_TEXT_TEST_TAG: String = "text task input test tag" @Composable -fun TextTaskScreen( - viewModel: TextTaskViewModel, - taskPosition: TaskPosition? = null, - onButtonClicked: (ButtonAction) -> Unit, -) { +fun TextTaskScreen(viewModel: TextTaskViewModel, taskPosition: TaskPosition? = null) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val responseText by viewModel.responseText.observeAsState("") @@ -51,7 +46,7 @@ fun TextTaskScreen( TaskHeader(label = viewModel.task.label, iconResId = R.drawable.ic_question_answer), taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, - onButtonClicked = onButtonClicked, + onButtonClicked = { viewModel.onButtonClick(it) }, taskBody = { TextTaskContent( responseText = responseText, diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreen.kt index 4b3e0ee6ee..cb949e185b 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreen.kt @@ -36,27 +36,22 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date import org.groundplatform.android.R import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport import org.groundplatform.android.ui.datacollection.TaskPosition -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.TaskHeader import org.groundplatform.android.ui.datacollection.tasks.TaskScreen import org.groundplatform.domain.model.submission.DateTimeTaskData import org.groundplatform.ui.theme.AppTheme import org.groundplatform.ui.theme.sizes +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date const val TIME_PICKER_TEST_TAG: String = "time picker test tag" @Composable -fun TimeTaskScreen( - viewModel: TimeTaskViewModel, - taskPosition: TaskPosition? = null, - onButtonClicked: (ButtonAction) -> Unit, -) { +fun TimeTaskScreen(viewModel: TimeTaskViewModel, taskPosition: TaskPosition? = null) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val taskData by viewModel.taskTaskData.collectAsStateWithLifecycle() @@ -65,7 +60,7 @@ fun TimeTaskScreen( TaskHeader(label = viewModel.task.label, iconResId = R.drawable.ic_question_answer), taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, - onButtonClicked = onButtonClicked, + onButtonClicked = { viewModel.onButtonClick(it) }, taskBody = { TimeTaskContent( taskData as? DateTimeTaskData, diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreenTest.kt index 8e98ff98b2..33326f41b0 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreenTest.kt @@ -66,7 +66,7 @@ class DateTaskScreenTest { ) composeTestRule.setContent { - DateTaskScreen(viewModel = viewModel, onButtonClicked = { lastButtonAction = it }) + DateTaskScreen(viewModel = viewModel) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreenTest.kt index 98bd1fec4b..66cd88dc40 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreenTest.kt @@ -69,7 +69,6 @@ class InstructionTaskScreenTest { composeTestRule.setContent { InstructionTaskScreen( viewModel = viewModel, - onButtonClicked = { lastButtonAction = it }, ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt index 5a5ae8a961..a20a16880a 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt @@ -243,10 +243,6 @@ class CaptureLocationTaskScreenTest { composeTestRule.setContent { CaptureLocationTaskScreen( viewModel = viewModel, - onButtonClicked = { action -> - lastScreenAction = action - viewModel.onButtonClick(action) - }, ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskScreenTest.kt index 6b5f6d7f9c..65df2b3fe9 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskScreenTest.kt @@ -75,7 +75,6 @@ class MultipleChoiceTaskScreenTest { composeTestRule.setContent { MultipleChoiceTaskScreen( viewModel = viewModel, - onButtonClicked = { lastButtonAction = it }, ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskScreenTest.kt index 7ddc8270ca..27bc2ba741 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskScreenTest.kt @@ -67,7 +67,6 @@ class NumberTaskScreenTest { composeTestRule.setContent { NumberTaskScreen( viewModel = viewModel, - onButtonClicked = { lastButtonAction = it }, ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt index 78b67f2656..b661bee0ff 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt @@ -89,7 +89,7 @@ class PhotoTaskScreenTest { ) composeTestRule.setContent { - PhotoTaskScreen(viewModel = viewModel, onButtonClicked = { lastButtonAction = it }) + PhotoTaskScreen(viewModel = viewModel) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt index 2545bc4e36..f4c37696eb 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt @@ -181,10 +181,6 @@ class DropPinTaskScreenTest : BaseHiltTest() { viewModel = viewModelToUse, shouldShowLoiNameDialog = false, loiName = "", - onButtonClicked = { action -> - lastButtonAction = action - viewModelToUse.onButtonClick(action) - }, onLoiNameAction = {}, ) } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt index d9532b1ded..d4d9e1868f 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt @@ -260,10 +260,6 @@ class DrawAreaTaskScreenTest : BaseHiltTest() { viewModel = viewModelToUse, shouldShowLoiNameDialog = false, loiName = "", - onButtonClicked = { action -> - lastButtonAction = action - viewModelToUse.onButtonClick(action) - }, onLoiNameAction = {}, ) } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreenTest.kt index dd6df74260..10ed5dea80 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreenTest.kt @@ -68,7 +68,6 @@ class TextTaskScreenTest { composeTestRule.setContent { TextTaskScreen( viewModel = viewModel, - onButtonClicked = { lastButtonAction = it }, ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreenTest.kt index 10bc27ebc0..8e97869d3c 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreenTest.kt @@ -68,7 +68,6 @@ class TimeTaskScreenTest { composeTestRule.setContent { TimeTaskScreen( viewModel = viewModel, - onButtonClicked = { lastButtonAction = it }, ) } } From dc299de38815e4233e4b5490ceb94c401e3349e8 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Wed, 13 May 2026 11:19:36 +0530 Subject: [PATCH 07/12] Refactor TaskScreenContainer to use hoisted state and move to tasks package --- .../ui/datacollection/DataCollectionScreen.kt | 23 +++++++++++---- .../{ => tasks}/TaskScreenContainer.kt | 29 +++++++++---------- 2 files changed, 31 insertions(+), 21 deletions(-) rename app/src/main/java/org/groundplatform/android/ui/datacollection/{ => tasks}/TaskScreenContainer.kt (84%) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt index 468c9cc7e7..a9fcb9bf29 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt @@ -42,6 +42,7 @@ 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. @@ -82,11 +83,23 @@ fun DataCollectionScreen( val currentTask = tasks[position.relativeIndex] key(currentTask.id) { - TaskScreenContainer( - task = currentTask, - taskPosition = position, - dataCollectionViewModel = viewModel, - ) + 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) }, + ) + } } } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/TaskScreenContainer.kt similarity index 84% rename from app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt rename to app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/TaskScreenContainer.kt index b0a99293a9..7e33641a21 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/TaskScreenContainer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/TaskScreenContainer.kt @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.ui.datacollection +package org.groundplatform.android.ui.datacollection.tasks import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.groundplatform.android.ui.datacollection.LoiNameAction +import org.groundplatform.android.ui.datacollection.TaskPosition import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskScreen import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskScreen @@ -44,24 +44,21 @@ import org.groundplatform.domain.model.task.Task * A container that renders the appropriate task screen based on the provided [task] type. * * @param task the task to be displayed. + * @param taskViewModel the [AbstractTaskViewModel] for the task. * @param taskPosition the position of this task within the data collection flow. - * @param dataCollectionViewModel the [DataCollectionViewModel] for the data collection flow. + * @param loiName the draft LOI name. + * @param shouldShowLoiNameDialog whether the LOI name dialog should be shown. + * @param onLoiNameAction callback for LOI name actions. */ @Composable fun TaskScreenContainer( task: Task, + taskViewModel: AbstractTaskViewModel, taskPosition: TaskPosition, - dataCollectionViewModel: DataCollectionViewModel, + loiName: String = "", + shouldShowLoiNameDialog: Boolean = false, + onLoiNameAction: (LoiNameAction) -> Unit = {}, ) { - val taskViewModel = dataCollectionViewModel.getTaskViewModel(task.id) ?: return - - val loiName by dataCollectionViewModel.loiNameDraft.collectAsStateWithLifecycle() - val showLoiNameDialog by dataCollectionViewModel.loiNameDialogOpen.collectAsStateWithLifecycle() - - val onLoiNameAction = { action: LoiNameAction -> - dataCollectionViewModel.handleLoiNameAction(action, task.id) - } - when (task.type) { Task.Type.CAPTURE_LOCATION -> CaptureLocationTaskScreen( @@ -78,7 +75,7 @@ fun TaskScreenContainer( taskPosition = taskPosition, onLoiNameAction = onLoiNameAction, loiName = loiName, - shouldShowLoiNameDialog = showLoiNameDialog, + shouldShowLoiNameDialog = shouldShowLoiNameDialog, ) Task.Type.DROP_PIN -> @@ -87,7 +84,7 @@ fun TaskScreenContainer( taskPosition = taskPosition, onLoiNameAction = onLoiNameAction, loiName = loiName, - shouldShowLoiNameDialog = showLoiNameDialog, + shouldShowLoiNameDialog = shouldShowLoiNameDialog, ) Task.Type.INSTRUCTIONS -> From df95c27ab03672643e000b1c85e74effcf49bbe9 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Wed, 13 May 2026 12:03:28 +0530 Subject: [PATCH 08/12] Fix failing tests --- .../tasks/location/CaptureLocationTaskScreen.kt | 14 ++++++++++---- .../tasks/point/DropPinTaskScreen.kt | 14 ++++++++++---- .../tasks/polygon/DrawAreaTaskScreen.kt | 15 +++++++++++---- .../tasks/date/DateTaskScreenTest.kt | 13 ++++++------- .../instruction/InstructionTaskScreenTest.kt | 11 ++++++----- .../location/CaptureLocationTaskScreenTest.kt | 12 ++++++------ .../tasks/photo/PhotoTaskScreenTest.kt | 9 +++++---- .../tasks/point/DropPinTaskScreenTest.kt | 2 ++ .../tasks/polygon/DrawAreaTaskScreenTest.kt | 1 + .../tasks/text/TextTaskScreenTest.kt | 9 +++++---- .../tasks/time/TimeTaskScreenTest.kt | 9 +++++---- 11 files changed, 67 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt index edba336e5c..3be28a933f 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt @@ -46,6 +46,12 @@ import org.groundplatform.ui.theme.AppTheme fun CaptureLocationTaskScreen( viewModel: CaptureLocationTaskViewModel, taskPosition: TaskPosition? = null, + mapContent: @Composable () -> Unit = { + AndroidFragment( + clazz = CaptureLocationTaskMapFragment::class.java, + arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, viewModel.task.id)), + ) + } ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val showAccuracyCard by viewModel.showAccuracyCard.collectAsStateWithLifecycle() @@ -64,6 +70,7 @@ fun CaptureLocationTaskScreen( onDismissAccuracyCard = { viewModel.dismissAccuracyCard() }, onAllowLocationClicked = { viewModel.onAllowLocationClicked() }, onButtonClicked = { viewModel.onButtonClick(it) }, + mapContent = mapContent, ) } @@ -90,6 +97,7 @@ private fun CaptureLocationTaskContent( onDismissAccuracyCard: () -> Unit, onAllowLocationClicked: () -> Unit, onButtonClicked: (ButtonAction) -> Unit, + mapContent: @Composable () -> Unit, ) { TaskScreen( taskHeader = TaskHeader(taskLabel, R.drawable.outline_pin_drop), @@ -105,10 +113,7 @@ private fun CaptureLocationTaskContent( } }, taskBody = { - AndroidFragment( - clazz = CaptureLocationTaskMapFragment::class.java, - arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, taskId)), - ) + mapContent() if (showPermissionDeniedDialog) { ConfirmationDialog( @@ -143,6 +148,7 @@ private fun CaptureLocationTaskScreenPreview() { onDismissAccuracyCard = {}, onAllowLocationClicked = {}, onButtonClicked = {}, + mapContent = {}, ) } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt index ac409b3cfd..a97d26e734 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt @@ -52,6 +52,12 @@ fun DropPinTaskScreen( shouldShowLoiNameDialog: Boolean, loiName: String, onLoiNameAction: (LoiNameAction) -> Unit, + mapContent: @Composable () -> Unit = { + AndroidFragment( + clazz = DropPinTaskMapFragment::class.java, + arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, viewModel.task.id)), + ) + } ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val showInstructionsDialog by viewModel.showInstructionsDialog.collectAsStateWithLifecycle() @@ -72,6 +78,7 @@ fun DropPinTaskScreen( onButtonClicked = { viewModel.onButtonClick(it) }, onLoiNameAction = onLoiNameAction, onInstructionsDismiss = { viewModel.dismissDropPinInstructions() }, + mapContent = mapContent, ) } @@ -95,6 +102,7 @@ private fun DropPinTaskContent( onButtonClicked: (ButtonAction) -> Unit, onLoiNameAction: (LoiNameAction) -> Unit, onInstructionsDismiss: () -> Unit, + mapContent: @Composable () -> Unit, ) { TaskScreen( taskHeader = TaskHeader(taskLabel, R.drawable.outline_pin_drop), @@ -109,10 +117,7 @@ private fun DropPinTaskContent( onLoiNameAction = onLoiNameAction, onInstructionsDismiss = onInstructionsDismiss, taskBody = { - AndroidFragment( - clazz = DropPinTaskMapFragment::class.java, - arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, taskId)), - ) + mapContent() }, ) } @@ -139,6 +144,7 @@ private fun DropPinTaskScreenPreview() { onButtonClicked = {}, onLoiNameAction = {}, onInstructionsDismiss = {}, + mapContent = {}, ) } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt index 72606a5a01..0510d55e7c 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt @@ -49,6 +49,12 @@ fun DrawAreaTaskScreen( shouldShowLoiNameDialog: Boolean, loiName: String, onLoiNameAction: (LoiNameAction) -> Unit, + mapContent: @Composable () -> Unit = { + AndroidFragment( + clazz = DrawAreaTaskMapFragment::class.java, + arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, viewModel.task.id)), + ) + } ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val showSelfIntersectionDialog by viewModel.showSelfIntersectionDialog @@ -79,6 +85,7 @@ fun DrawAreaTaskScreen( onLoiNameAction = onLoiNameAction, onInstructionsDismiss = { viewModel.dismissDrawAreaInstructions() }, onDismissSelfIntersectionDialog = { viewModel.showSelfIntersectionDialog.value = false }, + mapContent = mapContent, ) } @@ -111,6 +118,7 @@ private fun DrawAreaTaskContent( onLoiNameAction: (LoiNameAction) -> Unit, onInstructionsDismiss: () -> Unit, onDismissSelfIntersectionDialog: () -> Unit, + mapContent: @Composable () -> Unit, ) { TaskScreen( taskHeader = TaskHeader(taskLabel, R.drawable.outline_draw), @@ -128,10 +136,7 @@ private fun DrawAreaTaskContent( onLoiNameAction = onLoiNameAction, onInstructionsDismiss = onInstructionsDismiss, taskBody = { - AndroidFragment( - clazz = DrawAreaTaskMapFragment::class.java, - arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, taskId)), - ) + mapContent() }, ) @@ -171,6 +176,7 @@ private fun DrawAreaTaskScreenInstructionsPreview() { onLoiNameAction = {}, onInstructionsDismiss = {}, onDismissSelfIntersectionDialog = {}, + mapContent = {}, ) } } @@ -200,6 +206,7 @@ private fun DrawAreaTaskScreenSelfIntersectionPreview() { onLoiNameAction = {}, onInstructionsDismiss = {}, onDismissSelfIntersectionDialog = {}, + mapContent = {}, ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreenTest.kt index 33326f41b0..ac4bf9346c 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskScreenTest.kt @@ -27,6 +27,7 @@ import com.google.common.truth.Truth.assertThat import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.ButtonActionStateChecker +import org.groundplatform.android.ui.datacollection.tasks.DataCollectionEvent import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.submission.DateTimeTaskData @@ -45,11 +46,11 @@ class DateTaskScreenTest { @get:Rule val composeTestRule = createComposeRule() private lateinit var viewModel: DateTaskViewModel - private var lastButtonAction: ButtonAction? = null + private var lastEvent: DataCollectionEvent? = null private val buttonActionStateChecker = ButtonActionStateChecker(composeTestRule) private fun setupTaskScreen(task: Task, taskData: TaskData? = null) { - lastButtonAction = null + lastEvent = null viewModel = DateTaskViewModel() viewModel.initialize( job = JOB, @@ -62,12 +63,10 @@ class DateTaskScreenTest { override fun isLastWithValue(taskData: TaskData?) = false }, surveyId = "survey_id", - eventReporter = {}, + eventReporter = { lastEvent = it }, ) - composeTestRule.setContent { - DateTaskScreen(viewModel = viewModel) - } + composeTestRule.setContent { DateTaskScreen(viewModel = viewModel) } } @Test @@ -184,7 +183,7 @@ class DateTaskScreenTest { buttonActionStateChecker.getNode(ButtonAction.PREVIOUS).performClick() - assertThat(lastButtonAction).isEqualTo(ButtonAction.PREVIOUS) + assertThat(lastEvent).isEqualTo(DataCollectionEvent.NavigatePrevious) } companion object { diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreenTest.kt index 66cd88dc40..5f6275c302 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskScreenTest.kt @@ -29,6 +29,7 @@ import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.submission.TaskData import org.groundplatform.domain.model.task.Task +import org.groundplatform.android.ui.datacollection.tasks.DataCollectionEvent import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -39,7 +40,7 @@ class InstructionTaskScreenTest { @get:Rule val composeTestRule = createComposeRule() - private var lastButtonAction: ButtonAction? = null + private var lastEvent: DataCollectionEvent? = null private val buttonActionStateChecker = ButtonActionStateChecker(composeTestRule) private fun setupTaskScreen( @@ -48,7 +49,7 @@ class InstructionTaskScreenTest { isFirst: Boolean = false, isLastWithValue: Boolean = false, ) { - lastButtonAction = null + lastEvent = null val viewModel = InstructionTaskViewModel().apply { initialize( @@ -62,7 +63,7 @@ class InstructionTaskScreenTest { override fun isLastWithValue(taskData: TaskData?) = isLastWithValue }, surveyId = "survey_id", - eventReporter = {}, + eventReporter = { lastEvent = it }, ) } @@ -96,7 +97,7 @@ class InstructionTaskScreenTest { buttonActionStateChecker.getNode(ButtonAction.NEXT).performClick() - assertThat(lastButtonAction).isEqualTo(ButtonAction.NEXT) + assertThat(lastEvent).isEqualTo(DataCollectionEvent.NavigateNext) } @Test @@ -105,7 +106,7 @@ class InstructionTaskScreenTest { buttonActionStateChecker.getNode(ButtonAction.PREVIOUS).performClick() - assertThat(lastButtonAction).isEqualTo(ButtonAction.PREVIOUS) + assertThat(lastEvent).isEqualTo(DataCollectionEvent.NavigatePrevious) } @Test diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt index a20a16880a..ccf594987f 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreenTest.kt @@ -29,6 +29,7 @@ import org.groundplatform.android.getString import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.ButtonActionStateChecker +import org.groundplatform.android.ui.datacollection.tasks.DataCollectionEvent import org.groundplatform.android.ui.datacollection.tasks.LocationLockEnabledState import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface import org.groundplatform.domain.model.geometry.Coordinates @@ -49,8 +50,7 @@ class CaptureLocationTaskScreenTest { @get:Rule val composeTestRule = createComposeRule() - private var lastScreenAction: ButtonAction? = null - private var openSettingsCalled = false + private var lastEvent: DataCollectionEvent? = null private lateinit var viewModel: CaptureLocationTaskViewModel private lateinit var buttonActionStateChecker: ButtonActionStateChecker @@ -175,7 +175,7 @@ class CaptureLocationTaskScreenTest { val title = getString(R.string.allow_location_title) composeTestRule.onNodeWithText(title).assertDoesNotExist() - assertThat(openSettingsCalled).isTrue() + assertThat(lastEvent).isEqualTo(DataCollectionEvent.OpenSettings) } @Test @@ -225,15 +225,14 @@ class CaptureLocationTaskScreenTest { isLastWithValue: Boolean = false, location: Location? = GOOD_LOCATION, ) { - lastScreenAction = null - openSettingsCalled = false + lastEvent = null viewModel.initialize( job = JOB, task = task, taskData = null, taskPositionInterface = createTaskPositionInterface(isFirst, isLastWithValue), surveyId = "survey_id", - eventReporter = {}, + eventReporter = { lastEvent = it }, ) if (location != null) { @@ -243,6 +242,7 @@ class CaptureLocationTaskScreenTest { composeTestRule.setContent { CaptureLocationTaskScreen( viewModel = viewModel, + mapContent = {} ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt index b661bee0ff..0381213832 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskScreenTest.kt @@ -37,6 +37,7 @@ import org.groundplatform.domain.model.submission.TaskData import org.groundplatform.domain.model.task.PhotoTaskData import org.groundplatform.domain.model.task.Task import org.groundplatform.domain.repository.UserMediaRepositoryInterface +import org.groundplatform.android.ui.datacollection.tasks.DataCollectionEvent import org.junit.Before import org.junit.Rule import org.junit.Test @@ -58,7 +59,7 @@ class PhotoTaskScreenTest { @Mock private lateinit var userMediaRepository: UserMediaRepositoryInterface @Mock private lateinit var permissionsManager: PermissionsManager private lateinit var viewModel: PhotoTaskViewModel - private var lastButtonAction: ButtonAction? = null + private var lastEvent: DataCollectionEvent? = null private val buttonActionStateChecker = ButtonActionStateChecker(composeTestRule) @Before @@ -72,7 +73,7 @@ class PhotoTaskScreenTest { isFirst: Boolean = false, isLastWithValue: Boolean = false, ) { - lastButtonAction = null + lastEvent = null viewModel = PhotoTaskViewModel(userMediaRepository, permissionsManager) viewModel.initialize( job = JOB, @@ -85,7 +86,7 @@ class PhotoTaskScreenTest { override fun isLastWithValue(taskData: TaskData?) = isLastWithValue }, surveyId = "survey_1", - eventReporter = {}, + eventReporter = { lastEvent = it }, ) composeTestRule.setContent { @@ -205,7 +206,7 @@ class PhotoTaskScreenTest { buttonActionStateChecker.getNode(ButtonAction.PREVIOUS).performClick() - assertThat(lastButtonAction).isEqualTo(ButtonAction.PREVIOUS) + assertThat(lastEvent).isEqualTo(DataCollectionEvent.NavigatePrevious) } @Test diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt index f4c37696eb..58420b4f49 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt @@ -23,6 +23,7 @@ import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.ExperimentalCoroutinesApi import org.groundplatform.android.BaseHiltTest +import org.groundplatform.android.HiltTestActivity import org.groundplatform.android.FakeData.JOB import org.groundplatform.android.R import org.groundplatform.android.data.local.LocalValueStore @@ -182,6 +183,7 @@ class DropPinTaskScreenTest : BaseHiltTest() { shouldShowLoiNameDialog = false, loiName = "", onLoiNameAction = {}, + mapContent = {} ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt index d4d9e1868f..625590fc7f 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt @@ -261,6 +261,7 @@ class DrawAreaTaskScreenTest : BaseHiltTest() { shouldShowLoiNameDialog = false, loiName = "", onLoiNameAction = {}, + mapContent = {} ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreenTest.kt index 10ed5dea80..61a6bbb6e0 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskScreenTest.kt @@ -34,6 +34,7 @@ import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.submission.TaskData import org.groundplatform.domain.model.submission.TextTaskData import org.groundplatform.domain.model.task.Task +import org.groundplatform.android.ui.datacollection.tasks.DataCollectionEvent import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -45,11 +46,11 @@ class TextTaskScreenTest { @get:Rule val composeTestRule = createComposeRule() private lateinit var viewModel: TextTaskViewModel - private var lastButtonAction: ButtonAction? = null + private var lastEvent: DataCollectionEvent? = null private val buttonActionStateChecker = ButtonActionStateChecker(composeTestRule) private fun setupTaskScreen(task: Task, taskData: TaskData? = null) { - lastButtonAction = null + lastEvent = null viewModel = TextTaskViewModel() viewModel.initialize( job = JOB, @@ -62,7 +63,7 @@ class TextTaskScreenTest { override fun isLastWithValue(taskData: TaskData?) = false }, surveyId = "survey_id", - eventReporter = {}, + eventReporter = { lastEvent = it }, ) composeTestRule.setContent { @@ -162,7 +163,7 @@ class TextTaskScreenTest { buttonActionStateChecker.getNode(ButtonAction.PREVIOUS).performClick() - assertThat(lastButtonAction).isEqualTo(ButtonAction.PREVIOUS) + assertThat(lastEvent).isEqualTo(DataCollectionEvent.NavigatePrevious) } companion object { diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreenTest.kt index 8e97869d3c..c0e83e47d5 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskScreenTest.kt @@ -32,6 +32,7 @@ import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.submission.DateTimeTaskData import org.groundplatform.domain.model.submission.TaskData import org.groundplatform.domain.model.task.Task +import org.groundplatform.android.ui.datacollection.tasks.DataCollectionEvent import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -45,11 +46,11 @@ class TimeTaskScreenTest { @get:Rule val composeTestRule = createComposeRule() private lateinit var viewModel: TimeTaskViewModel - private var lastButtonAction: ButtonAction? = null + private var lastEvent: DataCollectionEvent? = null private val buttonActionStateChecker = ButtonActionStateChecker(composeTestRule) private fun setupTaskScreen(task: Task, taskData: TaskData? = null) { - lastButtonAction = null + lastEvent = null viewModel = TimeTaskViewModel() viewModel.initialize( job = JOB, @@ -62,7 +63,7 @@ class TimeTaskScreenTest { override fun isLastWithValue(taskData: TaskData?) = false }, surveyId = "survey_id", - eventReporter = {}, + eventReporter = { lastEvent = it }, ) composeTestRule.setContent { @@ -169,7 +170,7 @@ class TimeTaskScreenTest { buttonActionStateChecker.getNode(ButtonAction.PREVIOUS).performClick() - assertThat(lastButtonAction).isEqualTo(ButtonAction.PREVIOUS) + assertThat(lastEvent).isEqualTo(DataCollectionEvent.NavigatePrevious) } private fun getExpectedTimeHint(): String { From 195036ca8c327080c241ec7dc751f9c7debeb8bf Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Wed, 13 May 2026 12:19:24 +0530 Subject: [PATCH 09/12] Remove MutableSharedFlow and simplify event handling in DataCollectionViewModel --- .../datacollection/DataCollectionViewModel.kt | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt index b73f3c581e..89e72d320c 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt @@ -18,12 +18,12 @@ package org.groundplatform.android.ui.datacollection import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher 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 @@ -60,7 +60,6 @@ import org.groundplatform.domain.repository.SubmissionRepositoryInterface import org.groundplatform.domain.usecases.GetLoiReportUseCase import org.groundplatform.domain.usecases.submission.SubmitDataUseCase import timber.log.Timber -import javax.inject.Inject sealed interface DataCollectionUiEffect { data object Exit : DataCollectionUiEffect @@ -92,9 +91,6 @@ internal constructor( private val _uiEffects = Channel(Channel.BUFFERED) val uiEffects = _uiEffects.receiveAsFlow() - private val _dataCollectionEvents = - MutableSharedFlow(extraBufferCapacity = 1) - private val _uiState = MutableStateFlow(DataCollectionUiState.Loading) val uiState: StateFlow = _uiState @@ -140,25 +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() - DataCollectionEvent.OpenSettings -> - viewModelScope.launch { _uiEffects.send(DataCollectionUiEffect.OpenSettings) } - is DataCollectionEvent.SetAwaitingPhotoCapture -> - viewModelScope.launch { - _uiEffects.send(DataCollectionUiEffect.SetAwaitingPhotoCapture(event.awaiting)) - } - } - } - } - } } private fun setLoiName(name: String) { @@ -342,7 +319,22 @@ internal constructor( isLastPositionWithValue(task, taskData) }, surveyId = state.surveyId, - eventReporter = { _dataCollectionEvents.tryEmit(it) }, + eventReporter = { event -> + withReadyOrNull { it.currentTaskId } + ?.let { taskId -> + when (event) { + DataCollectionEvent.NavigatePrevious -> onPreviousClicked(taskId) + DataCollectionEvent.NavigateNext -> onNextClicked(taskId) + DataCollectionEvent.ShowLoiDialog -> openLoiNameDialog() + DataCollectionEvent.OpenSettings -> + viewModelScope.launch { _uiEffects.send(DataCollectionUiEffect.OpenSettings) } + is DataCollectionEvent.SetAwaitingPhotoCapture -> + viewModelScope.launch { + _uiEffects.send(DataCollectionUiEffect.SetAwaitingPhotoCapture(event.awaiting)) + } + } + } + }, ) updateDataAndInvalidateTasks(task, taskData) taskViewModels.value[task.id] = created From aa103c953db6924649425c061608760d24a5d8f8 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Wed, 13 May 2026 12:31:04 +0530 Subject: [PATCH 10/12] Cleanup unused imports and params --- .../tasks/location/CaptureLocationTaskScreen.kt | 3 --- .../ui/datacollection/tasks/point/DropPinTaskScreen.kt | 3 --- .../ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt | 4 ---- .../ui/datacollection/tasks/point/DropPinTaskScreenTest.kt | 1 - 4 files changed, 11 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt index 3be28a933f..9a227eb1fa 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt @@ -61,7 +61,6 @@ fun CaptureLocationTaskScreen( LaunchedEffect(Unit) { viewModel.enableLocationLock() } CaptureLocationTaskContent( - taskId = viewModel.task.id, taskLabel = viewModel.task.label, taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, @@ -88,7 +87,6 @@ fun CaptureLocationTaskScreen( */ @Composable private fun CaptureLocationTaskContent( - taskId: String, taskLabel: String, taskPosition: TaskPosition? = null, taskActionButtonsStates: List, @@ -133,7 +131,6 @@ private fun CaptureLocationTaskContent( private fun CaptureLocationTaskScreenPreview() { AppTheme { CaptureLocationTaskContent( - taskId = "task_id", taskLabel = "Task for capturing current location", taskActionButtonsStates = listOf( diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt index a97d26e734..7930ea0cc1 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt @@ -68,7 +68,6 @@ fun DropPinTaskScreen( } DropPinTaskContent( - taskId = viewModel.task.id, taskLabel = viewModel.task.label, taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, @@ -92,7 +91,6 @@ fun DropPinTaskScreen( */ @Composable private fun DropPinTaskContent( - taskId: String, taskLabel: String, taskPosition: TaskPosition? = null, taskActionButtonsStates: List, @@ -128,7 +126,6 @@ private fun DropPinTaskContent( private fun DropPinTaskScreenPreview() { AppTheme { DropPinTaskContent( - taskId = "task_id", taskLabel = "Task for dropping a pin", taskActionButtonsStates = listOf( diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt index 0510d55e7c..cd1e26d462 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt @@ -73,7 +73,6 @@ fun DrawAreaTaskScreen( } DrawAreaTaskContent( - taskId = viewModel.task.id, taskLabel = viewModel.task.label, taskPosition = taskPosition, taskActionButtonsStates = taskActionButtonsStates, @@ -106,7 +105,6 @@ fun DrawAreaTaskScreen( */ @Composable private fun DrawAreaTaskContent( - taskId: String, taskLabel: String, taskPosition: TaskPosition? = null, taskActionButtonsStates: List, @@ -157,7 +155,6 @@ private fun DrawAreaTaskContent( private fun DrawAreaTaskScreenInstructionsPreview() { AppTheme { DrawAreaTaskContent( - taskId = "task_id", taskLabel = "Task for drawing a polygon", taskActionButtonsStates = listOf( @@ -187,7 +184,6 @@ private fun DrawAreaTaskScreenInstructionsPreview() { private fun DrawAreaTaskScreenSelfIntersectionPreview() { AppTheme { DrawAreaTaskContent( - taskId = "task_id", taskLabel = "Task for drawing a polygon", taskActionButtonsStates = listOf( diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt index 58420b4f49..3c35991dd0 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt @@ -23,7 +23,6 @@ import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.ExperimentalCoroutinesApi import org.groundplatform.android.BaseHiltTest -import org.groundplatform.android.HiltTestActivity import org.groundplatform.android.FakeData.JOB import org.groundplatform.android.R import org.groundplatform.android.data.local.LocalValueStore From 7a39332b23f165d9f7be63fdc51101d3a9091e1b Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Thu, 14 May 2026 07:36:08 +0530 Subject: [PATCH 11/12] Apply suggested changes --- .../datacollection/DataCollectionViewModel.kt | 25 ++++++++++++------- .../location/CaptureLocationTaskScreen.kt | 1 + .../tasks/point/DropPinTaskScreen.kt | 17 ++++++++++--- .../tasks/polygon/DrawAreaTaskScreen.kt | 22 ++++++++++++---- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt index 89e72d320c..4089744089 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt @@ -18,7 +18,6 @@ package org.groundplatform.android.ui.datacollection import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -60,6 +59,7 @@ import org.groundplatform.domain.repository.SubmissionRepositoryInterface import org.groundplatform.domain.usecases.GetLoiReportUseCase import org.groundplatform.domain.usecases.submission.SubmitDataUseCase import timber.log.Timber +import javax.inject.Inject sealed interface DataCollectionUiEffect { data object Exit : DataCollectionUiEffect @@ -190,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 -> { @@ -323,15 +333,12 @@ internal constructor( withReadyOrNull { it.currentTaskId } ?.let { taskId -> when (event) { - DataCollectionEvent.NavigatePrevious -> onPreviousClicked(taskId) - DataCollectionEvent.NavigateNext -> onNextClicked(taskId) - DataCollectionEvent.ShowLoiDialog -> openLoiNameDialog() - DataCollectionEvent.OpenSettings -> - viewModelScope.launch { _uiEffects.send(DataCollectionUiEffect.OpenSettings) } + is DataCollectionEvent.NavigatePrevious -> onPreviousClicked(taskId) + is DataCollectionEvent.NavigateNext -> onNextClicked(taskId) + is DataCollectionEvent.ShowLoiDialog -> openLoiNameDialog() + is DataCollectionEvent.OpenSettings -> openSettings() is DataCollectionEvent.SetAwaitingPhotoCapture -> - viewModelScope.launch { - _uiEffects.send(DataCollectionUiEffect.SetAwaitingPhotoCapture(event.awaiting)) - } + setAwaitingPhotoCapture(event.awaiting) } } }, diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt index 9a227eb1fa..f49268433a 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskScreen.kt @@ -84,6 +84,7 @@ fun CaptureLocationTaskScreen( * @param onDismissAccuracyCard Callback when the accuracy card is dismissed. * @param onAllowLocationClicked Callback when the allow location button is clicked in the dialog. * @param onButtonClicked Callback when a button is clicked. + * @param mapContent Composable for rendering the map. */ @Composable private fun CaptureLocationTaskContent( diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt index 7930ea0cc1..98b981b803 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreen.kt @@ -44,6 +44,11 @@ import org.groundplatform.ui.theme.AppTheme * routing. * * @param viewModel The view model for this task. + * @param taskPosition The position of the task in the sequence. + * @param shouldShowLoiNameDialog Whether to show the dialog for setting the LOI name. + * @param loiName Value to be prepopulated in the LOI name dialog. + * @param onLoiNameAction Callback when user interacts with the LOI name dialog. + * @param mapContent Composable for rendering the map. */ @Composable fun DropPinTaskScreen( @@ -57,7 +62,7 @@ fun DropPinTaskScreen( clazz = DropPinTaskMapFragment::class.java, arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, viewModel.task.id)), ) - } + }, ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val showInstructionsDialog by viewModel.showInstructionsDialog.collectAsStateWithLifecycle() @@ -88,6 +93,12 @@ fun DropPinTaskScreen( * @param taskPosition The position of the task in the sequence. * @param taskActionButtonsStates The states of the action buttons. * @param showInstructionsDialog Whether to show the instructions' dialog. + * @param shouldShowLoiNameDialog Whether to show the dialog for setting the LOI name. + * @param loiName Value to be prepopulated in the LOI name dialog. + * @param onButtonClicked Callback when a button with a [ButtonAction] is clicked. + * @param onLoiNameAction Callback when user interacts with the LOI name dialog. + * @param onInstructionsDismiss Callback when user dismisses the instructions' dialog. + * @param mapContent Composable for rendering the map. */ @Composable private fun DropPinTaskContent( @@ -114,9 +125,7 @@ private fun DropPinTaskContent( onButtonClicked = onButtonClicked, onLoiNameAction = onLoiNameAction, onInstructionsDismiss = onInstructionsDismiss, - taskBody = { - mapContent() - }, + taskBody = { mapContent() }, ) } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt index cd1e26d462..0b1112cc1d 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreen.kt @@ -42,6 +42,19 @@ import org.groundplatform.android.ui.datacollection.components.TaskHeader import org.groundplatform.android.ui.datacollection.tasks.TaskScreen import org.groundplatform.ui.theme.AppTheme +/** + * A screen for drawing an area (polygon) on the map. + * + * This is the stateful wrapper that collects state from [DrawAreaTaskViewModel] and handles event + * routing. + * + * @param viewModel The view model for this task. + * @param taskPosition The position of the task in the sequence. + * @param shouldShowLoiNameDialog Whether to show the dialog for setting the LOI name. + * @param loiName Value to be prepopulated in the LOI name dialog. + * @param onLoiNameAction Callback when user interacts with the LOI name dialog. + * @param mapContent Composable for rendering the map. + */ @Composable fun DrawAreaTaskScreen( viewModel: DrawAreaTaskViewModel, @@ -54,7 +67,7 @@ fun DrawAreaTaskScreen( clazz = DrawAreaTaskMapFragment::class.java, arguments = bundleOf(Pair(DataCollectionFragment.TASK_ID, viewModel.task.id)), ) - } + }, ) { val taskActionButtonsStates by viewModel.taskActionButtonStates.collectAsStateWithLifecycle() val showSelfIntersectionDialog by viewModel.showSelfIntersectionDialog @@ -100,8 +113,9 @@ fun DrawAreaTaskScreen( * @param loiName Value to be prepopulated in the LOI name dialog. * @param onButtonClicked Callback when a button with a [ButtonAction] is clicked. * @param onLoiNameAction Callback when user interacts with the LOI name dialog. - * @param onInstructionsDismiss Callback when user dismisses the instructions dialog. + * @param onInstructionsDismiss Callback when user dismisses the instructions' dialog. * @param onDismissSelfIntersectionDialog Callback when the self-intersection dialog is dismissed. + * @param mapContent Composable for rendering the map. */ @Composable private fun DrawAreaTaskContent( @@ -133,9 +147,7 @@ private fun DrawAreaTaskContent( onButtonClicked = onButtonClicked, onLoiNameAction = onLoiNameAction, onInstructionsDismiss = onInstructionsDismiss, - taskBody = { - mapContent() - }, + taskBody = { mapContent() }, ) if (showSelfIntersectionDialog) { From a4d919392fef2455679b3ce4780f5bec44a19291 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Thu, 14 May 2026 08:10:08 +0530 Subject: [PATCH 12/12] Use absoluteIndex instead of relativeIndex for task selection in DataCollectionScreen --- .../ui/datacollection/DataCollectionScreen.kt | 2 +- .../DataCollectionScreenTest.kt | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt index a9fcb9bf29..766035e189 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionScreen.kt @@ -80,7 +80,7 @@ fun DataCollectionScreen( val tasks = readyState.tasks if (tasks.isNotEmpty()) { val position = readyState.position - val currentTask = tasks[position.relativeIndex] + val currentTask = tasks[position.absoluteIndex] key(currentTask.id) { viewModel.getTaskViewModel(currentTask.id)?.let { taskViewModel -> diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt index 36efe7787e..2e93bc5aa4 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionScreenTest.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.lifecycle.MutableLiveData import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -27,11 +28,16 @@ import kotlinx.coroutines.test.runTest import org.groundplatform.android.FakeData import org.groundplatform.android.R import org.groundplatform.android.getString +import org.groundplatform.android.ui.datacollection.components.ButtonActionState +import org.groundplatform.android.ui.datacollection.tasks.text.INPUT_TEXT_TEST_TAG +import org.groundplatform.android.ui.datacollection.tasks.text.TextTaskViewModel +import org.groundplatform.domain.model.task.Task import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.doReturn @@ -49,12 +55,16 @@ class DataCollectionScreenTest { private val uiState = MutableStateFlow(DataCollectionUiState.Loading) private val showExitWarning = MutableStateFlow(false) private val uiEffects = MutableSharedFlow() + private val loiNameDraft = MutableStateFlow("") + private val loiNameDialogOpen = MutableStateFlow(false) @Before fun setUp() { whenever(mockViewModel.uiState).doReturn(uiState) whenever(mockViewModel.showExitWarning).doReturn(showExitWarning) whenever(mockViewModel.uiEffects).doReturn(uiEffects) + whenever(mockViewModel.loiNameDraft).doReturn(loiNameDraft) + whenever(mockViewModel.loiNameDialogOpen).doReturn(loiNameDialogOpen) } private fun setContent(onValidationError: (Int) -> Unit = {}, onExitConfirmed: () -> Unit = {}) { @@ -165,6 +175,41 @@ class DataCollectionScreenTest { composeTestRule.onNodeWithText(getString(R.string.unexpected_error)).assertIsDisplayed() } + @Test + fun `Displays correct task when absoluteIndex differs from relativeIndex`() { + val task1 = + Task( + id = "task1", + index = 0, + type = Task.Type.INSTRUCTIONS, + label = "Instructions Task", + isRequired = false, + ) + val task2 = + Task(id = "task2", index = 1, type = Task.Type.TEXT, label = "Text Task", isRequired = true) + + val mockTextTaskViewModel = mock(TextTaskViewModel::class.java) + val mockTaskActionButtonStates = MutableStateFlow>(emptyList()) + val mockResponseText = MutableLiveData("") + + whenever(mockTextTaskViewModel.taskActionButtonStates).thenReturn(mockTaskActionButtonStates) + whenever(mockTextTaskViewModel.responseText).thenReturn(mockResponseText) + whenever(mockTextTaskViewModel.task).thenReturn(task2) + + whenever(mockViewModel.getTaskViewModel("task2")).thenReturn(mockTextTaskViewModel) + + uiState.value = + READY_STATE.copy( + tasks = listOf(task1, task2), + currentTaskId = "task2", + position = TaskPosition(absoluteIndex = 1, relativeIndex = 0, sequenceSize = 1), + ) + + setContent() + + composeTestRule.onNodeWithTag(INPUT_TEXT_TEST_TAG).assertIsDisplayed() + } + companion object { private val JOB = FakeData.JOB.copy(name = "Test Job") private const val LOI_NAME = "Test LOI"