Skip to content

Commit b05b832

Browse files
authored
Merge 4392941 into 1e0c3b1
2 parents 1e0c3b1 + 4392941 commit b05b832

File tree

5 files changed

+106
-27
lines changed

5 files changed

+106
-27
lines changed

app/src/main/java/com/google/android/ground/ui/map/gms/mog/MogTileDownloader.kt

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
package com.google.android.ground.ui.map.gms.mog
1818

1919
import java.io.File
20+
import kotlinx.coroutines.currentCoroutineContext
21+
import kotlinx.coroutines.ensureActive
22+
import kotlinx.coroutines.flow.cancellable
2023
import kotlinx.coroutines.flow.flow
2124
import timber.log.Timber
2225

@@ -33,17 +36,20 @@ class MogTileDownloader(private val client: MogClient, private val outputBasePat
3336
* Executes the provided [requests], writing resulting tiles to [outputBasePath] in sub-paths of
3437
* the form `{z}/{x}/{y}.jpg`.
3538
*/
36-
suspend fun downloadTiles(requests: List<MogTilesRequest>) = flow {
37-
client.getTiles(requests).collect { tile ->
38-
val outFile = File(outputBasePath, tile.metadata.tileCoordinates.getTilePath())
39-
outFile.parentFile?.mkdirs()
40-
val gmsTile = tile.toGmsTile()
41-
val data = gmsTile.data!!
42-
outFile.writeBytes(data)
43-
Timber.d("Saved ${data.size} bytes to ${outFile.path}")
44-
emit(data.size)
45-
}
46-
}
39+
suspend fun downloadTiles(requests: List<MogTilesRequest>) =
40+
flow {
41+
client.getTiles(requests).collect { tile ->
42+
currentCoroutineContext().ensureActive()
43+
val outFile = File(outputBasePath, tile.metadata.tileCoordinates.getTilePath())
44+
outFile.parentFile?.mkdirs()
45+
val gmsTile = tile.toGmsTile()
46+
val data = gmsTile.data!!
47+
outFile.writeBytes(data)
48+
Timber.d("Saved ${data.size} bytes to ${outFile.path}")
49+
emit(data.size)
50+
}
51+
}
52+
.cancellable()
4753
}
4854

4955
fun TileCoordinates.getTilePath() = "$zoom/$x/$y.jpg"

app/src/main/java/com/google/android/ground/ui/offlineareas/OfflineAreasViewModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ internal constructor(private val offlineAreaRepository: OfflineAreaRepository) :
4949
val showNoAreasMessage: LiveData<Boolean>
5050
val showProgressSpinner: LiveData<Boolean>
5151

52-
private val _navigateToOfflineAreaSelector = MutableSharedFlow<Unit>(replay = 1)
52+
private val _navigateToOfflineAreaSelector =
53+
MutableSharedFlow<Unit>(extraBufferCapacity = 1, replay = 0)
5354
val navigateToOfflineAreaSelector = _navigateToOfflineAreaSelector.asSharedFlow()
5455

