diff --git a/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/DownloadProgressDialog.kt b/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/DownloadProgressDialog.kt new file mode 100644 index 0000000000..e3d7ee002c --- /dev/null +++ b/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/DownloadProgressDialog.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2024 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 com.google.android.ground.ui.offlineareas.selector + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import com.google.android.ground.R + +@Composable +fun DownloadProgressDialog(progress: Float, onDismiss: () -> Unit) { + AlertDialog( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + onDismissRequest = {}, + title = { + Text( + stringResource( + R.string.offline_map_imagery_download_progress_dialog_title, + (progress * 100).toInt(), + ), + color = MaterialTheme.colorScheme.onSurface, + fontFamily = FontFamily(Font(R.font.text_500)), + ) + }, + text = { + Column { + val animatedProgress by + animateFloatAsState(targetValue = progress, animationSpec = tween(durationMillis = 300)) + + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(8.dp)).testTag("progressBar"), + progress = { animatedProgress }, + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + ) + Spacer(Modifier.height(16.dp)) + Text( + stringResource(R.string.offline_map_imagery_download_progress_dialog_message), + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontFamily = FontFamily(Font(R.font.text_500)), + ) + } + }, + dismissButton = { + TextButton(onClick = { onDismiss() }) { + Text(text = stringResource(R.string.cancel), fontFamily = FontFamily(Font(R.font.text_500))) + } + }, + confirmButton = {}, + ) +} diff --git a/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/DownloadProgressDialogFragment.kt b/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/DownloadProgressDialogFragment.kt deleted file mode 100644 index 2ead4840a9..0000000000 --- a/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/DownloadProgressDialogFragment.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2023 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 com.google.android.ground.ui.offlineareas.selector - -import android.app.AlertDialog -import android.app.Dialog -import android.os.Bundle -import androidx.fragment.app.FragmentManager -import com.google.android.ground.R -import com.google.android.ground.databinding.DownloadProgressDialogFragBinding -import com.google.android.ground.ui.common.AbstractDialogFragment -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class DownloadProgressDialogFragment : AbstractDialogFragment() { - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val inflater = requireActivity().layoutInflater - val binding = DownloadProgressDialogFragBinding.inflate(inflater) - binding.lifecycleOwner = this - binding.viewModel = getViewModel(OfflineAreaSelectorViewModel::class.java) - val dialog = - AlertDialog.Builder(requireActivity()) - .setTitle(getString(R.string.offline_map_imagery_download_progress_dialog_title)) - .setMessage(getString(R.string.offline_map_imagery_download_progress_dialog_message)) - .setView(binding.root) - .setCancelable(false) - .create() - dialog.setCanceledOnTouchOutside(false) - return dialog - } - - fun setVisibility(childFragmentManager: FragmentManager, newVisibility: Boolean) { - if (newVisibility && !isAdded && !isVisible) { - show(childFragmentManager, this::class.simpleName) - } else if (!newVisibility && isAdded && isVisible) { - dismiss() - } - } -} diff --git a/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorFragment.kt b/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorFragment.kt index fc129c3924..8389024988 100644 --- a/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorFragment.kt @@ -20,12 +20,17 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.lifecycleScope import com.google.android.ground.databinding.OfflineAreaSelectorFragBinding import com.google.android.ground.ui.common.AbstractMapContainerFragment import com.google.android.ground.ui.common.BaseMapViewModel import com.google.android.ground.ui.home.mapcontainer.HomeScreenMapContainerViewModel import com.google.android.ground.ui.map.MapFragment +import com.google.android.ground.ui.theme.AppTheme import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -36,8 +41,6 @@ class OfflineAreaSelectorFragment : AbstractMapContainerFragment() { private lateinit var viewModel: OfflineAreaSelectorViewModel private lateinit var mapContainerViewModel: HomeScreenMapContainerViewModel - private var downloadProgressDialogFragment: DownloadProgressDialogFragment? = null - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mapContainerViewModel = getViewModel(HomeScreenMapContainerViewModel::class.java) @@ -59,10 +62,7 @@ class OfflineAreaSelectorFragment : AbstractMapContainerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.isDownloadProgressVisible.observe(viewLifecycleOwner) { - if (downloadProgressDialogFragment == null) { - downloadProgressDialogFragment = DownloadProgressDialogFragment() - } - downloadProgressDialogFragment?.setVisibility(childFragmentManager, it) + showDownloadProgressDialog(it) } } @@ -75,4 +75,28 @@ class OfflineAreaSelectorFragment : AbstractMapContainerFragment() { } override fun getMapViewModel(): BaseMapViewModel = viewModel + + private fun showDownloadProgressDialog(isVisible: Boolean) { + val dialogComposeView = + ComposeView(requireContext()).apply { + setContent { + val openAlertDialog = remember { mutableStateOf(isVisible) } + val progress = viewModel.downloadProgress.observeAsState(0f) + when { + openAlertDialog.value -> { + AppTheme { + DownloadProgressDialog( + progress = progress.value, + // TODO - Add Download Cancel Feature + // https://github.com/google/ground-android/issues/2884 + onDismiss = { openAlertDialog.value = false }, + ) + } + } + } + } + } + + (view as ViewGroup).addView(dialogComposeView) + } } diff --git a/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorViewModel.kt index 8765d285c8..75b652fcad 100644 --- a/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorViewModel.kt @@ -74,8 +74,7 @@ internal constructor( private val offlineAreaSizeLoadingSymbol = resources.getString(R.string.offline_area_size_loading_symbol) val isDownloadProgressVisible = MutableLiveData(false) - val downloadProgressMax = MutableLiveData(0) - val downloadProgress = MutableLiveData(0) + val downloadProgress = MutableLiveData(0f) val bottomText = MutableLiveData(null) val downloadButtonEnabled = MutableLiveData(false) @@ -94,13 +93,16 @@ internal constructor( } isDownloadProgressVisible.value = true - downloadProgress.value = 0 + downloadProgress.value = 0f viewModelScope.launch(ioDispatcher) { offlineAreaRepository.downloadTiles(viewport!!).collect { (bytesDownloaded, totalBytes) -> - // Set total bytes / max value on first iteration. - if (downloadProgressMax.value != totalBytes) downloadProgressMax.postValue(totalBytes) - // Add number of bytes downloaded to progress. - downloadProgress.postValue(bytesDownloaded) + val progressValue = + if (totalBytes > 0) { + (bytesDownloaded.toFloat() / totalBytes.toFloat()).coerceIn(0f, 1f) + } else { + 0f + } + downloadProgress.postValue(progressValue) } isDownloadProgressVisible.postValue(false) navigator.navigate(OfflineAreaSelectorFragmentDirections.offlineAreaBackToHomescreen()) diff --git a/ground/src/main/res/layout/download_progress_dialog_frag.xml b/ground/src/main/res/layout/download_progress_dialog_frag.xml deleted file mode 100644 index 7932a8ceab..0000000000 --- a/ground/src/main/res/layout/download_progress_dialog_frag.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ground/src/main/res/values-es/strings.xml b/ground/src/main/res/values-es/strings.xml index 452d3647f4..f1d455edd3 100644 --- a/ground/src/main/res/values-es/strings.xml +++ b/ground/src/main/res/values-es/strings.xml @@ -94,7 +94,7 @@ Mostrar u ocultar las imágenes descargadas Capas Mapa base - Progreso de descarga + Progreso de descarga - %d%% Descargando imágenes del mapa sin conexión… Múltiples regiones Permiso denegado diff --git a/ground/src/main/res/values-fr/strings.xml b/ground/src/main/res/values-fr/strings.xml index ea904158b7..6a20cff412 100644 --- a/ground/src/main/res/values-fr/strings.xml +++ b/ground/src/main/res/values-fr/strings.xml @@ -94,7 +94,7 @@ Cacher ou afficher l’imagerie cartographique téléchargée Couches Carte de base - Progrès du téléchargement + Progrès du téléchargement - %d%% Téléchargement d’imagerie cartographique hors ligne… Plusieurs régions Autorisation refusée diff --git a/ground/src/main/res/values/strings.xml b/ground/src/main/res/values/strings.xml index 3c38b35dfb..81343f21d4 100644 --- a/ground/src/main/res/values/strings.xml +++ b/ground/src/main/res/values/strings.xml @@ -95,8 +95,8 @@ Hide or show downloaded imagery Layers Base map - Download progress - Downloading offline map imagery… + Downloading - %d%% + Download times may vary depending on the size of the selected area and your internet connection. Multiple regions Permission denied Close app diff --git a/ground/src/test/java/com/google/android/ground/ui/offlineareas/selector/DownloadProgressDialogTest.kt b/ground/src/test/java/com/google/android/ground/ui/offlineareas/selector/DownloadProgressDialogTest.kt new file mode 100644 index 0000000000..c4c212495a --- /dev/null +++ b/ground/src/test/java/com/google/android/ground/ui/offlineareas/selector/DownloadProgressDialogTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2024 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 com.google.android.ground.ui.offlineareas.selector + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.google.android.ground.BaseHiltTest +import com.google.android.ground.R +import dagger.hilt.android.testing.HiltAndroidTest +import javax.inject.Inject +import kotlin.test.Test +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +class DownloadProgressDialogTest : BaseHiltTest() { + + @get:Rule override val composeTestRule = createAndroidComposeRule() + + @Inject lateinit var viewModel: OfflineAreaSelectorViewModel + + @Test + fun downloadProgressDialog_DisplaysTitleCorrectly() { + composeTestRule.setContent { DownloadProgressDialog(viewModel.downloadProgress.value!!, {}) } + + composeTestRule + .onNodeWithText( + composeTestRule.activity.getString( + R.string.offline_map_imagery_download_progress_dialog_title, + 0, + ) + ) + .assertIsDisplayed() + } + + @Test + fun downloadProgressDialog_DisplaysCorrectMessage() { + composeTestRule.setContent { DownloadProgressDialog(viewModel.downloadProgress.value!!, {}) } + + composeTestRule + .onNodeWithText( + composeTestRule.activity.getString( + R.string.offline_map_imagery_download_progress_dialog_message + ) + ) + .assertIsDisplayed() + } + + @Test + fun downloadProgressDialog_CallsOnDismissOnDismissButtonClick() { + var isDismissed = false + + composeTestRule.setContent { + DownloadProgressDialog(viewModel.downloadProgress.value!!, { isDismissed = true }) + } + + composeTestRule + .onNodeWithText(composeTestRule.activity.getString(R.string.cancel)) + .performClick() + + assertTrue(isDismissed) + } + + @Test + fun downloadProgressDialog_DisplaysCorrectTitleForProgress() { + viewModel.downloadProgress.value = 0.5f + + composeTestRule.setContent { DownloadProgressDialog(viewModel.downloadProgress.value!!, {}) } + + composeTestRule + .onNodeWithText( + composeTestRule.activity.getString( + R.string.offline_map_imagery_download_progress_dialog_title, + 50, + ) + ) + .assertIsDisplayed() + } +}