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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ internal object ValueJsonConverter {
DataStoreException.checkType(Point::class.java, geometry!!)
DropPinTaskData(geometry as Point)
}
Task.Type.DRAW_GEOMETRY -> {
if (obj is JSONObject) {
(obj as JSONObject).toCaptureLocationTaskData()
} else {
DataStoreException.checkType(String::class.java, obj)
val geometry = GeometryWrapperTypeConverter.fromString(obj as String)?.getGeometry()
DataStoreException.checkNotNull(geometry, "Missing geometry in draw geometry task result")
DataStoreException.checkType(Point::class.java, geometry!!)
DropPinTaskData(geometry as Point)
}
}
Task.Type.CAPTURE_LOCATION -> {
DataStoreException.checkType(JSONObject::class.java, obj)
(obj as JSONObject).toCaptureLocationTaskData()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package org.groundplatform.android.data.remote.firebase.schema
import org.groundplatform.android.data.remote.firebase.schema.ConditionConverter.toCondition
import org.groundplatform.android.data.remote.firebase.schema.MultipleChoiceConverter.toMultipleChoice
import org.groundplatform.android.model.task.Condition
import org.groundplatform.android.model.task.DrawGeometry
import org.groundplatform.android.model.task.Task
import org.groundplatform.android.proto.Task as TaskProto
import org.groundplatform.android.proto.Task.DataCollectionLevel
Expand Down Expand Up @@ -54,7 +55,7 @@ internal object TaskConverter {
if (drawGeometry?.allowedMethodsList?.contains(Method.DRAW_AREA) == true) {
Task.Type.DRAW_AREA
} else {
Task.Type.DROP_PIN
Task.Type.DRAW_GEOMETRY
}

fun toTask(task: TaskProto): Task =
Expand Down Expand Up @@ -83,6 +84,15 @@ internal object TaskConverter {
multipleChoice,
task.level == DataCollectionLevel.LOI_METADATA,
condition = condition,
drawGeometry =
if (taskType == Task.Type.DRAW_GEOMETRY) {
DrawGeometry(
task.drawGeometry.requireDeviceLocation,
task.drawGeometry.minAccuracyMeters,
)
} else {
null
},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2025 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.model.task

data class DrawGeometry(val isLocationLockRequired: Boolean, val minAccuracyMeters: Float)
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ constructor(
val multipleChoice: MultipleChoice? = null,
val isAddLoiTask: Boolean = false,
val condition: Condition? = null,
val drawGeometry: DrawGeometry? = null,
) {

// TODO: Define these in data layer!
Expand All @@ -48,6 +49,7 @@ constructor(
TIME,
DROP_PIN,
DRAW_AREA,
DRAW_GEOMETRY,
CAPTURE_LOCATION,
INSTRUCTIONS,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoMap
import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskViewModel
import org.groundplatform.android.ui.datacollection.tasks.geometry.DrawGeometryTaskViewModel
import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskViewModel
import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskViewModel
import org.groundplatform.android.ui.datacollection.tasks.multiplechoice.MultipleChoiceTaskViewModel
Expand All @@ -48,6 +49,11 @@ import org.groundplatform.android.ui.tos.TermsOfServiceViewModel
@InstallIn(SingletonComponent::class)
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(DrawGeometryTaskViewModel::class)
abstract fun bindDrawGeometryTaskViewModel(viewModel: DrawGeometryTaskViewModel): ViewModel

@Binds
@IntoMap
@ViewModelKey(DrawAreaTaskViewModel::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import org.groundplatform.android.ui.common.EphemeralPopups
import org.groundplatform.android.ui.common.ViewModelFactory
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel
import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskViewModel
import org.groundplatform.android.ui.datacollection.tasks.geometry.DrawGeometryTaskViewModel
import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskViewModel
import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskViewModel
import org.groundplatform.android.ui.datacollection.tasks.multiplechoice.MultipleChoiceTaskViewModel
Expand Down Expand Up @@ -373,6 +374,7 @@ internal constructor(
Task.Type.DATE -> DateTaskViewModel::class.java
Task.Type.TIME -> TimeTaskViewModel::class.java
Task.Type.DROP_PIN -> DropPinTaskViewModel::class.java
Task.Type.DRAW_GEOMETRY -> DrawGeometryTaskViewModel::class.java
Task.Type.DRAW_AREA -> DrawAreaTaskViewModel::class.java
Task.Type.CAPTURE_LOCATION ->
if (unifyCaptureLocationTask) DropPinTaskViewModel::class.java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import javax.inject.Provider
import org.groundplatform.android.UnifyCaptureLocationTask
import org.groundplatform.android.model.task.Task
import org.groundplatform.android.ui.datacollection.tasks.date.DateTaskFragment
import org.groundplatform.android.ui.datacollection.tasks.geometry.DrawGeometryTaskFragment
import org.groundplatform.android.ui.datacollection.tasks.instruction.InstructionTaskFragment
import org.groundplatform.android.ui.datacollection.tasks.location.CaptureLocationTaskFragment
import org.groundplatform.android.ui.datacollection.tasks.multiplechoice.MultipleChoiceTaskFragment
Expand All @@ -42,12 +43,14 @@ constructor(
private val drawAreaTaskFragmentProvider: Provider<DrawAreaTaskFragment>,
private val captureLocationTaskFragmentProvider: Provider<CaptureLocationTaskFragment>,
private val dropPinTaskFragmentProvider: Provider<DropPinTaskFragment>,
private val drawGeometryTaskFragmentProvider: Provider<DrawGeometryTaskFragment>,
@UnifyCaptureLocationTask private val unifyCaptureLocationTask: Boolean,
@Assisted fragment: Fragment,
@Assisted val tasks: List<Task>,
) : FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = tasks.size

@Suppress("CyclomaticComplexMethod")
override fun createFragment(position: Int): Fragment {
val task = tasks[position]

Expand All @@ -57,6 +60,7 @@ constructor(
Task.Type.MULTIPLE_CHOICE -> MultipleChoiceTaskFragment()
Task.Type.PHOTO -> PhotoTaskFragment()
Task.Type.DROP_PIN -> dropPinTaskFragmentProvider.get()
Task.Type.DRAW_GEOMETRY -> drawGeometryTaskFragmentProvider.get()
Task.Type.DRAW_AREA -> drawAreaTaskFragmentProvider.get()
Task.Type.NUMBER -> NumberTaskFragment()
Task.Type.DATE -> DateTaskFragment()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* Copyright 2025 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.geometry

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.groundplatform.android.R
import org.groundplatform.android.model.submission.isNullOrEmpty
import org.groundplatform.android.ui.components.ConfirmationDialog
import org.groundplatform.android.ui.datacollection.components.ButtonAction
import org.groundplatform.android.ui.datacollection.components.InstructionsDialog
import org.groundplatform.android.ui.datacollection.components.TaskView
import org.groundplatform.android.ui.datacollection.components.TaskViewFactory
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskFragment
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskMapFragment.Companion.TASK_ID_FRAGMENT_ARG_KEY
import org.groundplatform.android.ui.datacollection.tasks.LocationLockEnabledState
import org.groundplatform.android.ui.datacollection.tasks.location.LocationAccuracyCard
import org.groundplatform.android.util.renderComposableDialog

@AndroidEntryPoint
class DrawGeometryTaskFragment @Inject constructor() :
AbstractTaskFragment<DrawGeometryTaskViewModel>() {
@Inject lateinit var drawGeometryTaskMapFragmentProvider: Provider<DrawGeometryTaskMapFragment>

override fun onCreateTaskView(inflater: LayoutInflater): TaskView =
TaskViewFactory.createWithCombinedHeader(inflater, R.drawable.outline_pin_drop)

override fun onCreateTaskBody(inflater: LayoutInflater): View {
// NOTE(#2493): Multiplying by a random prime to allow for some mathematical uniqueness.
val rowLayout = LinearLayout(requireContext()).apply { id = View.generateViewId() * 11149 }
val fragment = drawGeometryTaskMapFragmentProvider.get()
val args = Bundle()
args.putString(TASK_ID_FRAGMENT_ARG_KEY, taskId)
fragment.arguments = args
childFragmentManager
.beginTransaction()
.add(rowLayout.id, fragment, DrawGeometryTaskMapFragment::class.java.simpleName)
.commit()
return rowLayout
}

override fun onTaskResume() {
// Ensure that the location lock is enabled, if it hasn't been.
if (isVisible) {
if (viewModel.isLocationLockRequired()) {
viewModel.enableLocationLock()
lifecycleScope.launch {
viewModel.enableLocationLockFlow.collect {
if (it == LocationLockEnabledState.NEEDS_ENABLE) {
showLocationPermissionDialog()
}
}
}
} else if (viewModel.shouldShowInstructionsDialog()) {
showInstructionsDialog()
}
}
}

override fun onCreateActionButtons() {
addSkipButton()
addUndoButton()

if (viewModel.isLocationLockRequired()) {
addButton(ButtonAction.CAPTURE_LOCATION)
.setOnClickListener { viewModel.onCaptureLocation() }
.setOnValueChanged { button, value -> button.showIfTrue(value.isNullOrEmpty()) }
.apply {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.isCaptureEnabled.collect { isEnabled -> enableIfTrue(isEnabled) }
}
}
} else {
addButton(ButtonAction.DROP_PIN)
.setOnClickListener { viewModel.onDropPin() }
.setOnValueChanged { button, value -> button.showIfTrue(value.isNullOrEmpty()) }
}

addNextButton(hideIfEmpty = true)
}

@Composable
override fun HeaderCard() {
if (viewModel.isLocationLockRequired()) {
val location by viewModel.lastLocation.collectAsState()
var showAccuracyCard by remember { mutableStateOf(false) }

LaunchedEffect(location) {
showAccuracyCard = location != null && !viewModel.isCaptureEnabled.first()
}

if (showAccuracyCard) {
LocationAccuracyCard(
onDismiss = { showAccuracyCard = false },
modifier = Modifier.padding(bottom = 12.dp),
)
}
}
}

private fun showLocationPermissionDialog() {
renderComposableDialog {
ConfirmationDialog(
title = R.string.allow_location_title,
description = R.string.allow_location_description,
confirmButtonText = R.string.allow_location_confirmation,
onConfirmClicked = {
// Open the app settings
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", context?.packageName, null)
context?.startActivity(intent)
},
)
}
}

private fun showInstructionsDialog() {
viewModel.instructionsDialogShown = true
renderComposableDialog {
InstructionsDialog(iconId = R.drawable.swipe_24, stringId = R.string.drop_a_pin_tooltip_text)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2025 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.geometry

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch
import org.groundplatform.android.model.map.CameraPosition
import org.groundplatform.android.ui.common.MapConfig
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskMapFragment
import org.groundplatform.android.ui.map.Feature
import org.groundplatform.android.ui.map.MapFragment

@AndroidEntryPoint
class DrawGeometryTaskMapFragment @Inject constructor() :
AbstractTaskMapFragment<DrawGeometryTaskViewModel>() {

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
val root = super.onCreateView(inflater, container, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
getMapViewModel().getLocationUpdates().collect { taskViewModel.updateLocation(it) }
}
return root
}

override fun getMapConfig(): MapConfig {
val config = super.getMapConfig()
return if (taskViewModel.isLocationLockRequired()) {
config.copy(allowGestures = false)
} else {
config
}
}

override fun onMapReady(map: MapFragment) {
super.onMapReady(map)
viewLifecycleOwner.lifecycleScope.launch {
taskViewModel.initLocationUpdates(getMapViewModel())
}
}

override fun onMapCameraMoved(position: CameraPosition) {
super.onMapCameraMoved(position)
taskViewModel.updateCameraPosition(position)
}

override fun renderFeatures(): LiveData<Set<Feature>> = taskViewModel.features

override fun setDefaultViewPort() {
val feature = taskViewModel.features.value?.firstOrNull() ?: return
val coordinates = feature.geometry.center()
moveToPosition(coordinates)
}
}
Loading