5556
init {

app/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorFragment.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,10 @@ class OfflineAreaSelectorFragment : AbstractMapContainerFragment() {
119119
openAlertDialog.value -> {
120120
DownloadProgressDialog(
121121
progress = progress.value,
122-
// TODO: - Add Download Cancel Feature
123-
// Issue URL: https://github.com/google/ground-android/issues/1596
124-
onDismiss = { openAlertDialog.value = false },
122+
onDismiss = {
123+
openAlertDialog.value = false
124+
viewModel.stopDownloading()
125+
},
125126
)
126127
}
127128
}

app/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorViewModel.kt

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.google.android.ground.util.toMb
3636
import com.google.android.ground.util.toMbString
3737
import javax.inject.Inject
3838
import kotlinx.coroutines.CoroutineDispatcher
39+
import kotlinx.coroutines.Job
3940
import kotlinx.coroutines.flow.MutableSharedFlow
4041
import kotlinx.coroutines.flow.asSharedFlow
4142
import kotlinx.coroutines.launch
@@ -84,6 +85,8 @@ internal constructor(
8485
private val _networkUnavailableEvent = MutableSharedFlow<Unit>()
8586
val networkUnavailableEvent = _networkUnavailableEvent.asSharedFlow()
8687

88+
var downloadJob: Job? = null
89+
8790
fun onDownloadClick() {
8891
if (!networkManager.isNetworkConnected()) {
8992
viewModelScope.launch { _networkUnavailableEvent.emit(Unit) }
@@ -97,25 +100,32 @@ internal constructor(
97100

98101
isDownloadProgressVisible.value = true
99102
downloadProgress.value = 0f
100-
viewModelScope.launch(ioDispatcher) {
101-
offlineAreaRepository.downloadTiles(viewport!!).collect { (bytesDownloaded, totalBytes) ->
102-
val progressValue =
103-
if (totalBytes > 0) {
104-
(bytesDownloaded.toFloat() / totalBytes.toFloat()).coerceIn(0f, 1f)
105-
} else {
106-
0f
107-
}
108-
downloadProgress.postValue(progressValue)
103+
downloadJob =
104+
viewModelScope.launch(ioDispatcher) {
105+
offlineAreaRepository.downloadTiles(viewport!!).collect { (bytesDownloaded, totalBytes) ->
106+
val progressValue =
107+
if (totalBytes > 0) {
108+
(bytesDownloaded.toFloat() / totalBytes.toFloat()).coerceIn(0f, 1f)
109+
} else {
110+
0f
111+
}
112+
downloadProgress.postValue(progressValue)
113+
}
114+
isDownloadProgressVisible.postValue(false)
115+
_navigate.emit(UiState.OfflineAreaBackToHomeScreen)
109116
}
110-
isDownloadProgressVisible.postValue(false)
111-
_navigate.emit(UiState.OfflineAreaBackToHomeScreen)
112-
}
113117
}
114118

115119
fun onCancelClick() {
116120
viewModelScope.launch { _navigate.emit(UiState.Up) }
117121
}
118122

123+
fun stopDownloading() {
124+
downloadJob?.cancel()
125+
downloadJob = null
126+
isDownloadProgressVisible.postValue(false)
127+
}
128+
119129
override fun onMapDragged() {
120130
downloadButtonEnabled.postValue(false)
121131
bottomText.postValue(null)

app/src/test/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorFragmentTest.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
*/
1616
package com.google.android.ground.ui.offlineareas.selector
1717

18+
import androidx.activity.ComponentActivity
19+
import androidx.compose.ui.test.isDisplayed
20+
import androidx.compose.ui.test.isNotDisplayed
21+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
22+
import androidx.compose.ui.test.onNodeWithText
23+
import androidx.compose.ui.test.performClick
24+
import androidx.lifecycle.Observer
1825
import androidx.test.espresso.Espresso.onView
1926
import androidx.test.espresso.assertion.ViewAssertions.matches
2027
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
@@ -25,17 +32,32 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
2532
import com.google.android.ground.BaseHiltTest
2633
import com.google.android.ground.R
2734
import com.google.android.ground.launchFragmentInHiltContainer
35+
import com.google.android.ground.repository.OfflineAreaRepository
2836
import dagger.hilt.android.testing.HiltAndroidTest
37+
import javax.inject.Inject
38+
import junit.framework.Assert.assertFalse
39+
import kotlinx.coroutines.flow.MutableSharedFlow
40+
import kotlinx.coroutines.test.advanceUntilIdle
41+
import org.junit.Assert.assertNull
2942
import org.junit.Before
43+
import org.junit.Rule
3044
import org.junit.Test
3145
import org.junit.runner.RunWith
46+
import org.mockito.Mockito.mock
47+
import org.mockito.kotlin.any
48+
import org.mockito.kotlin.whenever
3249
import org.robolectric.RobolectricTestRunner
3350

3451
@HiltAndroidTest
3552
@RunWith(RobolectricTestRunner::class)
3653
class OfflineAreaSelectorFragmentTest : BaseHiltTest() {
3754

3855
lateinit var fragment: OfflineAreaSelectorFragment
56+
@Inject lateinit var viewModel: OfflineAreaSelectorViewModel
57+
58+
private val offlineAreaRepository: OfflineAreaRepository = mock()
59+
60+
@get:Rule override val composeTestRule = createAndroidComposeRule<ComponentActivity>()
3961

4062
@Before
4163
override fun setUp() {
@@ -64,4 +86,43 @@ class OfflineAreaSelectorFragmentTest : BaseHiltTest() {
6486
matches(hasDescendant(withText(fragment.getString(R.string.offline_area_selector_title))))
6587
)
6688
}
89+
90+
// TODO: Complete below test
91+
// Issue URL: https://github.com/google/ground-android/issues/3032
92+
@Test
93+
fun `stopDownloading cancels active download and updates UI state`() = runWithTestDispatcher {
94+
composeTestRule.setContent { DownloadProgressDialog(viewModel.downloadProgress.value!!, {}) }
95+
96+
val progressFlow = MutableSharedFlow<Pair<Int, Int>>()
97+
whenever(offlineAreaRepository.downloadTiles(any())).thenReturn(progressFlow)
98+
99+
val downloadProgressValues = mutableListOf<Float>()
100+
val observer = Observer<Float> { downloadProgressValues.add(it) }
101+
102+
viewModel.downloadProgress.observeForever(observer)
103+
104+
viewModel.onDownloadClick()
105+
advanceUntilIdle()
106+
107+
progressFlow.emit(Pair(50, 100))
108+
advanceUntilIdle()
109+
110+
composeTestRule
111+
.onNodeWithText(composeTestRule.activity.getString(R.string.cancel))
112+
.isDisplayed()
113+
114+
composeTestRule
115+
.onNodeWithText(composeTestRule.activity.getString(R.string.cancel))
116+
.performClick()
117+
progressFlow.emit(Pair(75, 100))
118+
119+
composeTestRule
120+
.onNodeWithText(composeTestRule.activity.getString(R.string.cancel))
121+
.isNotDisplayed()
122+
123+
assertFalse(viewModel.isDownloadProgressVisible.value!!)
124+
assertNull(viewModel.downloadJob)
125+
126+
viewModel.downloadProgress.removeObserver(observer)
127+
}
67128
}

0 commit comments

Comments
 (0)