Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* 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.
Expand All @@ -13,13 +13,13 @@
* 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.common

import androidx.fragment.app.Fragment
import dagger.assisted.AssistedFactory
import org.groundplatform.android.model.task.Task
import androidx.compose.runtime.staticCompositionLocalOf
import org.groundplatform.android.system.PermissionsManager

@AssistedFactory
interface DataCollectionViewPagerAdapterFactory {
fun create(fragment: Fragment, tasks: List<Task>): DataCollectionViewPagerAdapter
}
val LocalPermissionsManager =
staticCompositionLocalOf<PermissionsManager> { error("No PermissionsManager provided") }

val LocalEphemeralPopups =
staticCompositionLocalOf<EphemeralPopups> { error("No EphemeralPopups provided") }
Original file line number Diff line number Diff line change
Expand Up @@ -21,49 +21,78 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.constraintlayout.widget.Guideline
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnLayout
import androidx.hilt.navigation.fragment.hiltNavGraphViewModels
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.widget.ViewPager2
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.launch
import org.groundplatform.android.R
import org.groundplatform.android.databinding.DataCollectionFragBinding
import org.groundplatform.android.model.task.Task
import org.groundplatform.android.system.PermissionsManager
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.common.LocalEphemeralPopups
import org.groundplatform.android.ui.common.LocalPermissionsManager
import org.groundplatform.android.ui.components.ConfirmationDialog
import org.groundplatform.android.ui.datacollection.tasks.TaskScreenEnvironment
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.HomeScreenFragmentDirections
import org.groundplatform.android.ui.home.HomeScreenViewModel
import org.groundplatform.android.util.renderComposableDialog
import org.groundplatform.android.util.setComposableContent

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

@Inject
lateinit var captureLocationTaskMapFragmentProvider: Provider<CaptureLocationTaskMapFragment>
@Inject lateinit var drawAreaTaskMapFragmentProvider: Provider<DrawAreaTaskMapFragment>
@Inject lateinit var dropPinTaskMapFragmentProvider: Provider<DropPinTaskMapFragment>
@Inject lateinit var permissionsManager: PermissionsManager
@Inject lateinit var popups: EphemeralPopups

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

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

private lateinit var binding: DataCollectionFragBinding
private lateinit var progressBar: ProgressBar
private lateinit var guideline: Guideline
private lateinit var viewPager: ViewPager2
private var isNavigatingUp = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
// Clean up child fragments to prevent "No view found for id" exception on rotation
childFragmentManager.fragments.forEach {
childFragmentManager.beginTransaction().remove(it).commitNowAllowingStateLoss()
}
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
super.onCreateView(inflater, container, savedInstanceState)
binding = DataCollectionFragBinding.inflate(inflater, container, false)
viewPager = binding.pager
progressBar = binding.progressBar
guideline = binding.progressBarGuideline
getAbstractActivity().setSupportActionBar(binding.dataCollectionToolbar)
Expand All @@ -77,9 +106,6 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner

viewPager.isUserInputEnabled = false
viewPager.offscreenPageLimit = 1

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.footerVerticalPosition.collect { setProgressBarPosition(it) }
Expand All @@ -92,6 +118,31 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
viewModel.uiState.collect { ui -> updateUI(ui) }
}
}

binding.composeView.setComposableContent {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

if (uiState is DataCollectionUiState.Ready) {
CompositionLocalProvider(
LocalPermissionsManager provides permissionsManager,
LocalEphemeralPopups provides popups,
) {
TaskPager(
env =
TaskScreenEnvironment(
captureLocationTaskMapFragmentProvider = captureLocationTaskMapFragmentProvider,
dataCollectionViewModel = viewModel,
drawAreaTaskMapFragmentProvider = drawAreaTaskMapFragmentProvider,
dropPinTaskMapFragmentProvider = dropPinTaskMapFragmentProvider,
fragmentManager = childFragmentManager,
homeScreenViewModel = homeScreenViewModel,
),
taskPosition = (uiState as DataCollectionUiState.Ready).position,
tasks = (uiState as DataCollectionUiState.Ready).tasks,
)
}
}
}
}

