From f1be4128a6da397c91b4d3b95bce34fbdf9ad412 Mon Sep 17 00:00:00 2001 From: andreia Date: Wed, 1 Apr 2026 17:40:34 +0200 Subject: [PATCH 01/18] refactor JobMapComponentState into a sealed class with the different mutually exclusive states --- .../home/mapcontainer/HomeScreenMapContainerViewModelTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt index addf25a6ec..4041910c5c 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt @@ -38,6 +38,7 @@ import org.groundplatform.android.system.auth.FakeAuthenticationManager import org.groundplatform.android.ui.home.mapcontainer.jobs.AdHocDataCollectionButtonData import org.groundplatform.android.ui.home.mapcontainer.jobs.JobMapComponentState import org.groundplatform.android.ui.home.mapcontainer.jobs.SelectedLoiSheetData +import org.groundplatform.android.ui.home.mapcontainer.jobs.JobMapComponentState import org.groundplatform.android.usecases.survey.ActivateSurveyUseCase import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.map.Bounds @@ -91,8 +92,7 @@ class HomeScreenMapContainerViewModelTest : BaseHiltTest() { .isEqualTo( JobMapComponentState.LoiSelected( SelectedLoiSheetData(canCollectData = true, LOCATION_OF_INTEREST, 0, true) - ) - ) + )) } @Test From c40c34dab2ff0738f348f5ee65f858e6960ae438 Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 2 Apr 2026 14:17:00 +0200 Subject: [PATCH 02/18] extract JobSelectionModal state --- .../ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt index 3cd1d83912..e661c2387e 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt @@ -204,8 +204,7 @@ internal constructor( combine(loisInViewport, featureClicked, adHocLoiJobs, showJobSelectionModal) { loisInView, feature, - jobs, - isModalShown -> + jobs, isModalShown -> val canUserSubmitData = userRepository.canUserSubmitData() val loiCard = loisInView From 3d82a59ddfbb8a95fdda8de7f41cce5af83ff914 Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 2 Apr 2026 15:51:48 +0200 Subject: [PATCH 03/18] move logic deciding OnAddLoiButtonClicked to the VM --- .../ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt | 3 ++- .../home/mapcontainer/HomeScreenMapContainerViewModelTest.kt | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt index e661c2387e..3cd1d83912 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt @@ -204,7 +204,8 @@ internal constructor( combine(loisInViewport, featureClicked, adHocLoiJobs, showJobSelectionModal) { loisInView, feature, - jobs, isModalShown -> + jobs, + isModalShown -> val canUserSubmitData = userRepository.canUserSubmitData() val loiCard = loisInView diff --git a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt index 4041910c5c..61defbccf1 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt @@ -38,7 +38,6 @@ import org.groundplatform.android.system.auth.FakeAuthenticationManager import org.groundplatform.android.ui.home.mapcontainer.jobs.AdHocDataCollectionButtonData import org.groundplatform.android.ui.home.mapcontainer.jobs.JobMapComponentState import org.groundplatform.android.ui.home.mapcontainer.jobs.SelectedLoiSheetData -import org.groundplatform.android.ui.home.mapcontainer.jobs.JobMapComponentState import org.groundplatform.android.usecases.survey.ActivateSurveyUseCase import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.map.Bounds From 53269378c3eab87dfb7f4937d25fd0982083f54a Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 2 Apr 2026 16:29:16 +0200 Subject: [PATCH 04/18] fix code format --- .../home/mapcontainer/HomeScreenMapContainerViewModelTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt index 61defbccf1..addf25a6ec 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt @@ -91,7 +91,8 @@ class HomeScreenMapContainerViewModelTest : BaseHiltTest() { .isEqualTo( JobMapComponentState.LoiSelected( SelectedLoiSheetData(canCollectData = true, LOCATION_OF_INTEREST, 0, true) - )) + ) + ) } @Test From d95a06b151b8651520590a9f86ea779929a9ba50 Mon Sep 17 00:00:00 2001 From: andreia Date: Wed, 11 Mar 2026 16:43:05 +0100 Subject: [PATCH 05/18] update DataSubmissionConfirmationScreen with new design with QR code --- .../datacollection/DataCollectionFragment.kt | 7 +- .../DataSubmissionConfirmationScreen.kt | 161 ++++++++++++++---- .../main/res/layout/data_collection_frag.xml | 90 +++++----- app/src/main/res/values/strings.xml | 2 + .../model/locationofinterest/LoiReport.kt | 2 +- .../domain/usecases/GetLoiReportUseCase.kt | 4 +- 6 files changed, 179 insertions(+), 87 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 82c9af942a..ac5ea78ba7 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 @@ -75,7 +75,6 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.lifecycleOwner = viewLifecycleOwner viewPager.isUserInputEnabled = false viewPager.offscreenPageLimit = 1 @@ -110,8 +109,8 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { 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 + binding.dataCollectionToolbar.title = uiState.job.name + binding.dataCollectionToolbar.subtitle = uiState.loiName loadTasks(uiState.tasks, uiState.position) } @@ -120,6 +119,8 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { } is DataCollectionUiState.TaskSubmitted -> { + binding.dataCollectionToolbar.title = getString(R.string.data_collection_complete) + binding.dataCollectionToolbar.subtitle = null onTaskSubmitted() } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt index 8140ae3786..1b2f21c5dd 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt @@ -16,92 +16,195 @@ package org.groundplatform.android.ui.datacollection import android.content.res.Configuration -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import org.groundplatform.android.R import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport +import org.groundplatform.domain.model.locationofinterest.LoiReport +import org.groundplatform.ui.components.GroundQrCode import org.groundplatform.ui.theme.AppTheme @Composable -fun DataSubmissionConfirmationScreen(onDismissed: () -> Unit) { +fun DataSubmissionConfirmationScreen(loiReport: LoiReport? = null, onDismissed: () -> Unit) { + val baseModifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface) if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { Row( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), + modifier = baseModifier.padding(vertical = 16.dp, horizontal = 48.dp), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically, ) { - DataSubmittedImage() - BodyContent { onDismissed() } + Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) { + HeaderContent() + OutlinedButton(modifier = Modifier.padding(top = 24.dp), onClick = { onDismissed() }) { + Text( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + text = stringResource(id = R.string.close), + style = MaterialTheme.typography.bodyMedium, + ) + } + } + ShareableContent(modifier = Modifier.weight(1f), loiReport = loiReport) } } else { Column( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), - verticalArrangement = Arrangement.SpaceEvenly, + modifier = baseModifier.padding(horizontal = 48.dp), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly, ) { - DataSubmittedImage() - BodyContent { onDismissed() } + HeaderContent(modifier = Modifier.padding(vertical = 16.dp)) + ShareableContent(loiReport = loiReport) + OutlinedButton(modifier = Modifier.padding(top = 24.dp), onClick = { onDismissed() }) { + Text( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + text = stringResource(id = R.string.close), + style = MaterialTheme.typography.bodyMedium, + ) + } } } } +// if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { +// Row( +// modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), +// horizontalArrangement = Arrangement.SpaceEvenly, +// verticalAlignment = Alignment.CenterVertically, +// ) { +// DataSubmittedImage() +// BodyContent { onDismissed() } +// } +// } else { +// Column( +// modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), +// verticalArrangement = Arrangement.SpaceEvenly, +// horizontalAlignment = Alignment.CenterHorizontally, +// ) { +// DataSubmittedImage() +// BodyContent { onDismissed() } +// } +// } + @Composable -private fun DataSubmittedImage() { - Image( - painter = painterResource(id = R.drawable.data_submitted), - contentDescription = stringResource(R.string.data_submitted_image), - contentScale = ContentScale.Fit, - ) +private fun DataSubmittedIcon(modifier: Modifier = Modifier) { + Box( + modifier = + modifier.size(64.dp).clip(CircleShape).background(MaterialTheme.colorScheme.secondary) + ) { + Icon( + modifier = Modifier.align(Alignment.Center).fillMaxSize().padding(8.dp), + painter = painterResource(id = R.drawable.baseline_check_24), + contentDescription = null, + tint = Color.White, + ) + } } @Composable -private fun BodyContent(onDismiss: () -> Unit) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResource(R.string.data_collection_complete), - style = MaterialTheme.typography.titleLarge, - ) +private fun HeaderContent(modifier: Modifier = Modifier) { + Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { + DataSubmittedIcon(modifier = Modifier.padding(8.dp)) Spacer(modifier = Modifier.height(8.dp)) Text( - modifier = Modifier.padding(horizontal = 28.dp), text = stringResource(R.string.data_collection_complete_details), style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Normal, textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(30.dp)) - OutlinedButton(onClick = { onDismiss() }) { + } +} + +@Composable +private fun ShareableContent(modifier: Modifier = Modifier, loiReport: LoiReport?) { + loiReport?.let { + Column(modifier) { Text( - modifier = Modifier.padding(horizontal = 24.dp, vertical = 10.dp), - text = stringResource(id = R.string.close), - style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + text = stringResource(R.string.share_location), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, ) + Box( + modifier = + Modifier.fillMaxWidth() + .background( + MaterialTheme.colorScheme.background, + RoundedCornerShape(12.dp), + ) // todo consider shapes + .padding(24.dp) + ) { + GroundQrCode( + modifier = Modifier.align(Alignment.Center), + title = loiReport.loiName, + footer = stringResource(R.string.scan_this_qr_to_download_geojson), + qrContent = loiReport.geoJson.toString(), + contentDescription = "QR code with LOI Geometry", + centerLogoPainter = painterResource(R.drawable.ground_logo), + ) + } } } } +private val testLoiReport = + LoiReport( + loiName = "Test LOI", + geoJson = + JsonObject( + mapOf( + "type" to JsonPrimitive("Feature"), + "properties" to JsonObject(mapOf("name" to JsonPrimitive("Point test"))), + "geometry" to + JsonObject( + mapOf( + "type" to JsonPrimitive("Point"), + "coordinates" to JsonArray(listOf(JsonPrimitive(-89.0), JsonPrimitive(41.0))), + ) + ), + ) + ), + ) + +@Composable +@Preview +@ExcludeFromJacocoGeneratedReport +private fun DataSubmissionConfirmationScreenPortraitPreview() { + AppTheme { DataSubmissionConfirmationScreen(loiReport = testLoiReport) {} } +} + @Composable @Preview(heightDp = 320, widthDp = 800) @ExcludeFromJacocoGeneratedReport -private fun DataSubmissionConfirmationScreenPreview() { - AppTheme { DataSubmissionConfirmationScreen {} } +private fun DataSubmissionConfirmationScreenLandscapePreview() { + AppTheme { DataSubmissionConfirmationScreen(loiReport = testLoiReport) {} } } diff --git a/app/src/main/res/layout/data_collection_frag.xml b/app/src/main/res/layout/data_collection_frag.xml index a91e3a191a..b0191bd37f 100644 --- a/app/src/main/res/layout/data_collection_frag.xml +++ b/app/src/main/res/layout/data_collection_frag.xml @@ -16,57 +16,43 @@ ~ limitations under the License. --> - - - - - - - - + + - - - - - - - - + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:navigationIcon="@drawable/baseline_close_24" + app:subtitleCentered="true" /> + + + + + + - - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae50c9985b..de6d9fff2b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -238,4 +238,6 @@ Try standing still or waiting for a more accurate position Re-center + Share location and answers + Scan this QR code to download the GeoJson diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/locationofinterest/LoiReport.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/locationofinterest/LoiReport.kt index bd5a30d06c..c0dc8a99f9 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/locationofinterest/LoiReport.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/locationofinterest/LoiReport.kt @@ -18,4 +18,4 @@ package org.groundplatform.domain.model.locationofinterest import kotlinx.serialization.json.JsonObject /** Represents the data collected for a specific LOI which can be downloaded and shared. */ -data class LoiReport(val geoJson: JsonObject) +data class LoiReport(val loiName: String, val geoJson: JsonObject) diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt index 0a8286eeda..2dc32db36f 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt @@ -45,9 +45,9 @@ class GetLoiReportUseCase( * @param surveyId the identifier of the survey the LOI belongs to. * @throws IllegalStateException if the LOI geometry is a bare [LinearRing]. */ - suspend operator fun invoke(loiId: String, surveyId: String): LoiReport? { + suspend operator fun invoke(loiName: String, loiId: String, surveyId: String): LoiReport? { val loi = locationOfInterestRepository.getOfflineLoi(surveyId, loiId) - return loi?.let { LoiReport(it.geometry.toGeoJson(it.properties)) } + return loi?.let { LoiReport(loiName, it.geometry.toGeoJson(it.properties)) } } /** From db694cd6fd778c76bbf6c95143a2655520b2037c Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 12 Mar 2026 15:25:26 +0100 Subject: [PATCH 06/18] implement GetLoiReportUseCase in data completion screen --- .../datacollection/DataCollectionFragment.kt | 19 ++++--- .../datacollection/DataCollectionUiState.kt | 3 +- .../datacollection/DataCollectionViewModel.kt | 54 +++++++++++++------ .../DataSubmissionConfirmationScreen.kt | 43 ++++++--------- .../usecases/submission/SubmitDataUseCase.kt | 3 +- 5 files changed, 68 insertions(+), 54 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 ac5ea78ba7..00aba755b3 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 @@ -41,6 +41,7 @@ import org.groundplatform.android.ui.common.BackPressListener import org.groundplatform.android.ui.components.ConfirmationDialog import org.groundplatform.android.ui.home.HomeScreenFragmentDirections import org.groundplatform.android.util.renderComposableDialog +import org.groundplatform.domain.model.locationofinterest.LoiReport import org.groundplatform.domain.model.task.Task /** Fragment allowing the user to collect data to complete a task. */ @@ -68,7 +69,13 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { guideline = binding.progressBarGuideline getAbstractActivity().setSupportActionBar(binding.dataCollectionToolbar) - binding.dataCollectionToolbar.setNavigationOnClickListener { showExitWarningDialog() } + binding.dataCollectionToolbar.setNavigationOnClickListener { + if (viewModel.uiState.value is DataCollectionUiState.TaskSubmitted) { + navigateBack() + } else { + showExitWarningDialog() + } + } return binding.root } @@ -121,7 +128,7 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { is DataCollectionUiState.TaskSubmitted -> { binding.dataCollectionToolbar.title = getString(R.string.data_collection_complete) binding.dataCollectionToolbar.subtitle = null - onTaskSubmitted() + onTaskSubmitted(uiState.loiReport) } is DataCollectionUiState.Loading, @@ -157,14 +164,12 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { updateProgressBar(taskPosition, true) } - private fun onTaskSubmitted() { - // Hide close button - binding.dataCollectionToolbar.navigationIcon = null + private fun onTaskSubmitted(loiReport: LoiReport?) { viewPager.adapter = null // Display a confirmation dialog and move to home screen after that. renderComposableDialog { - DataSubmissionConfirmationScreen { + DataSubmissionConfirmationScreen(loiReport) { findNavController().navigate(HomeScreenFragmentDirections.showHomeScreen()) } } @@ -190,7 +195,7 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { } override fun onBack(): Boolean { - if (viewModel.uiState.value == DataCollectionUiState.TaskSubmitted) { + if (viewModel.uiState.value is DataCollectionUiState.TaskSubmitted) { // Pressing back button after submitting task should navigate back to home screen. navigateBack() return true diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionUiState.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionUiState.kt index ef2e3a73b2..07c3782a5d 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionUiState.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionUiState.kt @@ -17,6 +17,7 @@ package org.groundplatform.android.ui.datacollection import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.task.Task +import org.groundplatform.domain.model.locationofinterest.LoiReport /** * Top-level UI state for the Data Collection flow. @@ -85,7 +86,7 @@ sealed interface DataCollectionUiState { * affordance or navigate away (e.g., back to the home screen) and must not attempt to read or * save further draft data for this session. */ - data object TaskSubmitted : DataCollectionUiState + data class TaskSubmitted(val loiReport: LoiReport?) : DataCollectionUiState } /** Stable, UI-mappable error codes for data collection bootstrapping and flow. */ 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 198c81eba7..eed7440c4c 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 @@ -23,6 +23,7 @@ import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -30,6 +31,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.groundplatform.android.data.local.room.converter.SubmissionDeltasConverter import org.groundplatform.android.data.uuid.OfflineUuidGenerator import org.groundplatform.android.di.coroutines.ApplicationScope @@ -56,6 +58,7 @@ import org.groundplatform.domain.model.submission.TaskData import org.groundplatform.domain.model.submission.ValueDelta import org.groundplatform.domain.model.submission.isNotNullOrEmpty import org.groundplatform.domain.model.task.Task +import org.groundplatform.domain.usecases.GetLoiReportUseCase import timber.log.Timber /** View model for the Data Collection fragment. */ @@ -72,6 +75,7 @@ internal constructor( private val popups: Provider, private val viewModelFactory: ViewModelFactory, private val dataCollectionInitializer: DataCollectionInitializer, + private val getLoiReportUseCase: GetLoiReportUseCase, ) : AbstractViewModel() { /** The current vertical position of the task view footer. */ @@ -86,7 +90,6 @@ internal constructor( private val jobId: String = requireNotNull(savedStateHandle[TASK_JOB_ID_KEY]) private val loiId: String? = savedStateHandle[TASK_LOI_ID_KEY] - private val loiName: String? = savedStateHandle[TASK_LOI_NAME_KEY] private val taskDataHandler = TaskDataHandler() private lateinit var taskSequenceHandler: TaskSequenceHandler @@ -99,7 +102,13 @@ internal constructor( init { viewModelScope.launch { - val initResult = dataCollectionInitializer.initialize(savedStateHandle, jobId, loiId, loiName) + val initResult = + dataCollectionInitializer.initialize( + savedStateHandle, + jobId, + loiId, + getTypedLoiNameOrEmpty(), + ) if (initResult is DataCollectionUiState.Ready) { taskSequenceHandler = TaskSequenceHandler(initResult.tasks, taskDataHandler) @@ -167,8 +176,18 @@ internal constructor( moveToNextTask() } else { clearDraft() - saveChanges(st, getDeltas()) - _uiState.value = DataCollectionUiState.TaskSubmitted + externalScope.launch(ioDispatcher) { + val submittedLoiId = saveChanges(st, getDeltas()) + val loiReport = + getLoiReportUseCase.invoke( + loiName = getTypedLoiNameOrEmpty(), + loiId = submittedLoiId, + surveyId = st.surveyId, + ) + withContext(Dispatchers.Main) { + _uiState.value = DataCollectionUiState.TaskSubmitted(loiReport) + } + } } } } @@ -240,18 +259,19 @@ internal constructor( moveToTask(withReady { taskSequenceHandler.getNextTask(it.currentTaskId) }) } - private fun saveChanges(state: DataCollectionUiState.Ready, deltas: List) { - externalScope.launch(ioDispatcher) { - val collectionId = offlineUuidGenerator.generateUuid() - submitDataUseCase.invoke( - loiId, - state.job, - state.surveyId, - deltas, - savedStateHandle[TASK_LOI_NAME_KEY], - collectionId, - ) - } + private suspend fun saveChanges( + state: DataCollectionUiState.Ready, + deltas: List, + ): String { + val collectionId = offlineUuidGenerator.generateUuid() + return submitDataUseCase.invoke( + selectedLoiId = loiId, + job = state.job, + surveyId = state.surveyId, + deltas = deltas, + loiName = savedStateHandle[TASK_LOI_NAME_KEY], + collectionId = collectionId, + ) } private fun suppressDrafts() { @@ -282,7 +302,7 @@ internal constructor( val state = uiState.value if ( - state == DataCollectionUiState.TaskSubmitted || + state is DataCollectionUiState.TaskSubmitted || deltas.isEmpty() || state !is DataCollectionUiState.Ready ) { diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt index 1b2f21c5dd..83f72ebfcd 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt @@ -27,8 +27,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -54,12 +57,20 @@ import org.groundplatform.domain.model.locationofinterest.LoiReport import org.groundplatform.ui.components.GroundQrCode import org.groundplatform.ui.theme.AppTheme +private val DEFAULT_TOOLBAR_HEIGHT = 56.dp + @Composable fun DataSubmissionConfirmationScreen(loiReport: LoiReport? = null, onDismissed: () -> Unit) { - val baseModifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface) + val baseModifier = + Modifier.fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .padding(top = DEFAULT_TOOLBAR_HEIGHT) + .padding(horizontal = 48.dp) + .systemBarsPadding() + .verticalScroll(rememberScrollState()) if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { Row( - modifier = baseModifier.padding(vertical = 16.dp, horizontal = 48.dp), + modifier = baseModifier, horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically, ) { @@ -76,11 +87,7 @@ fun DataSubmissionConfirmationScreen(loiReport: LoiReport? = null, onDismissed: ShareableContent(modifier = Modifier.weight(1f), loiReport = loiReport) } } else { - Column( - modifier = baseModifier.padding(horizontal = 48.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceEvenly, - ) { + Column(modifier = baseModifier, horizontalAlignment = Alignment.CenterHorizontally) { HeaderContent(modifier = Modifier.padding(vertical = 16.dp)) ShareableContent(loiReport = loiReport) OutlinedButton(modifier = Modifier.padding(top = 24.dp), onClick = { onDismissed() }) { @@ -94,26 +101,6 @@ fun DataSubmissionConfirmationScreen(loiReport: LoiReport? = null, onDismissed: } } -// if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { -// Row( -// modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), -// horizontalArrangement = Arrangement.SpaceEvenly, -// verticalAlignment = Alignment.CenterVertically, -// ) { -// DataSubmittedImage() -// BodyContent { onDismissed() } -// } -// } else { -// Column( -// modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), -// verticalArrangement = Arrangement.SpaceEvenly, -// horizontalAlignment = Alignment.CenterHorizontally, -// ) { -// DataSubmittedImage() -// BodyContent { onDismissed() } -// } -// } - @Composable private fun DataSubmittedIcon(modifier: Modifier = Modifier) { Box( @@ -196,7 +183,7 @@ private val testLoiReport = ) @Composable -@Preview +@Preview(showSystemUi = true) @ExcludeFromJacocoGeneratedReport private fun DataSubmissionConfirmationScreenPortraitPreview() { AppTheme { DataSubmissionConfirmationScreen(loiReport = testLoiReport) {} } diff --git a/app/src/main/java/org/groundplatform/android/usecases/submission/SubmitDataUseCase.kt b/app/src/main/java/org/groundplatform/android/usecases/submission/SubmitDataUseCase.kt index 608ceb6ec7..fe2ae391f3 100644 --- a/app/src/main/java/org/groundplatform/android/usecases/submission/SubmitDataUseCase.kt +++ b/app/src/main/java/org/groundplatform/android/usecases/submission/SubmitDataUseCase.kt @@ -48,12 +48,13 @@ constructor( deltas: List, loiName: String?, collectionId: String, - ) { + ): String { Timber.v("Submitting data for LOI: $selectedLoiId") val deltasToSubmit = deltas.toMutableList() val submissionLoiId = selectedLoiId ?: addLocationOfInterest(surveyId, job, deltasToSubmit, loiName, collectionId) submissionRepository.saveSubmission(surveyId, submissionLoiId, deltasToSubmit, collectionId) + return submissionLoiId } /** From dce791d37a9d0eb64deb1680b00d87c2a549deff Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 24 Mar 2026 10:26:15 +0100 Subject: [PATCH 07/18] update paddings --- .../ui/datacollection/DataSubmissionConfirmationScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt index 83f72ebfcd..df776a79db 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt @@ -65,9 +65,9 @@ fun DataSubmissionConfirmationScreen(loiReport: LoiReport? = null, onDismissed: Modifier.fillMaxSize() .background(MaterialTheme.colorScheme.surface) .padding(top = DEFAULT_TOOLBAR_HEIGHT) - .padding(horizontal = 48.dp) .systemBarsPadding() .verticalScroll(rememberScrollState()) + .padding(horizontal = 48.dp) if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { Row( modifier = baseModifier, @@ -90,7 +90,7 @@ fun DataSubmissionConfirmationScreen(loiReport: LoiReport? = null, onDismissed: Column(modifier = baseModifier, horizontalAlignment = Alignment.CenterHorizontally) { HeaderContent(modifier = Modifier.padding(vertical = 16.dp)) ShareableContent(loiReport = loiReport) - OutlinedButton(modifier = Modifier.padding(top = 24.dp), onClick = { onDismissed() }) { + OutlinedButton(modifier = Modifier.padding(vertical = 24.dp), onClick = { onDismissed() }) { Text( modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), text = stringResource(id = R.string.close), From f4ad082c28f2db16e211c304ccfa0e51bead6051 Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 24 Mar 2026 12:49:07 +0100 Subject: [PATCH 08/18] enable share location geojson by clicking on loi on the map --- .../HomeScreenMapContainerViewModel.kt | 12 ++ .../jobs/DataCollectionEntryPointData.kt | 2 + .../home/mapcontainer/jobs/JobMapComponent.kt | 15 ++- .../ui/home/mapcontainer/jobs/LoiJobSheet.kt | 86 ++++++++---- .../mapcontainer/jobs/ShareLocationModal.kt | 122 ++++++++++++++++++ app/src/main/res/values/strings.xml | 5 +- 6 files changed, 213 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt index 3cd1d83912..4dbee512ee 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt @@ -47,6 +47,7 @@ import org.groundplatform.android.system.LocationManager import org.groundplatform.android.system.PermissionsManager import org.groundplatform.android.system.SettingsManager import org.groundplatform.android.ui.common.BaseMapViewModel +import org.groundplatform.android.ui.common.LocationOfInterestHelper import org.groundplatform.android.ui.common.SharedViewModel import org.groundplatform.android.ui.home.mapcontainer.jobs.AdHocDataCollectionButtonData import org.groundplatform.android.ui.home.mapcontainer.jobs.JobMapComponentState @@ -59,6 +60,7 @@ import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.locationofinterest.LocationOfInterest import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface +import org.groundplatform.domain.usecases.GetLoiReportUseCase import org.groundplatform.domain.repository.UserRepositoryInterface @OptIn(ExperimentalCoroutinesApi::class) @@ -77,6 +79,8 @@ internal constructor( private val surveyRepository: SurveyRepository, private val userRepository: UserRepositoryInterface, private val localValueStore: LocalValueStore, + private val locationOfInterestHelper: LocationOfInterestHelper, + private val getLoiReportUseCase: GetLoiReportUseCase, ) : BaseMapViewModel( locationManager, @@ -212,11 +216,19 @@ internal constructor( .firstOrNull { it.geometry == feature?.geometry } ?.let { loi -> val canDelete = userRepository.canDeleteLoi(loi) + val loiReport = + getLoiReportUseCase.invoke( + loiName = locationOfInterestHelper.getDisplayLoiName(loi), + loiId = loi.id, + surveyId = activeSurvey.filterNotNull().first().id, + ) + SelectedLoiSheetData( canCollectData = canUserSubmitData, loi = loi, submissionCount = submissionRepository.getTotalSubmissionCount(loi), showDeleteLoiButton = canDelete, + loiReport = loiReport, ) } diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/DataCollectionEntryPointData.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/DataCollectionEntryPointData.kt index c3cb9cc697..8a9b289a31 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/DataCollectionEntryPointData.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/DataCollectionEntryPointData.kt @@ -18,6 +18,7 @@ package org.groundplatform.android.ui.home.mapcontainer.jobs import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.locationofinterest.LocationOfInterest +import org.groundplatform.domain.model.locationofinterest.LoiReport /** Data classes used to populate the data collection entry UI, like the LOI bottom sheet. */ sealed interface DataCollectionEntryPointData { @@ -29,6 +30,7 @@ data class SelectedLoiSheetData( val loi: LocationOfInterest, val submissionCount: Int, val showDeleteLoiButton: Boolean, + val loiReport: LoiReport?, ) : DataCollectionEntryPointData data class AdHocDataCollectionButtonData(override val canCollectData: Boolean, val job: Job) : diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponent.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponent.kt index 4d613fa339..1faab401d0 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponent.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponent.kt @@ -26,6 +26,10 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -42,16 +46,19 @@ import org.groundplatform.ui.theme.AppTheme @Composable fun JobMapComponent(state: JobMapComponentState, onAction: (JobMapComponentAction) -> Unit) { + var showShareLoiModal by rememberSaveable { mutableStateOf(false) } when (state) { is JobMapComponentState.LoiSelected -> { + if (showShareLoiModal && state.loi.loiReport != null) { + ShareLocationModal(state.loi.loiReport) { showShareLoiModal = false } + } + LoiJobSheet( - loi = state.loi.loi, + state = state.loi, onCollectClicked = { onAction(OnAddDataClicked(state.loi)) }, onDeleteClicked = { onAction(OnDeleteSiteClicked(state.loi)) }, onDismiss = { onAction(JobMapComponentAction.OnJobCardDismissed) }, - canUserSubmitData = state.loi.canCollectData, - submissionCount = state.loi.submissionCount, - showDeleteLoiButton = state.loi.showDeleteLoiButton, + onShareClicked = { showShareLoiModal = true }, ) } is JobMapComponentState.AddLoiButton -> { diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheet.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheet.kt index 3d6d186952..f0310ffba2 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheet.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheet.kt @@ -23,9 +23,12 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet @@ -64,13 +67,11 @@ import org.groundplatform.ui.theme.AppTheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun LoiJobSheet( - loi: LocationOfInterest, - canUserSubmitData: Boolean, - submissionCount: Int, - showDeleteLoiButton: Boolean = false, + state: SelectedLoiSheetData, onCollectClicked: () -> Unit, onDeleteClicked: (() -> Unit)? = null, onDismiss: () -> Unit, + onShareClicked: () -> Unit, ) { val scope = rememberCoroutineScope() val sheetState = rememberModalBottomSheetState() @@ -81,20 +82,31 @@ fun LoiJobSheet( containerColor = MaterialTheme.colorScheme.surface, dragHandle = { BottomSheetDefaults.DragHandle(width = 32.dp) }, ) { - ModalContents(loi, canUserSubmitData, submissionCount, showDeleteLoiButton, onDeleteClicked) { - scope.launch { sheetState.hide() }.invokeOnCompletion { onCollectClicked() } - } + ModalContents( + loi = state.loi, + canUserSubmitData = state.canCollectData, + submissionCount = state.submissionCount, + showDeleteLoiButton = state.showDeleteLoiButton, + showShareButton = state.loiReport != null, + onDeleteClicked = onDeleteClicked, + onCollectClicked = { + scope.launch { sheetState.hide() }.invokeOnCompletion { onCollectClicked() } + }, + onShareClicked = onShareClicked, + ) } } @Composable -private fun ModalContents( +private fun ModalContents( // todo consider refactor to simpler state loi: LocationOfInterest, canUserSubmitData: Boolean, submissionCount: Int, showDeleteLoiButton: Boolean, + showShareButton: Boolean, onDeleteClicked: (() -> Unit)?, onCollectClicked: () -> Unit, + onShareClicked: () -> Unit, ) { val resources = LocalContext.current.resources val loiHelper = remember(resources) { LocationOfInterestHelper(resources) } @@ -107,7 +119,9 @@ private fun ModalContents( loi = loi, submissionCount = submissionCount, canUserSubmitData = canUserSubmitData, + showShareButton = showShareButton, onCollectClicked = onCollectClicked, + onShareClicked = onShareClicked, ) DeleteSiteSection( showDeleteLoiButton = showDeleteLoiButton, @@ -163,13 +177,11 @@ private fun SubmissionRow( loi: LocationOfInterest, submissionCount: Int, canUserSubmitData: Boolean, + showShareButton: Boolean, onCollectClicked: () -> Unit, + onShareClicked: () -> Unit, ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.SpaceBetween, - ) { + Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.Top) { Text( if (submissionCount <= 0) stringResource(R.string.no_submissions) else pluralStringResource(R.plurals.submission_count, submissionCount, submissionCount), @@ -177,11 +189,27 @@ private fun SubmissionRow( style = MaterialTheme.typography.bodyLarge, ) - // NOTE(#2539): Avoid crash when there are no non-LOI tasks. - val showAddData = canUserSubmitData && loi.job.hasNonLoiTasks() && loi.isPredefined == true - if (showAddData) { - Button(onClick = onCollectClicked) { - Text(stringResource(R.string.add_data), modifier = Modifier.padding(4.dp)) + Row( + modifier = Modifier.padding(top = 16.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + if (showShareButton) { + FilledTonalButton(onClick = onShareClicked) { + Icon( + modifier = Modifier.padding(end = 8.dp), + imageVector = Icons.Outlined.Share, + contentDescription = "Share", + ) + Text(stringResource(R.string.share), modifier = Modifier.padding(4.dp)) + } + } + + // NOTE(#2539): Avoid crash when there are no non-LOI tasks. + val showAddData = canUserSubmitData && loi.job.hasNonLoiTasks() && loi.isPredefined == true + if (showAddData) { + Button(modifier = Modifier.padding(start = 8.dp), onClick = onCollectClicked) { + Text(stringResource(R.string.add_data), modifier = Modifier.padding(4.dp)) + } } } } @@ -243,8 +271,11 @@ private fun PreviewModalContentsWhenJobHasNoTasks() { canUserSubmitData = true, submissionCount = 0, showDeleteLoiButton = false, + showShareButton = true, onDeleteClicked = null, - ) {} + onShareClicked = {}, + onCollectClicked = {}, + ) } } @@ -285,8 +316,11 @@ private fun PreviewModalContentsWhenUserCannotSubmitData() { canUserSubmitData = false, submissionCount = 1, showDeleteLoiButton = false, + showShareButton = true, onDeleteClicked = null, - ) {} + onShareClicked = {}, + onCollectClicked = {}, + ) } } @@ -329,8 +363,11 @@ private fun PreviewModalContentsWhenJobHasTasks() { canUserSubmitData = true, submissionCount = 20, showDeleteLoiButton = false, + showShareButton = true, onDeleteClicked = null, - ) {} + onShareClicked = {}, + onCollectClicked = {}, + ) } } @@ -372,8 +409,11 @@ private fun PreviewModalContentsWhenJobHasTasksAndIsPredefined() { loi = loi, canUserSubmitData = true, submissionCount = 20, - showDeleteLoiButton = false, + showDeleteLoiButton = true, + showShareButton = true, onDeleteClicked = null, - ) {} + onShareClicked = {}, + onCollectClicked = {}, + ) } } diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt new file mode 100644 index 0000000000..6e1e2e9cb0 --- /dev/null +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt @@ -0,0 +1,122 @@ +/* + * 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.home.mapcontainer.jobs + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.groundplatform.android.R +import org.groundplatform.domain.model.locationofinterest.LoiReport +import org.groundplatform.ui.components.GroundQrCode +import org.groundplatform.ui.theme.AppTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ShareLocationModal(loiReport: LoiReport, onDismiss: () -> Unit) { + Dialog(onDismissRequest = onDismiss) { + Card( + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + ) { + Column(Modifier.padding(24.dp)) { + Text( + text = stringResource(R.string.share_location), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Normal, + ) + + Box( + modifier = + Modifier.fillMaxWidth() + .padding(vertical = 16.dp) + .background( + MaterialTheme.colorScheme.background, + RoundedCornerShape(12.dp), + ) // todo consider shapes + .padding(24.dp) + ) { + GroundQrCode( + modifier = Modifier.align(Alignment.Center), + title = loiReport.loiName, + footer = stringResource(R.string.scan_this_qr_to_download_geojson), + qrContent = loiReport.geoJson.toString(), + contentDescription = "QR code with LOI Geometry", + centerLogoPainter = painterResource(R.drawable.ground_logo), + ) + } + + TextButton( + modifier = Modifier.align(Alignment.End).padding(top = 16.dp), + onClick = onDismiss, + ) { + Text(text = stringResource(R.string.close)) + } + } + } + } +} + +@Preview +@Composable +private fun ShareLocationModalPreview() { + val testLoiReport = + LoiReport( + loiName = "Test LOI", + geoJson = + JsonObject( + mapOf( + "type" to JsonPrimitive("Feature"), + "properties" to JsonObject(mapOf("name" to JsonPrimitive("Point test"))), + "geometry" to + JsonObject( + mapOf( + "type" to JsonPrimitive("Point"), + "coordinates" to JsonArray(listOf(JsonPrimitive(-89.0), JsonPrimitive(41.0))), + ) + ), + ) + ), + ) + + AppTheme { + Surface(modifier = Modifier.fillMaxSize()) { + ShareLocationModal(loiReport = testLoiReport, onDismiss = {}) + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de6d9fff2b..fa31cf3702 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -238,6 +238,7 @@ Try standing still or waiting for a more accurate position Re-center - Share location and answers - Scan this QR code to download the GeoJson + Share location + Scan this QR code to see the GeoJson + Share From 73a592c4d143a479ba901ba231d8f452571438a4 Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 24 Mar 2026 18:42:23 +0100 Subject: [PATCH 09/18] fix tests --- .../datacollection/DataCollectionViewModel.kt | 6 +-- .../ui/home/mapcontainer/jobs/LoiJobSheet.kt | 2 +- .../org/groundplatform/android/FakeData.kt | 24 ++++++++++- .../DataCollectionFragmentTest.kt | 13 ++++-- .../HomeScreenMapContainerViewModelTest.kt | 16 ++++++-- .../mapcontainer/jobs/JobMapComponentTest.kt | 6 ++- .../home/mapcontainer/jobs/LoiJobSheetTest.kt | 40 ++++++++++++++++--- .../domain/usecases/GetLoiReportUseCase.kt | 8 +++- 8 files changed, 94 insertions(+), 21 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 eed7440c4c..4c615d9417 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 @@ -23,7 +23,6 @@ import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -31,7 +30,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.groundplatform.android.data.local.room.converter.SubmissionDeltasConverter import org.groundplatform.android.data.uuid.OfflineUuidGenerator import org.groundplatform.android.di.coroutines.ApplicationScope @@ -184,9 +182,7 @@ internal constructor( loiId = submittedLoiId, surveyId = st.surveyId, ) - withContext(Dispatchers.Main) { - _uiState.value = DataCollectionUiState.TaskSubmitted(loiReport) - } + _uiState.value = DataCollectionUiState.TaskSubmitted(loiReport) } } } diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheet.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheet.kt index f0310ffba2..b02a1816cf 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheet.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheet.kt @@ -74,7 +74,7 @@ fun LoiJobSheet( onShareClicked: () -> Unit, ) { val scope = rememberCoroutineScope() - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet( onDismissRequest = onDismiss, diff --git a/app/src/test/java/org/groundplatform/android/FakeData.kt b/app/src/test/java/org/groundplatform/android/FakeData.kt index 1597514d8e..fb337b532f 100644 --- a/app/src/test/java/org/groundplatform/android/FakeData.kt +++ b/app/src/test/java/org/groundplatform/android/FakeData.kt @@ -16,7 +16,10 @@ package org.groundplatform.android import kotlin.time.Clock -import org.groundplatform.android.model.imagery.OfflineArea +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject import org.groundplatform.android.ui.map.Feature import org.groundplatform.android.ui.map.gms.features.FeatureClusterItem import org.groundplatform.domain.model.Survey @@ -36,6 +39,7 @@ import org.groundplatform.domain.model.map.Bounds import org.groundplatform.domain.model.mutation.LocationOfInterestMutation import org.groundplatform.domain.model.mutation.Mutation import org.groundplatform.domain.model.mutation.SubmissionMutation +import org.groundplatform.domain.model.locationofinterest.LoiReport import org.groundplatform.domain.model.task.Condition import org.groundplatform.domain.model.task.MultipleChoice import org.groundplatform.domain.model.task.Task @@ -109,6 +113,24 @@ object FakeData { geometry = Point(Coordinates(0.0, 0.0)), ) + val LOCATION_OF_INTEREST_LOI_REPORT = + LoiReport( + loiName = "Unnamed point", + geoJson = + JsonObject( + mapOf( + "type" to JsonPrimitive("Feature"), + "properties" to buildJsonObject {}, + "geometry" to + JsonObject( + mapOf( + "type" to JsonPrimitive("Point"), + "coordinates" to JsonArray(listOf(JsonPrimitive(0.0), JsonPrimitive(0.0))), + ) + ), + ) + ), + ) val LOCATION_OF_INTEREST_FEATURE = Feature( id = LOCATION_OF_INTEREST.id, diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionFragmentTest.kt index 450acdff44..bdf1fdd95b 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/DataCollectionFragmentTest.kt @@ -38,6 +38,7 @@ import org.groundplatform.android.R import org.groundplatform.android.data.local.room.converter.SubmissionDeltasConverter import org.groundplatform.android.data.remote.FakeRemoteDataStore import org.groundplatform.android.data.sync.MutationSyncWorkManager +import org.groundplatform.android.getString import org.groundplatform.android.model.map.CameraPosition import org.groundplatform.android.repository.MutationRepository import org.groundplatform.android.repository.SubmissionRepository @@ -60,6 +61,7 @@ import org.groundplatform.domain.model.task.Option import org.groundplatform.domain.model.task.Task import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface import org.groundplatform.domain.repository.UserRepositoryInterface +import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -315,7 +317,7 @@ class DataCollectionFragmentTest : BaseHiltTest() { } @Test - fun `Clicking done on final task hides the navigation close button`() = runWithTestDispatcher { + fun `Clicking done on final task displays the close button and title`() = runWithTestDispatcher { setupFragment() runner() @@ -326,7 +328,10 @@ class DataCollectionFragmentTest : BaseHiltTest() { .selectOption(TASK_2_OPTION_LABEL) .clickDoneButton() // Click "done" on final task - assertThat(getToolbar()?.navigationIcon).isNull() + advanceUntilIdle() + + assertThat(getToolbar()?.navigationIcon).isNotNull() + assertThat(getToolbar()?.title).isEqualTo(getString(R.string.data_collection_complete)) } @Test @@ -652,9 +657,11 @@ class DataCollectionFragmentTest : BaseHiltTest() { .selectOption(TASK_2_OPTION_LABEL) .clickDoneButton() + advanceUntilIdle() + // Simulate state after task submission val state = fragment.viewModel.uiState.value - assertThat(state).isEqualTo(DataCollectionUiState.TaskSubmitted) + assertTrue(state is DataCollectionUiState.TaskSubmitted) } @Test diff --git a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt index addf25a6ec..6dff83cb88 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt @@ -29,6 +29,7 @@ import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData.ADHOC_JOB import org.groundplatform.android.FakeData.LOCATION_OF_INTEREST import org.groundplatform.android.FakeData.LOCATION_OF_INTEREST_FEATURE +import org.groundplatform.android.FakeData.LOCATION_OF_INTEREST_LOI_REPORT import org.groundplatform.android.FakeData.SURVEY import org.groundplatform.android.FakeData.USER import org.groundplatform.android.data.remote.FakeRemoteDataStore @@ -48,7 +49,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @OptIn(ExperimentalCoroutinesApi::class) @@ -76,8 +78,9 @@ class HomeScreenMapContainerViewModelTest : BaseHiltTest() { remoteDataStore.predefinedLois = listOf(LOCATION_OF_INTEREST) activateSurvey(SURVEY.id) advanceUntilIdle() - `when`(loiRepository.getWithinBounds(SURVEY, BOUNDS)) + whenever(loiRepository.getWithinBounds(SURVEY, BOUNDS)) .thenReturn(flowOf(listOf(LOCATION_OF_INTEREST))) + whenever(loiRepository.getOfflineLoi(any(), any())).thenReturn(LOCATION_OF_INTEREST) viewModel.onMapCameraMoved(CAMERA_POSITION) advanceUntilIdle() } @@ -87,10 +90,17 @@ class HomeScreenMapContainerViewModelTest : BaseHiltTest() { fun `renders the job card when zoomed into LOI and clicked on`() = runWithTestDispatcher { viewModel.onFeatureClicked(features = setOf(LOCATION_OF_INTEREST_FEATURE)) val state = viewModel.processJobMapComponentState().first() + advanceUntilIdle() assertThat(state) .isEqualTo( JobMapComponentState.LoiSelected( - SelectedLoiSheetData(canCollectData = true, LOCATION_OF_INTEREST, 0, true) + SelectedLoiSheetData( + canCollectData = true, + loi = LOCATION_OF_INTEREST, + submissionCount = 0, + showDeleteLoiButton = true, + loiReport = LOCATION_OF_INTEREST_LOI_REPORT, + ) ) ) } diff --git a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponentTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponentTest.kt index a1dd13f679..ba88f739df 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponentTest.kt @@ -27,6 +27,7 @@ import kotlin.test.assertTrue import org.groundplatform.android.FakeData.ADHOC_JOB import org.groundplatform.android.FakeData.JOB import org.groundplatform.android.FakeData.LOCATION_OF_INTEREST +import org.groundplatform.android.FakeData.LOCATION_OF_INTEREST_LOI_REPORT import org.groundplatform.android.FakeData.newTask import org.groundplatform.android.R import org.groundplatform.android.getString @@ -148,7 +149,7 @@ class JobMapComponentTest { @Test fun `LoiJobSheet should be shown when there is a selected LOI`() { val selectedLoiSheetData = - SelectedLoiSheetData(canCollectData = true, LOCATION_OF_INTEREST, 0, true) + SelectedLoiSheetData(canCollectData = true, LOCATION_OF_INTEREST, 0, true, LOCATION_OF_INTEREST_LOI_REPORT) setContent(JobMapComponentState.LoiSelected(selectedLoiSheetData)) composeTestRule @@ -161,7 +162,7 @@ class JobMapComponentTest { fun `Clicking to delete site in the LoiJobSheet should dispatch the OnDeleteSiteClicked action`() { val performedActions = mutableListOf() val selectedLoiSheetData = - SelectedLoiSheetData(canCollectData = true, LOCATION_OF_INTEREST, 0, true) + SelectedLoiSheetData(canCollectData = true, LOCATION_OF_INTEREST, 0, true, LOCATION_OF_INTEREST_LOI_REPORT) setContent( state = JobMapComponentState.LoiSelected(selectedLoiSheetData), onAction = { performedActions += it }, @@ -191,6 +192,7 @@ class JobMapComponentTest { ), submissionCount = 20, showDeleteLoiButton = false, + loiReport = LOCATION_OF_INTEREST_LOI_REPORT ) setContent( state = JobMapComponentState.LoiSelected(selectedLoiSheetData), diff --git a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheetTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheetTest.kt index b8f433921e..2be638ccc4 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheetTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheetTest.kt @@ -20,6 +20,9 @@ import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import kotlin.test.Test +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import org.groundplatform.android.FakeData import org.groundplatform.android.FakeData.USER import org.groundplatform.android.R @@ -30,6 +33,7 @@ import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.job.Style import org.groundplatform.domain.model.locationofinterest.AuditInfo import org.groundplatform.domain.model.locationofinterest.LocationOfInterest +import org.groundplatform.domain.model.locationofinterest.LoiReport import org.groundplatform.domain.model.task.Task import org.junit.Rule import org.junit.runner.RunWith @@ -84,15 +88,41 @@ class LoiJobSheetTest { private fun setContent(loi: LocationOfInterest, showDeleteLoiButton: Boolean = false) { composeTestRule.setContent { LoiJobSheet( - loi = loi, - canUserSubmitData = true, - submissionCount = 0, - showDeleteLoiButton = showDeleteLoiButton, + state = + SelectedLoiSheetData( + canCollectData = true, + submissionCount = 0, + loi = loi, + showDeleteLoiButton = showDeleteLoiButton, + loiReport = getLoiReport(loi.id), + ), onCollectClicked = {}, - ) {} + onDismiss = {}, + onShareClicked = {}, + ) } } + private fun getLoiReport(name: String): LoiReport { + return LoiReport( + loiName = name, + geoJson = + JsonObject( + mapOf( + "type" to JsonPrimitive("Feature"), + "properties" to JsonObject(mapOf("name" to JsonPrimitive(name))), + "geometry" to + JsonObject( + mapOf( + "type" to JsonPrimitive("Point"), + "coordinates" to JsonArray(listOf(JsonPrimitive(20.0), JsonPrimitive(20.0))), + ) + ), + ) + ), + ) + } + companion object { private val NO_TASK_LOI = LocationOfInterest( diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt index 2dc32db36f..7d69f8086c 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt @@ -26,6 +26,7 @@ import org.groundplatform.domain.model.geometry.LinearRing import org.groundplatform.domain.model.geometry.MultiPolygon import org.groundplatform.domain.model.geometry.Point import org.groundplatform.domain.model.geometry.Polygon +import org.groundplatform.domain.model.locationofinterest.LOI_NAME_PROPERTY import org.groundplatform.domain.model.locationofinterest.LoiProperties import org.groundplatform.domain.model.locationofinterest.LoiReport import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface @@ -47,7 +48,12 @@ class GetLoiReportUseCase( */ suspend operator fun invoke(loiName: String, loiId: String, surveyId: String): LoiReport? { val loi = locationOfInterestRepository.getOfflineLoi(surveyId, loiId) - return loi?.let { LoiReport(loiName, it.geometry.toGeoJson(it.properties)) } + return loi?.let { + LoiReport( + loiName, + it.geometry.toGeoJson(it.properties.filter { property -> property.key == LOI_NAME_PROPERTY }), + ) + } } /** From c7a181f962cba19889b8c83bfd0b73bca44e369d Mon Sep 17 00:00:00 2001 From: andreia Date: Wed, 25 Mar 2026 17:44:59 +0100 Subject: [PATCH 10/18] optimize coordinates max decimals to 6 --- .../domain/usecases/GetLoiReportUseCase.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt index 7d69f8086c..bb82ba145b 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt @@ -15,6 +15,7 @@ */ package org.groundplatform.domain.usecases +import kotlin.math.round import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject @@ -51,7 +52,9 @@ class GetLoiReportUseCase( return loi?.let { LoiReport( loiName, - it.geometry.toGeoJson(it.properties.filter { property -> property.key == LOI_NAME_PROPERTY }), + it.geometry.toGeoJson( + it.properties.filter { property -> property.key == LOI_NAME_PROPERTY } + ), ) } } @@ -94,8 +97,14 @@ class GetLoiReportUseCase( JsonObject(mapOf(KEY_TYPE to JsonPrimitive(type), KEY_COORDINATES to coordinates)) /** Converts a single [Coordinates] to a GeoJSON position: [lng, lat]. */ - private fun coordinatesToPosition(coordinates: Coordinates): JsonArray = - JsonArray(listOf(JsonPrimitive(coordinates.lng), JsonPrimitive(coordinates.lat))) + private fun coordinatesToPosition(coordinates: Coordinates): JsonArray { + return JsonArray( + listOf( + JsonPrimitive(coordinates.lng.roundTo6Decimals()), + JsonPrimitive(coordinates.lat.roundTo6Decimals()), + ) + ) + } /** Converts a list of [Coordinates] to a GeoJSON array of positions. */ private fun coordinatesToPositions(coordinates: List): JsonArray = @@ -108,6 +117,8 @@ class GetLoiReportUseCase( return JsonArray(rings) } + private fun Double.roundTo6Decimals(): Double = round(this * 1e6) / 1e6 + private companion object { const val KEY_TYPE = "type" const val TYPE_FEATURE = "Feature" From 71bf1ee9b15c6be20f256e3ab0c348134aba0997 Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 26 Mar 2026 16:23:08 +0100 Subject: [PATCH 11/18] fix rebase issues --- .../DataSubmissionConfirmationScreen.kt | 4 +- .../mapcontainer/jobs/ShareLocationModal.kt | 5 +- .../ui/components/qrcode/GroundQrCode.kt | 61 +++++++++++++------ 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt index df776a79db..425d1b912f 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt @@ -54,7 +54,7 @@ import kotlinx.serialization.json.JsonPrimitive import org.groundplatform.android.R import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport import org.groundplatform.domain.model.locationofinterest.LoiReport -import org.groundplatform.ui.components.GroundQrCode +import org.groundplatform.ui.components.qrcode.GroundQrCode import org.groundplatform.ui.theme.AppTheme private val DEFAULT_TOOLBAR_HEIGHT = 56.dp @@ -154,7 +154,7 @@ private fun ShareableContent(modifier: Modifier = Modifier, loiReport: LoiReport modifier = Modifier.align(Alignment.Center), title = loiReport.loiName, footer = stringResource(R.string.scan_this_qr_to_download_geojson), - qrContent = loiReport.geoJson.toString(), + content = loiReport.geoJson.toString(), contentDescription = "QR code with LOI Geometry", centerLogoPainter = painterResource(R.drawable.ground_logo), ) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt index 6e1e2e9cb0..0758f17f85 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -43,7 +44,7 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import org.groundplatform.android.R import org.groundplatform.domain.model.locationofinterest.LoiReport -import org.groundplatform.ui.components.GroundQrCode +import org.groundplatform.ui.components.qrcode.GroundQrCode import org.groundplatform.ui.theme.AppTheme @OptIn(ExperimentalMaterial3Api::class) @@ -75,7 +76,7 @@ fun ShareLocationModal(loiReport: LoiReport, onDismiss: () -> Unit) { modifier = Modifier.align(Alignment.Center), title = loiReport.loiName, footer = stringResource(R.string.scan_this_qr_to_download_geojson), - qrContent = loiReport.geoJson.toString(), + content = loiReport.geoJson.toString(), contentDescription = "QR code with LOI Geometry", centerLogoPainter = painterResource(R.drawable.ground_logo), ) diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/components/qrcode/GroundQrCode.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/components/qrcode/GroundQrCode.kt index 6d779d805d..7625902a28 100644 --- a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/components/qrcode/GroundQrCode.kt +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/components/qrcode/GroundQrCode.kt @@ -17,13 +17,16 @@ package org.groundplatform.ui.components.qrcode import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState @@ -33,6 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.Dispatchers @@ -65,9 +69,11 @@ private const val LOGO_SIZE_FRACTION = 0.15f @Composable fun GroundQrCode( modifier: Modifier = Modifier, + title: String, content: String, contentDescription: String, centerLogoPainter: Painter?, + footer: String, ) { val contentBytes = remember(content) { content.encodeToByteArray().size } val showLogo = centerLogoPainter != null && contentBytes <= MAX_QR_BYTES_WITH_LOGO @@ -77,30 +83,43 @@ fun GroundQrCode( value = withContext(Dispatchers.Default) { generateQrBitmap(content, showLogo) } } - Box(modifier = modifier.sizeIn(minWidth = 142.dp, minHeight = 142.dp)) { - qrBitmap?.let { - Image( - modifier = Modifier.align(Alignment.Center), - bitmap = it, - contentDescription = contentDescription, - contentScale = ContentScale.Fit, - ) - if (showLogo) { + Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Box(modifier = modifier.sizeIn(minWidth = 142.dp, minHeight = 142.dp)) { + qrBitmap?.let { Image( - painter = centerLogoPainter, - contentDescription = null, - modifier = Modifier.align(Alignment.Center).fillMaxSize(LOGO_SIZE_FRACTION), + modifier = Modifier.align(Alignment.Center), + bitmap = it, + contentDescription = contentDescription, + contentScale = ContentScale.Fit, ) + if (showLogo) { + Image( + painter = centerLogoPainter, + contentDescription = null, + modifier = Modifier.align(Alignment.Center).fillMaxSize(LOGO_SIZE_FRACTION), + ) + } } + ?: run { + CircularProgressIndicator( + modifier = + Modifier.size(MaterialTheme.sizes.progressIndicatorSize).align(Alignment.Center), + color = MaterialTheme.colorScheme.primary, + strokeWidth = MaterialTheme.sizes.progressIndicatorStrokeWidth, + ) + } } - ?: run { - CircularProgressIndicator( - modifier = - Modifier.size(MaterialTheme.sizes.progressIndicatorSize).align(Alignment.Center), - color = MaterialTheme.colorScheme.primary, - strokeWidth = MaterialTheme.sizes.progressIndicatorStrokeWidth, - ) - } + Text( + text = footer, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } @@ -111,8 +130,10 @@ private fun GroundQrCodePreview() { Surface { GroundQrCode( modifier = Modifier.padding(16.dp).size(400.dp), + title = "Test QR code", content = "https://www.google.com", contentDescription = "Google", + footer = "Scan this QR", centerLogoPainter = null, ) } From 8011d0f73e603550e05e71916721266098a2679e Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 31 Mar 2026 16:16:24 +0200 Subject: [PATCH 12/18] fix code formatting --- .../ui/datacollection/DataCollectionUiState.kt | 2 +- .../mapcontainer/jobs/ShareLocationModal.kt | 1 - .../org/groundplatform/android/FakeData.kt | 2 +- .../mapcontainer/jobs/JobMapComponentTest.kt | 18 +++++++++++++++--- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionUiState.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionUiState.kt index 07c3782a5d..ff9aa9b81b 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionUiState.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionUiState.kt @@ -16,8 +16,8 @@ package org.groundplatform.android.ui.datacollection import org.groundplatform.domain.model.job.Job -import org.groundplatform.domain.model.task.Task import org.groundplatform.domain.model.locationofinterest.LoiReport +import org.groundplatform.domain.model.task.Task /** * Top-level UI state for the Data Collection flow. diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt index 0758f17f85..f333b80da1 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults diff --git a/app/src/test/java/org/groundplatform/android/FakeData.kt b/app/src/test/java/org/groundplatform/android/FakeData.kt index fb337b532f..89bbdd2167 100644 --- a/app/src/test/java/org/groundplatform/android/FakeData.kt +++ b/app/src/test/java/org/groundplatform/android/FakeData.kt @@ -35,11 +35,11 @@ import org.groundplatform.domain.model.job.Style import org.groundplatform.domain.model.locationofinterest.AuditInfo import org.groundplatform.domain.model.locationofinterest.LOI_NAME_PROPERTY import org.groundplatform.domain.model.locationofinterest.LocationOfInterest +import org.groundplatform.domain.model.locationofinterest.LoiReport import org.groundplatform.domain.model.map.Bounds import org.groundplatform.domain.model.mutation.LocationOfInterestMutation import org.groundplatform.domain.model.mutation.Mutation import org.groundplatform.domain.model.mutation.SubmissionMutation -import org.groundplatform.domain.model.locationofinterest.LoiReport import org.groundplatform.domain.model.task.Condition import org.groundplatform.domain.model.task.MultipleChoice import org.groundplatform.domain.model.task.Task diff --git a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponentTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponentTest.kt index ba88f739df..46926dffa5 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponentTest.kt @@ -149,7 +149,13 @@ class JobMapComponentTest { @Test fun `LoiJobSheet should be shown when there is a selected LOI`() { val selectedLoiSheetData = - SelectedLoiSheetData(canCollectData = true, LOCATION_OF_INTEREST, 0, true, LOCATION_OF_INTEREST_LOI_REPORT) + SelectedLoiSheetData( + canCollectData = true, + LOCATION_OF_INTEREST, + 0, + true, + LOCATION_OF_INTEREST_LOI_REPORT, + ) setContent(JobMapComponentState.LoiSelected(selectedLoiSheetData)) composeTestRule @@ -162,7 +168,13 @@ class JobMapComponentTest { fun `Clicking to delete site in the LoiJobSheet should dispatch the OnDeleteSiteClicked action`() { val performedActions = mutableListOf() val selectedLoiSheetData = - SelectedLoiSheetData(canCollectData = true, LOCATION_OF_INTEREST, 0, true, LOCATION_OF_INTEREST_LOI_REPORT) + SelectedLoiSheetData( + canCollectData = true, + LOCATION_OF_INTEREST, + 0, + true, + LOCATION_OF_INTEREST_LOI_REPORT, + ) setContent( state = JobMapComponentState.LoiSelected(selectedLoiSheetData), onAction = { performedActions += it }, @@ -192,7 +204,7 @@ class JobMapComponentTest { ), submissionCount = 20, showDeleteLoiButton = false, - loiReport = LOCATION_OF_INTEREST_LOI_REPORT + loiReport = LOCATION_OF_INTEREST_LOI_REPORT, ) setContent( state = JobMapComponentState.LoiSelected(selectedLoiSheetData), From ca74d2e2da3a2225c4d022198393622bc080f0ea Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 31 Mar 2026 17:49:31 +0200 Subject: [PATCH 13/18] add test to GetLoiReportUseCaseTest --- .../org/groundplatform/android/FakeData.kt | 1 + .../usecases/GetLoiReportUseCaseTest.kt | 60 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/org/groundplatform/android/FakeData.kt b/app/src/test/java/org/groundplatform/android/FakeData.kt index 89bbdd2167..10a477d016 100644 --- a/app/src/test/java/org/groundplatform/android/FakeData.kt +++ b/app/src/test/java/org/groundplatform/android/FakeData.kt @@ -20,6 +20,7 @@ import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject +import org.groundplatform.android.model.imagery.OfflineArea import org.groundplatform.android.ui.map.Feature import org.groundplatform.android.ui.map.gms.features.FeatureClusterItem import org.groundplatform.domain.model.Survey diff --git a/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt index ae4e4d295f..8829c990fb 100644 --- a/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt @@ -242,9 +242,67 @@ class GetLoiReportUseCaseTest { } } + @Test + fun `Should round coordinates to 6 decimals`() = runTest { + val lineString = + LineString( + listOf( + Coordinates(1.123456789, 2.987654321), + Coordinates(3.123456789, 4.987654321), + Coordinates(5.123456789, 6.987654321), + ) + ) + val loiReport = + invokeUseCase(geometry = lineString, properties = generateProperties("Rounding test")) + + val expectedGeoJson = + """ + { + "type": "Feature", + "properties": {"name": "Rounding test"}, + "geometry": { + "type": "LineString", + "coordinates": [ + [2.987654, 1.123457], + [4.987654, 3.123457], + [6.987654, 5.123457] + ] + } + } + """ + .trimIndent() + + assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + } + + @Test + fun `Should only include name property even if more are provided`() = runTest { + val properties = + generateProperties("Name test") + + mapOf("description" to "Should be removed", "extra" to "Also removed") + + val loiReport = + invokeUseCase(geometry = Point(Coordinates(lat = 0.0, lng = 0.0)), properties = properties) + + val expectedGeoJson = + """ + { + "type": "Feature", + "properties": {"name": "Name test"}, + "geometry": { + "type": "Point", + "coordinates": [0.0, 0.0] + } + } + """ + .trimIndent() + + assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + } + private suspend fun invokeUseCase(geometry: Geometry, properties: LoiProperties): LoiReport { loiRepository.offlineLoi = loiRepository.offlineLoi.copy(geometry = geometry, properties = properties) - return getLoiReportUseCase.invoke("loiId", "surveyId")!! + return getLoiReportUseCase.invoke("loiName", "loiId", "surveyId")!! } } From 78de376d1b6993d55ca0798af18bbdacea15be6c Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 31 Mar 2026 17:54:43 +0200 Subject: [PATCH 14/18] fix code format --- .../ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt index 4dbee512ee..e7618b1d1d 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt @@ -60,8 +60,8 @@ import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.locationofinterest.LocationOfInterest import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface -import org.groundplatform.domain.usecases.GetLoiReportUseCase import org.groundplatform.domain.repository.UserRepositoryInterface +import org.groundplatform.domain.usecases.GetLoiReportUseCase @OptIn(ExperimentalCoroutinesApi::class) @SharedViewModel From 64021474d8294dbd2bd15fafe7365f45f27aff10 Mon Sep 17 00:00:00 2001 From: andreia Date: Wed, 1 Apr 2026 11:05:01 +0200 Subject: [PATCH 15/18] remove unused code --- app/src/main/res/drawable/data_submitted.xml | 1068 ----------------- app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-lo/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-th/strings.xml | 1 - app/src/main/res/values-vi/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - .../home/mapcontainer/jobs/LoiJobSheetTest.kt | 5 +- .../domain/usecases/GetLoiReportUseCase.kt | 5 +- 10 files changed, 4 insertions(+), 1081 deletions(-) delete mode 100644 app/src/main/res/drawable/data_submitted.xml diff --git a/app/src/main/res/drawable/data_submitted.xml b/app/src/main/res/drawable/data_submitted.xml deleted file mode 100644 index b1f5f1ed3c..0000000000 --- a/app/src/main/res/drawable/data_submitted.xml +++ /dev/null @@ -1,1068 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8039f6d9a3..a9255fe10c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -184,7 +184,6 @@ Datos no guardados restaurados No se pudo conectar, intente de nuevo más tarde - Datos de imagen enviados ¡Recolección de datos completada! Sus datos se han guardado y se sincronizarán automáticamente cuando esté en línea. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 07b69a30ea..6cda5873a4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -171,7 +171,6 @@ Données non sauvegardées ont été restaurées Impossible de se connecter, réessayer plus tard - Données d’image soumises Collecte des données terminée ! Vos données ont été sauvegardées et seront automatiquement synchronisées lorsque vous serez en ligne. Autoriser la localisation diff --git a/app/src/main/res/values-lo/strings.xml b/app/src/main/res/values-lo/strings.xml index 21072161b3..08ecd68150 100644 --- a/app/src/main/res/values-lo/strings.xml +++ b/app/src/main/res/values-lo/strings.xml @@ -167,7 +167,6 @@ \nຜູ້ຈັດການແບບສຳຫຼວດສາມາດແບ່ງປັນ ແລະ ນຳໃຊ້ຂໍ້ມູນຕໍ່ສາທາລະນະ ພາຍໃຕ້ໃບອະນຸຍາດ *Creative Commons CC0 1.0* ກູ້ຄືນຂໍ້ມູນທີ່ຍັງບໍ່ໄດ້ບັນທຶກ ບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້ ກະລຸນາລອງໃໝ່ໃນພາຍຫຼັງ - ຮູບພາບການສົ່ງຂໍ້ມູນ ການເກັບຂໍ້ມູນສຳເລັດແລ້ວ! ຂໍ້ມູນຂອງທ່ານຖືກບັນທຶກແລ້ວ ແລະ ຈະຖືກຊິງອັດຕະໂນມັດເມື່ອທ່ານອອນລາຍ ອະນຸຍາດການລະບຸຕຳແໜ່ງ diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index f71f162528..8ce4c54cbf 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -185,7 +185,6 @@ Os dados não salvos foram restaurados Não foi possível conectar, tente novamente mais tarde - Dados da imagem enviada Coleta de dados concluída! Seus dados foram salvos e serão sincronizados automaticamente quando você estiver online. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 2ff810b821..3982d98ece 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -169,7 +169,6 @@ บันทึกข้อมูลที่ยังไม่ได้บันทึกถูกกู้คืนแล้ว ไม่สามารถเชื่อมต่อได้ กรุณาลองใหม่ภายหลัง - ภาพการส่งข้อมูล การเก็บข้อมูลเสร็จสมบูรณ์! ข้อมูลของคุณถูกบันทึกแล้ว และจะซิงค์อัตโนมัติเมื่อคุณเชื่อมต่ออินเทอร์เน็ต อนุญาตการเข้าถึงตำแหน่ง diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index a7b042b342..f433905e07 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -166,7 +166,6 @@ \nNgười tổ chức khảo sát có thể chia sẻ và sử dụng dữ liệu công khai theo Giấy phép *Creative Commons CC0 1.0* Dữ liệu chưa lưu đã được khôi phục Không thể kết nối, vui lòng thử lại sau - Ảnh đã được gửi kèm dữ liệu Thu thập dữ liệu hoàn tất! Dữ liệu của bạn đã được lưu và sẽ tự động đồng bộ khi bạn trực tuyến. Cho phép truy cập vị trí diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa31cf3702..432ac18816 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -184,7 +184,6 @@ Unsaved data restored Could not connect, try again later - Data submitted image Data collection complete! Your data has been saved and will be automatically synced when you’re online. diff --git a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheetTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheetTest.kt index 2be638ccc4..ad6ee20374 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheetTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/LoiJobSheetTest.kt @@ -103,8 +103,8 @@ class LoiJobSheetTest { } } - private fun getLoiReport(name: String): LoiReport { - return LoiReport( + private fun getLoiReport(name: String): LoiReport = + LoiReport( loiName = name, geoJson = JsonObject( @@ -121,7 +121,6 @@ class LoiJobSheetTest { ) ), ) - } companion object { private val NO_TASK_LOI = diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt index bb82ba145b..bee7a36003 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt @@ -97,14 +97,13 @@ class GetLoiReportUseCase( JsonObject(mapOf(KEY_TYPE to JsonPrimitive(type), KEY_COORDINATES to coordinates)) /** Converts a single [Coordinates] to a GeoJSON position: [lng, lat]. */ - private fun coordinatesToPosition(coordinates: Coordinates): JsonArray { - return JsonArray( + private fun coordinatesToPosition(coordinates: Coordinates): JsonArray = + JsonArray( listOf( JsonPrimitive(coordinates.lng.roundTo6Decimals()), JsonPrimitive(coordinates.lat.roundTo6Decimals()), ) ) - } /** Converts a list of [Coordinates] to a GeoJSON array of positions. */ private fun coordinatesToPositions(coordinates: List): JsonArray = From 89305186ac9477e9eff0de33047af821a8aa6c09 Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 2 Apr 2026 16:50:31 +0200 Subject: [PATCH 16/18] move modal declaration after LoiJobSheet to avoid wrong overlap on screen rotation --- .../android/ui/home/mapcontainer/jobs/JobMapComponent.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponent.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponent.kt index 1faab401d0..5a73e513f6 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponent.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/JobMapComponent.kt @@ -46,12 +46,9 @@ import org.groundplatform.ui.theme.AppTheme @Composable fun JobMapComponent(state: JobMapComponentState, onAction: (JobMapComponentAction) -> Unit) { - var showShareLoiModal by rememberSaveable { mutableStateOf(false) } when (state) { is JobMapComponentState.LoiSelected -> { - if (showShareLoiModal && state.loi.loiReport != null) { - ShareLocationModal(state.loi.loiReport) { showShareLoiModal = false } - } + var showShareLoiModal by rememberSaveable { mutableStateOf(false) } LoiJobSheet( state = state.loi, @@ -60,6 +57,10 @@ fun JobMapComponent(state: JobMapComponentState, onAction: (JobMapComponentActio onDismiss = { onAction(JobMapComponentAction.OnJobCardDismissed) }, onShareClicked = { showShareLoiModal = true }, ) + + if (showShareLoiModal && state.loi.loiReport != null) { + ShareLocationModal(state.loi.loiReport) { showShareLoiModal = false } + } } is JobMapComponentState.AddLoiButton -> { AddLoiButton(onClick = { onAction(JobMapComponentAction.OnAddLoiButtonClicked) }) From 8f35a78bc523ae1d5d9d491c63e51b4bc691162d Mon Sep 17 00:00:00 2001 From: andreia Date: Fri, 3 Apr 2026 17:20:43 +0200 Subject: [PATCH 17/18] update scan qr label --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 432ac18816..b0bcb94950 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -238,6 +238,6 @@ Re-center Share location - Scan this QR code to see the GeoJson + Scan this QR code to view the GeoJson Share From e936d2818c0cb674b0339302fbccc2b380c81430 Mon Sep 17 00:00:00 2001 From: andreia Date: Fri, 3 Apr 2026 18:09:09 +0200 Subject: [PATCH 18/18] add tests to ShareLocationModal --- .../mapcontainer/jobs/ShareLocationModal.kt | 5 +- .../jobs/ShareLocationModalTest.kt | 66 +++++++++++++++++++ .../ui/components/qrcode/GroundQrCode.kt | 9 ++- 3 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModalTest.kt diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt index f333b80da1..165d80bba9 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt @@ -65,10 +65,7 @@ fun ShareLocationModal(loiReport: LoiReport, onDismiss: () -> Unit) { modifier = Modifier.fillMaxWidth() .padding(vertical = 16.dp) - .background( - MaterialTheme.colorScheme.background, - RoundedCornerShape(12.dp), - ) // todo consider shapes + .background(MaterialTheme.colorScheme.background, RoundedCornerShape(12.dp)) .padding(24.dp) ) { GroundQrCode( diff --git a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModalTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModalTest.kt new file mode 100644 index 0000000000..d51c63d323 --- /dev/null +++ b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModalTest.kt @@ -0,0 +1,66 @@ +/* + * 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.home.mapcontainer.jobs + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import kotlin.test.Test +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.groundplatform.android.R +import org.groundplatform.android.getString +import org.groundplatform.domain.model.locationofinterest.LoiReport +import org.groundplatform.ui.components.qrcode.TEST_TAG_GROUND_QR_CODE +import org.groundplatform.ui.theme.AppTheme +import org.junit.Rule +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ShareLocationModalTest { + @get:Rule val composeTestRule = createComposeRule() + + @Test + fun `Modal is displayed correctly and shows the QR code with the LOI geometry`() { + composeTestRule.setContent { + AppTheme { ShareLocationModal(loiReport = LoiReport(LOI_NAME, LOI_GEO_JSON), onDismiss = {}) } + } + composeTestRule.onNodeWithText(getString(R.string.share_location)).assertIsDisplayed() + composeTestRule.onNodeWithText(LOI_NAME).assertIsDisplayed() + composeTestRule.onNodeWithTag(TEST_TAG_GROUND_QR_CODE).assertIsDisplayed() + } + + private companion object { + const val LOI_NAME = "Test Loi" + val LOI_GEO_JSON = + JsonObject( + mapOf( + "type" to JsonPrimitive("Feature"), + "properties" to JsonObject(mapOf("name" to JsonPrimitive(LOI_NAME))), + "geometry" to + JsonObject( + mapOf( + "type" to JsonPrimitive("Point"), + "coordinates" to JsonArray(listOf(JsonPrimitive(20.0), JsonPrimitive(20.0))), + ) + ), + ) + ) + } +} diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/components/qrcode/GroundQrCode.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/components/qrcode/GroundQrCode.kt index 7625902a28..10093dbecb 100644 --- a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/components/qrcode/GroundQrCode.kt +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/components/qrcode/GroundQrCode.kt @@ -15,6 +15,7 @@ */ package org.groundplatform.ui.components.qrcode +import androidx.annotation.VisibleForTesting import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -36,6 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -43,6 +45,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.groundplatform.ui.theme.sizes +@VisibleForTesting const val TEST_TAG_GROUND_QR_CODE = "TEST_TAG_GROUND_QR_CODE" + /** * Maximum content size (in UTF-8 bytes) for which a center logo is displayed. * @@ -83,7 +87,10 @@ fun GroundQrCode( value = withContext(Dispatchers.Default) { generateQrBitmap(content, showLogo) } } - Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Column( + modifier = modifier.fillMaxWidth().testTag(TEST_TAG_GROUND_QR_CODE), + horizontalAlignment = Alignment.CenterHorizontally, + ) { Text( text = title, style = MaterialTheme.typography.titleMedium,