diff --git a/ground/src/main/java/com/google/android/ground/Config.kt b/ground/src/main/java/com/google/android/ground/Config.kt index 4570968600..1b99c1cac8 100644 --- a/ground/src/main/java/com/google/android/ground/Config.kt +++ b/ground/src/main/java/com/google/android/ground/Config.kt @@ -26,7 +26,7 @@ object Config { // Local db settings. // TODO(#128): Reset version to 1 before releasing. - const val DB_VERSION = 103 + const val DB_VERSION = 104 const val DB_NAME = "ground.db" // Firebase Cloud Firestore settings. diff --git a/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt b/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt index a8387f9f63..93993e28ed 100644 --- a/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt @@ -114,4 +114,10 @@ constructor( private fun getFirstTileSourceUrl() = surveyRepository.activeSurvey?.tileSources?.firstOrNull()?.url ?: error("Survey has no tile sources") + + suspend fun estimateSizeOnDisk(bounds: Bounds): Int { + val client = getMogClient() + val requests = client.buildTilesRequests(bounds.toGoogleMapsObject()) + return requests.sumOf { it.totalBytes } + } } diff --git a/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorViewModel.kt index 857fa2acf3..931ff15ad4 100644 --- a/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorViewModel.kt @@ -17,6 +17,7 @@ package com.google.android.ground.ui.offlinebasemap.selector import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.google.android.ground.R import com.google.android.ground.coroutines.IoDispatcher import com.google.android.ground.model.imagery.TileSource import com.google.android.ground.repository.LocationOfInterestRepository @@ -30,12 +31,17 @@ import com.google.android.ground.ui.common.BaseMapViewModel import com.google.android.ground.ui.common.Navigator import com.google.android.ground.ui.common.SharedViewModel import com.google.android.ground.ui.map.Bounds +import com.google.android.ground.ui.map.CameraPosition import com.google.android.ground.ui.map.Map import com.google.android.ground.ui.map.MapType import javax.inject.Inject +import kotlin.math.round import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.launch +private const val MIN_DOWNLOAD_ZOOM_LEVEL = 10 +private const val MAX_AREA_DOWNLOAD_SIZE_MB = 50 + /** States and behaviors of Map UI used to select areas for download and viewing offline. */ @SharedViewModel class OfflineAreaSelectorViewModel @@ -67,6 +73,9 @@ internal constructor( val isDownloadProgressVisible = MutableLiveData(false) val downloadProgressMax = MutableLiveData(0) val downloadProgress = MutableLiveData(0) + val sizeOnDisk = MutableLiveData(null) + val visibleBottomTextView = MutableLiveData(null) + val downloadButtonEnabled = MutableLiveData(false) init { tileSources = surveyRepository.activeSurvey!!.tileSources @@ -81,11 +90,11 @@ internal constructor( isDownloadProgressVisible.value = true downloadProgress.value = 0 viewModelScope.launch(ioDispatcher) { - offlineAreaRepository.downloadTiles(viewport!!).collect { (byteDownloaded, totalBytes) -> + 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(byteDownloaded) + downloadProgress.postValue(bytesDownloaded) } isDownloadProgressVisible.postValue(false) navigator.navigateUp() @@ -97,4 +106,31 @@ internal constructor( tileSources.forEach { map.addTileOverlay(it) } disposeOnClear(cameraBoundUpdates.subscribe { viewport = it }) } + + override fun onMapCameraMoved(newCameraPosition: CameraPosition) { + super.onMapCameraMoved(newCameraPosition) + val (_, zoomLevel, _, bounds) = newCameraPosition + if (bounds == null || zoomLevel == null) return + if (zoomLevel < MIN_DOWNLOAD_ZOOM_LEVEL) { + visibleBottomTextView.value = R.id.area_too_large_text_view + downloadButtonEnabled.value = false + } else { + sizeOnDisk.value = "…" + visibleBottomTextView.value = R.id.size_on_disk_text_view + viewModelScope.launch(ioDispatcher) { + val size = offlineAreaRepository.estimateSizeOnDisk(bounds) / (1024f * 1024f) + if (size > MAX_AREA_DOWNLOAD_SIZE_MB) { + visibleBottomTextView.postValue(R.id.area_too_large_text_view) + downloadButtonEnabled.postValue(false) + } else { + if (size < 1f) { + sizeOnDisk.postValue("<1") + } else { + sizeOnDisk.postValue(round(size).toInt().toString()) + } + downloadButtonEnabled.postValue(true) + } + } + } + } } diff --git a/ground/src/main/res/color/color_states_chip_button.xml b/ground/src/main/res/color/color_states_chip_button.xml new file mode 100644 index 0000000000..989428007c --- /dev/null +++ b/ground/src/main/res/color/color_states_chip_button.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/ground/src/main/res/layout/offline_base_map_selector_frag.xml b/ground/src/main/res/layout/offline_base_map_selector_frag.xml index ea6f2f61eb..dd3f1364de 100644 --- a/ground/src/main/res/layout/offline_base_map_selector_frag.xml +++ b/ground/src/main/res/layout/offline_base_map_selector_frag.xml @@ -20,6 +20,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + + @@ -70,10 +72,43 @@ app:backgroundTint="@color/colorMapAccent" app:chipIcon="@drawable/ic_area_download" app:icon="@drawable/ic_area_download" + android:enabled="@{viewModel.downloadButtonEnabled}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:useCompatPadding="true" /> + + diff --git a/ground/src/main/res/values/colors.xml b/ground/src/main/res/values/colors.xml index 43db6c4cec..655bc768cf 100644 --- a/ground/src/main/res/values/colors.xml +++ b/ground/src/main/res/values/colors.xml @@ -20,6 +20,7 @@ #ff9131 #6DDD81 #55ffffff + #FCFDF7 #006E2C diff --git a/ground/src/main/res/values/strings.xml b/ground/src/main/res/values/strings.xml index f5db64555e..6c20e99a35 100644 --- a/ground/src/main/res/values/strings.xml +++ b/ground/src/main/res/values/strings.xml @@ -163,4 +163,6 @@ Contact your system administrator to request access Close app Warning: If you sign out, all unsaved data will be lost + The selected area may take up to %s\u00A0MB of space on your device + Zoom in and select a smaller area to download to your device diff --git a/ground/src/main/res/values/styles.xml b/ground/src/main/res/values/styles.xml index 50ef4f4ab8..25cfea3a50 100644 --- a/ground/src/main/res/values/styles.xml +++ b/ground/src/main/res/values/styles.xml @@ -134,7 +134,8 @@