override fun onResume() {
Expand All @@ -108,15 +159,14 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {

private fun updateUI(uiState: DataCollectionUiState) {
when (uiState) {
// Ensure adapter has the task list; then jump to the current position.
is DataCollectionUiState.Ready -> {
binding.jobName = uiState.job.name
binding.loiName = uiState.loiName
loadTasks(uiState.tasks, uiState.position)
updateProgressBar(uiState.position)
}

is DataCollectionUiState.TaskUpdated -> {
onTaskChanged(uiState.position)
updateProgressBar(uiState.position)
}

is DataCollectionUiState.TaskSubmitted -> {
Expand All @@ -142,24 +192,9 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
}
}

private fun loadTasks(tasks: List<Task>, taskPosition: TaskPosition) {
val currentAdapter = viewPager.adapter as? DataCollectionViewPagerAdapter
if (currentAdapter == null || currentAdapter.tasks != tasks) {
viewPager.adapter = viewPagerAdapterFactory.create(this, tasks)
}
viewPager.doOnLayout { onTaskChanged(taskPosition) }
}

private fun onTaskChanged(taskPosition: TaskPosition) {
// Pass false to parameter smoothScroll to avoid smooth scrolling animation.
viewPager.setCurrentItem(taskPosition.absoluteIndex, false)
updateProgressBar(taskPosition, true)
}

private fun onTaskSubmitted() {
// Hide close button
binding.dataCollectionToolbar.navigationIcon = null
viewPager.adapter = null

// Display a confirmation dialog and move to home screen after that.
renderComposableDialog {
Expand All @@ -169,23 +204,19 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
}
}

private fun updateProgressBar(taskPosition: TaskPosition, shouldAnimate: Boolean) {
private fun updateProgressBar(taskPosition: TaskPosition) {
// Reset progress bar
progressBar.max = (taskPosition.sequenceSize - 1) * PROGRESS_SCALE

val target = taskPosition.relativeIndex * PROGRESS_SCALE
if (shouldAnimate) {
progressBar.clearAnimation()
ValueAnimator.ofInt(progressBar.progress, target)
.apply {
duration = 400L
interpolator = FastOutSlowInInterpolator()
addUpdateListener { progressBar.progress = it.animatedValue as Int }
}
.start()
} else {
progressBar.progress = target
}
progressBar.clearAnimation()
ValueAnimator.ofInt(progressBar.progress, target)
.apply {
duration = 400L
interpolator = FastOutSlowInInterpolator()
addUpdateListener { progressBar.progress = it.animatedValue as Int }
}
.start()
}

override fun onBack(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import org.groundplatform.android.repository.SubmissionRepository
import org.groundplatform.android.ui.common.AbstractViewModel
import org.groundplatform.android.ui.common.EphemeralPopups
import org.groundplatform.android.ui.common.ViewModelFactory
import org.groundplatform.android.ui.datacollection.components.ButtonAction
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel
import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface
import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskViewModel
Expand Down Expand Up @@ -188,6 +189,38 @@ internal constructor(
}
}

fun onAction(action: ButtonAction, taskViewModel: AbstractTaskViewModel) {
when (action) {
ButtonAction.PREVIOUS -> onPreviousClicked(taskViewModel)
ButtonAction.NEXT,
ButtonAction.DONE -> {
if (taskViewModel.task.isAddLoiTask) {
loiNameDialogOpen.value = true
} else {
onNextClicked(taskViewModel)
}
}
ButtonAction.SKIP -> {
check(taskViewModel.hasNoData()) { "User should not be able to skip a task with data." }
taskViewModel.setSkipped()
onNextClicked(taskViewModel)
}
else -> taskViewModel.onButtonClick(action)
}
}

fun onLoiNameDialogConfirm(name: String, taskViewModel: AbstractTaskViewModel) {
loiNameDialogOpen.value = false
if (name.isNotBlank()) {
setLoiName(name)
onNextClicked(taskViewModel)
}
}

fun onLoiNameDialogDismiss() {
loiNameDialogOpen.value = false
}

fun getTaskViewModel(taskId: String): AbstractTaskViewModel? = withReady { state ->
taskViewModels.value[taskId]?.let {
return it
Expand Down Expand Up @@ -342,8 +375,9 @@ internal constructor(
synchronized(draftLock) {
if (draftCache == null) {
draftCache = parsed
draftMapCache =
parsed.associate { (taskId, taskType, value) -> (taskId to taskType) to value }
draftMapCache = parsed.associate { (taskId, taskType, value) ->
(taskId to taskType) to value
}
}
}
}
Expand Down

This file was deleted.

Loading
Loading