diff --git a/app/src/main/java/com/google/android/ground/ui/compose/ConfirmationDialog.kt b/app/src/main/java/com/google/android/ground/ui/compose/ConfirmationDialog.kt index b7d00f40ef..83762b2155 100644 --- a/app/src/main/java/com/google/android/ground/ui/compose/ConfirmationDialog.kt +++ b/app/src/main/java/com/google/android/ground/ui/compose/ConfirmationDialog.kt @@ -20,8 +20,13 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.google.android.ground.ExcludeFromJacocoGeneratedReport import com.google.android.ground.R +import com.google.android.ground.ui.theme.AppTheme @Composable fun ConfirmationDialog( @@ -29,18 +34,44 @@ fun ConfirmationDialog( @StringRes description: Int, @StringRes dismissButtonText: Int = R.string.cancel, @StringRes confirmButtonText: Int, - signOutCallback: () -> Unit, - dismissCallback: () -> Unit, + onConfirmClicked: () -> Unit, ) { - AlertDialog( - onDismissRequest = { dismissCallback() }, - title = { Text(text = stringResource(title)) }, - text = { Text(text = stringResource(description)) }, - dismissButton = { - TextButton(onClick = { dismissCallback() }) { Text(text = stringResource(dismissButtonText)) } - }, - confirmButton = { - TextButton(onClick = { signOutCallback() }) { Text(text = stringResource(confirmButtonText)) } - }, - ) + val showRemoveWarningDialog = remember { mutableStateOf(true) } + + fun onConfirm() { + showRemoveWarningDialog.value = false + onConfirmClicked() + } + + fun onDismiss() { + showRemoveWarningDialog.value = false + } + + if (showRemoveWarningDialog.value) { + AlertDialog( + onDismissRequest = { onDismiss() }, + title = { Text(text = stringResource(title)) }, + text = { Text(text = stringResource(description)) }, + dismissButton = { + TextButton(onClick = { onDismiss() }) { Text(text = stringResource(dismissButtonText)) } + }, + confirmButton = { + TextButton(onClick = { onConfirm() }) { Text(text = stringResource(confirmButtonText)) } + }, + ) + } +} + +@Composable +@Preview +@ExcludeFromJacocoGeneratedReport +fun PreviewConfirmationDialog() { + AppTheme { + ConfirmationDialog( + title = R.string.data_collection_cancellation_title, + description = R.string.data_collection_cancellation_description, + confirmButtonText = R.string.data_collection_cancellation_confirm_button, + onConfirmClicked = {}, + ) + } } diff --git a/app/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt b/app/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt index a86d43d864..6a1a6f1f02 100644 --- a/app/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt +++ b/app/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt @@ -37,6 +37,7 @@ import com.google.android.ground.databinding.DataCollectionFragBinding import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.AbstractFragment import com.google.android.ground.ui.common.BackPressListener +import com.google.android.ground.ui.compose.ConfirmationDialog import com.google.android.ground.ui.home.HomeScreenFragmentDirections import com.google.android.ground.ui.theme.AppTheme import dagger.hilt.android.AndroidEntryPoint @@ -71,11 +72,7 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { guideline = binding.progressBarGuideline getAbstractActivity().setSupportActionBar(binding.dataCollectionToolbar) - binding.dataCollectionToolbar.setNavigationOnClickListener { - isNavigatingUp = true - viewModel.clearDraft() - findNavController().navigateUp() - } + binding.dataCollectionToolbar.setNavigationOnClickListener { showExitWarningDialog() } return binding.root } @@ -196,18 +193,39 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { } } - override fun onBack(): Boolean = + override fun onBack(): Boolean { if (viewPager.currentItem == 0) { - isNavigatingUp = true - // If the user is currently looking at the first step, allow the system to handle the - // Back button. This calls finish() on this activity and pops the back stack. - viewModel.clearDraft() - false + showExitWarningDialog() } else { - // Otherwise, select the previous task. viewModel.moveToPreviousTask() - true } + return true + } + + private fun showExitWarningDialog() { + showConfirmationDialog { + isNavigatingUp = true + viewModel.clearDraft() + findNavController().navigateUp() + } + } + + private fun showConfirmationDialog(onConfirm: () -> Unit) { + (view as ViewGroup).addView( + ComposeView(requireContext()).apply { + setContent { + AppTheme { + ConfirmationDialog( + title = R.string.data_collection_cancellation_title, + description = R.string.data_collection_cancellation_description, + confirmButtonText = R.string.data_collection_cancellation_confirm_button, + onConfirmClicked = { onConfirm() }, + ) + } + } + } + ) + } private companion object { private const val PROGRESS_SCALE = 100 diff --git a/app/src/main/java/com/google/android/ground/ui/surveyselector/SurveySelectorFragment.kt b/app/src/main/java/com/google/android/ground/ui/surveyselector/SurveySelectorFragment.kt index 28f6f17837..1fad6e5676 100644 --- a/app/src/main/java/com/google/android/ground/ui/surveyselector/SurveySelectorFragment.kt +++ b/app/src/main/java/com/google/android/ground/ui/surveyselector/SurveySelectorFragment.kt @@ -22,9 +22,6 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.PopupMenu -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.compose.ui.platform.ComposeView import androidx.navigation.fragment.findNavController import com.google.android.ground.R @@ -124,7 +121,7 @@ class SurveySelectorFragment : AbstractFragment(), BackPressListener { object : PopupMenu.OnMenuItemClickListener { override fun onMenuItemClick(item: MenuItem): Boolean { if (item.itemId == R.id.remove_offline_access_menu_item) { - showRemoveConfirmationDialogs { viewModel.deleteSurvey(surveyId) } + showConfirmationDialog { viewModel.deleteSurvey(surveyId) } return true } return false @@ -135,30 +132,21 @@ class SurveySelectorFragment : AbstractFragment(), BackPressListener { } } - @Suppress("UnrememberedMutableState") - private fun showRemoveConfirmationDialogs(onConfirm: () -> Unit) { - val dialogComposeView = + private fun showConfirmationDialog(onConfirm: () -> Unit) { + (view as ViewGroup).addView( ComposeView(requireContext()).apply { setContent { - var showRemoveWarningDialog by mutableStateOf(true) - AppTheme { - if (showRemoveWarningDialog) { - ConfirmationDialog( - title = R.string.remove_offline_access_warning_title, - description = R.string.remove_offline_access_warning_dialog_body, - confirmButtonText = R.string.remove_offline_access_warning_confirm_button, - signOutCallback = { - onConfirm() - showRemoveWarningDialog = false - }, - dismissCallback = { showRemoveWarningDialog = false }, - ) - } + ConfirmationDialog( + title = R.string.remove_offline_access_warning_title, + description = R.string.remove_offline_access_warning_dialog_body, + confirmButtonText = R.string.remove_offline_access_warning_confirm_button, + onConfirmClicked = { onConfirm() }, + ) } } } - (view as ViewGroup).addView(dialogComposeView) + ) } private fun shouldExitApp(): Boolean = diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index a2e697845e..91bbe9c56d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -195,4 +195,8 @@ Permitir ubicación Permitir el uso compartido de la ubicación Si no permite que Ground acceda a la ubicación de este dispositivo, no podrá continuar recopilando datos para este sitio. + + ¿Quiere parar de recopilar datos? + Si sale, los datos que aún no hayan enviado se descartarán. + Confirmar diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0ec9ebc723..16e46c441a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -70,7 +70,7 @@ Ajouter point Finir - + Carte routière Satellite Relief @@ -195,4 +195,8 @@ Autoriser la localisation Autoriser le partage de la localisation Si vous n’autorisez pas Ground à accéder à la localisation de cet appareil, vous ne pourrez pas continuer à collecter des données pour ce site. + + Arrêter la collecte de données ? + Si vous quittez, les données que vous n’avez pas encore soumises seront supprimées. + Confirmer diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 77c4e40af0..1aba439497 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -133,7 +133,7 @@ Localização do mapa: Aviso de fim de sessão Se você terminar sua sessão:\n \u2022 Quaisquer dados não sincronizados serão descartados.\n \u2022 Você não poderá iniciar sessão se estiver offline\n \u2022 Inquéritos offline não estarão disponíveis até você iniciar sessão novamente\n\nTem certeza de que deseja sair? - + Amplie para começar a coletar dados Inquérito apenas para leitura Amplie até um local de coleta de dados para coletar informações @@ -154,7 +154,7 @@ Sobre Termos de serviço Ground é um projeto comunitário de código aberto desenvolvido pelo Google e pela FAO no âmbito da “Forest Data Partnership”, com a ajuda do SIG, Ecam e dos contribuintes da comunidade de código aberto. \n\nEle é licenciado sob a Apache License, Version 2.0. \n\nVeja outras licenças - + Falhou Sincronizado Sincronizando @@ -196,4 +196,8 @@ Permitir localizaçã Permitir compartilhamento de localização Se você não permitir que o Ground acesse a localização deste dispositivo, não poderá continuar coletando dados para este local. + + Parar de coletar dados? + Se você sair, os dados que ainda não enviou serão descartados. + Confirmar diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e0524fa941..ee2edc01ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -196,4 +196,8 @@ Allow location Allow location sharing If you don’t allow Ground to access this device’s location, you won’t be able to continue collecting data for this site. + + Stop collecting data? + If you exit, data you haven’t submitted yet will be discarded. + Confirm diff --git a/app/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt b/app/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt index 7a54a39521..a65eb8a1c0 100644 --- a/app/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt +++ b/app/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt @@ -16,6 +16,9 @@ package com.google.android.ground.ui.datacollection +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import com.google.android.ground.BaseHiltTest import com.google.android.ground.FakeData import com.google.android.ground.FakeData.LOCATION_OF_INTEREST @@ -188,7 +191,7 @@ class DataCollectionFragmentTest : BaseHiltTest() { runner() .inputText(TASK_1_RESPONSE) .clickNextButton() - .pressBackButton(true) + .pressBackButton() .validateTextIsDisplayed(TASK_1_NAME) .validateTextIsNotDisplayed(TASK_2_NAME) @@ -287,16 +290,28 @@ class DataCollectionFragmentTest : BaseHiltTest() { } @Test - fun `Clicking back button on first task clears the draft and returns false`() = + fun `Clicking back button on first task displays a confirmation dialog and clears the draft`() = runWithTestDispatcher { setupFragment() - runner() - .inputText(TASK_1_RESPONSE) - .clickNextButton() - .pressBackButton(true) - .pressBackButton(false) + // Save the draft and move back to first task. + runner().inputText(TASK_1_RESPONSE).clickNextButton().pressBackButton() + + // Click back on first draft + runner().pressBackButton() + + // Assert that confirmation dialog is shown + composeTestRule + .onNodeWithText(fragment.getString(R.string.data_collection_cancellation_title)) + .assertIsDisplayed() + + // Click confirm button + composeTestRule + .onNodeWithText(fragment.getString(R.string.data_collection_cancellation_confirm_button)) + .performClick() + advanceUntilIdle() + // Assert that draft is cleared on confirmation assertNoDraftSaved() } diff --git a/app/src/test/java/com/google/android/ground/ui/datacollection/TaskFragmentRunner.kt b/app/src/test/java/com/google/android/ground/ui/datacollection/TaskFragmentRunner.kt index c2555e97fa..484b128b51 100644 --- a/app/src/test/java/com/google/android/ground/ui/datacollection/TaskFragmentRunner.kt +++ b/app/src/test/java/com/google/android/ground/ui/datacollection/TaskFragmentRunner.kt @@ -185,8 +185,8 @@ class TaskFragmentRunner( return this } - internal fun pressBackButton(result: Boolean): TaskFragmentRunner { - waitUntilDone { assertThat(fragment?.onBack()).isEqualTo(result) } + internal fun pressBackButton(): TaskFragmentRunner { + waitUntilDone { assertThat(fragment?.onBack()).isTrue() } return this }