From 3c98b42af61810ab1fb4bef01639e523026229ef Mon Sep 17 00:00:00 2001 From: Ellen Poe Date: Wed, 10 Dec 2025 13:28:38 -0800 Subject: [PATCH] fix: offline download notification changes to "Finished" when done --- .../maps/cardinal/data/room/AppDatabase.kt | 11 ++- .../maps/cardinal/data/room/DownloadedTile.kt | 1 + .../cardinal/data/room/DownloadedTileDao.kt | 6 ++ .../tileserver/DownloadProgressReporter.kt | 6 +- .../tileserver/ServiceProgressReporter.kt | 2 +- .../TileDownloadForegroundService.kt | 61 +++++++++------ .../tileserver/TileDownloadManager.kt | 78 ++++++++++++------- .../tileserver/TileDownloadManagerTest.kt | 20 ++++- 8 files changed, 127 insertions(+), 58 deletions(-) diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/AppDatabase.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/AppDatabase.kt index 0cceb10..dd1c8fe 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/AppDatabase.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/AppDatabase.kt @@ -29,7 +29,7 @@ import earth.maps.cardinal.data.DownloadStatusConverter @Database( entities = [OfflineArea::class, RoutingProfile::class, DownloadedTile::class, SavedList::class, SavedPlace::class, ListItem::class, RecentSearch::class], - version = 12, + version = 13, exportSchema = false ) @TypeConverters(TileTypeConverter::class, DownloadStatusConverter::class, ItemTypeConverter::class) @@ -224,6 +224,14 @@ abstract class AppDatabase : RoomDatabase() { } } + private val MIGRATION_12_13 = object : Migration(12, 13) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE downloaded_tiles ADD COLUMN processed INTEGER" + ) + } + } + fun getDatabase(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( @@ -239,6 +247,7 @@ abstract class AppDatabase : RoomDatabase() { MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, + MIGRATION_12_13, ).build() INSTANCE = instance instance diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/DownloadedTile.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/DownloadedTile.kt index 83b706e..465f711 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/DownloadedTile.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/DownloadedTile.kt @@ -38,6 +38,7 @@ data class DownloadedTile( val zoom: Int? = null, val tileX: Int? = null, val tileY: Int? = null, + val processed: Boolean? = null, // For Valhalla tiles (hierarchyLevel/tileIndex system) val hierarchyLevel: Int? = null, diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/DownloadedTileDao.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/DownloadedTileDao.kt index fb1538f..e737a9a 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/DownloadedTileDao.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/room/DownloadedTileDao.kt @@ -51,6 +51,12 @@ interface DownloadedTileDao { @Query("SELECT COUNT(*) FROM downloaded_tiles WHERE areaId = :areaId AND tileType = :tileType") suspend fun getDownloadedTileCountForAreaAndType(areaId: String, tileType: TileType): Int + @Query("SELECT COUNT(*) FROM downloaded_tiles WHERE areaId = :areaId AND COALESCE(processed, 0) <> 0") + suspend fun getProcessedTileCountForArea(areaId: String): Int + + @Query("SELECT COUNT(*) FROM downloaded_tiles WHERE areaId = :areaId AND COALESCE(processed, 0) == 0 AND zoom = 14") + suspend fun getUnprocessedTileCountForArea(areaId: String): Int + @Query("SELECT COUNT(*) FROM downloaded_tiles WHERE areaId = :areaId") suspend fun getDownloadedTileCountForArea(areaId: String): Int diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/DownloadProgressReporter.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/DownloadProgressReporter.kt index dce05d4..034285a 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/DownloadProgressReporter.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/DownloadProgressReporter.kt @@ -35,7 +35,7 @@ interface DownloadProgressReporter { fun updateProgress( areaId: String, areaName: String, - currentStage: DownloadStage?, + currentStage: DownloadStage, stageProgress: Int, stageTotal: Int, isCompleted: Boolean, @@ -49,5 +49,7 @@ interface DownloadProgressReporter { enum class DownloadStage { BASEMAP, VALHALLA, - PROCESSING + PROCESSING, + DONE, + ERROR, } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/ServiceProgressReporter.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/ServiceProgressReporter.kt index 96ed5f7..3305ae1 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/ServiceProgressReporter.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/ServiceProgressReporter.kt @@ -28,7 +28,7 @@ class ServiceProgressReporter( override fun updateProgress( areaId: String, areaName: String, - currentStage: DownloadStage?, + currentStage: DownloadStage, stageProgress: Int, stageTotal: Int, isCompleted: Boolean, diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/TileDownloadForegroundService.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/TileDownloadForegroundService.kt index 8f6d060..7dff911 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/TileDownloadForegroundService.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/TileDownloadForegroundService.kt @@ -75,8 +75,7 @@ class TileDownloadForegroundService : Service() { private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var downloadJob: Job? = null - private val _downloadProgress = MutableStateFlow(DownloadProgress()) - val downloadProgress: StateFlow = _downloadProgress.asStateFlow() + private val _downloadProgress: MutableStateFlow = MutableStateFlow(null) private val _isDownloading = MutableStateFlow(false) val isDownloading: StateFlow = _isDownloading.asStateFlow() @@ -110,7 +109,7 @@ class TileDownloadForegroundService : Service() { data class DownloadProgress( val areaId: String = "", val areaName: String = "", - val currentStage: DownloadStage? = null, + val currentStage: DownloadStage, val stageProgress: Int = 0, val stageTotal: Int = 0, val isCompleted: Boolean = false, @@ -124,17 +123,19 @@ class TileDownloadForegroundService : Service() { DownloadStage.BASEMAP -> "Downloaded $stageProgress of $stageTotal map tiles" DownloadStage.VALHALLA -> "Downloaded $stageProgress of $stageTotal routing tiles" DownloadStage.PROCESSING -> "Processed $stageProgress of $stageTotal map tiles" - null -> "Unknown download state" + DownloadStage.DONE -> "Finished download" + DownloadStage.ERROR -> "Download finished with error" } } private fun calculateUnifiedProgress(): Float { - val stageProgressFraction = stageProgress.toFloat() / stageTotal.toFloat() + val stageProgressFraction = stageTotal?.let { stageProgress.toFloat() / stageTotal.toFloat() } ?: 0.0f return when (currentStage) { DownloadStage.BASEMAP -> stageProgressFraction * 0.6f DownloadStage.VALHALLA -> 0.6f + stageProgressFraction * 0.2f DownloadStage.PROCESSING -> 0.8f + stageProgressFraction * 0.2f - null -> 0f + DownloadStage.DONE -> 1f + DownloadStage.ERROR -> 0f } } } @@ -264,20 +265,21 @@ class TileDownloadForegroundService : Service() { builder.addAction( R.drawable.cloud_download_24dp, "Resume", resumePendingIntent ) - } else { + } else if (progress.currentStage != DownloadStage.DONE) { builder.addAction( android.R.drawable.ic_media_pause, "Pause", pausePendingIntent ) } - builder.addAction( - android.R.drawable.ic_menu_close_clear_cancel, "Cancel", cancelPendingIntent - ) - + if (progress.currentStage != DownloadStage.DONE) { + builder.addAction( + android.R.drawable.ic_menu_close_clear_cancel, "Cancel", cancelPendingIntent + ) + } - if (progress.unifiedProgress > 0) { + if (progress.unifiedProgress > 0 && progress.currentStage != DownloadStage.DONE) { builder.setProgress(1000, (progress.unifiedProgress * 1000f).toInt(), false) - } else { + } else if (progress.currentStage != DownloadStage.DONE) { // Initializing state builder.setProgress(0, 0, true) // Indeterminate progress } @@ -289,7 +291,7 @@ class TileDownloadForegroundService : Service() { areaId: String, areaName: String, boundingBox: BoundingBox, minZoom: Int, maxZoom: Int ) { // Check if we're already downloading the same area - if (_isDownloading.value && _downloadProgress.value.areaId == areaId) { + if (_isDownloading.value && _downloadProgress.value?.areaId == areaId) { Log.w(TAG, "Download already in progress for area $areaId, ignoring duplicate request") return } @@ -306,9 +308,10 @@ class TileDownloadForegroundService : Service() { Log.d(TAG, "Starting download for area: $areaName (ID: $areaId)") _isDownloading.value = true - _downloadProgress.value = DownloadProgress( + val progress = DownloadProgress( areaId = areaId, areaName = areaName, currentStage = DownloadStage.BASEMAP ) + _downloadProgress.value = progress // Check notification permission for Android 13+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -332,11 +335,11 @@ class TileDownloadForegroundService : Service() { // Android 14+ requires explicit foreground service type startForeground( NOTIFICATION_ID, - createNotification(_downloadProgress.value), + createNotification(progress), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC ) } else { - startForeground(NOTIFICATION_ID, createNotification(_downloadProgress.value)) + startForeground(NOTIFICATION_ID, createNotification(progress)) } Log.d(TAG, "Foreground service started successfully") } catch (e: Exception) { @@ -390,7 +393,7 @@ class TileDownloadForegroundService : Service() { offlineAreaDao.updateOfflineArea(updatedArea) } - _downloadProgress.value = _downloadProgress.value.copy( + _downloadProgress.value = _downloadProgress.value?.copy( isCompleted = true, hasError = !success ) @@ -429,9 +432,9 @@ class TileDownloadForegroundService : Service() { val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - val notification = createNotification(_downloadProgress.value) + val notification = _downloadProgress.value?.let { createNotification(it) } notificationManager.notify(NOTIFICATION_ID, notification) - Log.d(TAG, "Notification updated successfully: ${notification.extras}") + Log.d(TAG, "Notification updated successfully: ${notification?.extras}") } catch (e: Exception) { Log.e(TAG, "Failed to update notification", e) } @@ -482,7 +485,11 @@ class TileDownloadForegroundService : Service() { fun cancelDownload() { Log.d(TAG, "Cancelling download") // Capture the value for the closure before we start changing state. - val areaId = _downloadProgress.value.areaId + val areaId = _downloadProgress.value?.areaId + if (areaId == null) { + Log.d(TAG, "Download progress was null") + return + } stopDownloadJob() _isDownloading.value = false serviceScope.launch { @@ -506,7 +513,7 @@ class TileDownloadForegroundService : Service() { fun updateProgress( areaId: String, areaName: String, - currentStage: DownloadStage?, + currentStage: DownloadStage, stageProgress: Int, stageTotal: Int, isCompleted: Boolean, @@ -559,7 +566,9 @@ class TileDownloadForegroundService : Service() { } } catch (e: Exception) { Log.e(TAG, "Error processing download queue", e) + } finally { stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() } } @@ -573,6 +582,10 @@ class TileDownloadForegroundService : Service() { // Update the current offline area to mark it as paused serviceScope.launch { val currentProgress = _downloadProgress.value + if (currentProgress == null) { + Log.d(TAG, "Progress was null while attempting to pause download") + return@launch + } if (currentProgress.areaId.isNotEmpty()) { val area = offlineAreaDao.getOfflineAreaById(currentProgress.areaId) if (area != null) { @@ -598,6 +611,10 @@ class TileDownloadForegroundService : Service() { // Update the current offline area to mark it as not paused serviceScope.launch { val currentProgress = _downloadProgress.value + if (currentProgress == null) { + Log.d(TAG, "Progress was null while attempting to resume download") + return@launch + } if (currentProgress.areaId.isNotEmpty()) { val area = offlineAreaDao.getOfflineAreaById(currentProgress.areaId) if (area != null) { diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/TileDownloadManager.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/TileDownloadManager.kt index fc8db6e..0e725f5 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/TileDownloadManager.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/tileserver/TileDownloadManager.kt @@ -90,7 +90,7 @@ class TileDownloadManager( if (existingArea == null) return false // Calculate expected total basemap tiles - val totalExpectedBasemapTiles = calculateTotalTiles( + val (totalExpectedBasemapTiles, _) = calculateTotalTiles( boundingBox = existingArea.boundingBox(), existingArea.minZoom, minOf(existingArea.maxZoom, MAX_BASEMAP_ZOOM) @@ -242,14 +242,15 @@ class TileDownloadManager( Log.d(TAG, "Resuming download from phase: $resumePhase") db = initializeDatabase() - val (totalBasemapTiles, totalValhallaTiles) = + val (totalBasemapTiles, totalTilesToProcess, totalValhallaTiles) = calculateTotalDownloadCounts(boundingBox, minZoom, maxZoom) - val (downloadedBasemapTiles, downloadedValhallaTiles) = + val (downloadedBasemapTiles, downloadedValhallaTiles, processedTiles) = getCurrentProgressCounts(areaId) initializeProgressReporting( areaId, name, totalBasemapTiles, totalValhallaTiles, - downloadedBasemapTiles, downloadedValhallaTiles + downloadedBasemapTiles, downloadedValhallaTiles, + processedTiles, totalTilesToProcess ) tileProcessor?.beginTileProcessing() @@ -289,7 +290,7 @@ class TileDownloadManager( /** * Initialize or open the MBTiles database */ - private suspend fun initializeDatabase(): SQLiteDatabase { + private fun initializeDatabase(): SQLiteDatabase { val outputFile = File(context.filesDir, OFFLINE_DATABASE_NAME) val dbExists = outputFile.exists() Log.d(TAG, "Using database file: ${outputFile.absolutePath}, exists: $dbExists") @@ -308,51 +309,55 @@ class TileDownloadManager( /** * Calculate total counts for basemap and Valhalla tiles */ - private suspend fun calculateTotalDownloadCounts( + private fun calculateTotalDownloadCounts( boundingBox: BoundingBox, minZoom: Int, maxZoom: Int - ): Pair { - val totalBasemapTiles = calculateTotalTiles( + ): Triple { + val (totalBasemapTiles, totalTilesToProcess) = calculateTotalTiles( boundingBox, minZoom, min(maxZoom, MAX_BASEMAP_ZOOM) ) val totalValhallaTiles = ValhallaTileUtils.tilesForBoundingBox(boundingBox).size Log.d(TAG, "Total basemap tiles to download: $totalBasemapTiles") + Log.d(TAG, "Total basemap tiles to process: $totalTilesToProcess") Log.d(TAG, "Total valhalla tiles to download: $totalValhallaTiles") - return Pair(totalBasemapTiles, totalValhallaTiles) + return Triple(totalBasemapTiles, totalTilesToProcess, totalValhallaTiles) } /** * Get current download progress counts */ - private suspend fun getCurrentProgressCounts(areaId: String): Pair { + private suspend fun getCurrentProgressCounts(areaId: String): Triple { val downloadedBasemapTiles = downloadedTileDao.getDownloadedTileCountForAreaAndType(areaId, TileType.BASEMAP) val downloadedValhallaTiles = downloadedTileDao.getDownloadedTileCountForAreaAndType(areaId, TileType.VALHALLA) + val processedTiles = downloadedTileDao.getProcessedTileCountForArea(areaId) Log.d( TAG, "Already downloaded: $downloadedBasemapTiles basemap tiles, $downloadedValhallaTiles valhalla tiles" ) - return Pair(downloadedBasemapTiles, downloadedValhallaTiles) + return Triple(downloadedBasemapTiles, downloadedValhallaTiles, processedTiles) } /** * Initialize progress reporting with current state */ - private suspend fun initializeProgressReporting( + private fun initializeProgressReporting( areaId: String, name: String, totalBasemapTiles: Int, totalValhallaTiles: Int, - downloadedBasemapTiles: Int, downloadedValhallaTiles: Int + downloadedBasemapTiles: Int, downloadedValhallaTiles: Int, + processedTiles: Int, totalTilesToProcess: Int ) { val currentStage = determineCurrentStage( totalBasemapTiles, totalValhallaTiles, - downloadedBasemapTiles, downloadedValhallaTiles + downloadedBasemapTiles, downloadedValhallaTiles, + processedTiles, totalTilesToProcess ) val stageProgress = - getStageProgress(currentStage, downloadedBasemapTiles, downloadedValhallaTiles) - val stageTotal = getStageTotal(currentStage, totalBasemapTiles, totalValhallaTiles) + getStageProgress(currentStage, downloadedBasemapTiles, downloadedValhallaTiles, processedTiles) + val stageTotal = getStageTotal(currentStage, totalBasemapTiles, totalValhallaTiles, totalTilesToProcess) progressReporter?.updateProgress( areaId = areaId, @@ -380,14 +385,17 @@ class TileDownloadManager( */ private fun determineCurrentStage( totalBasemapTiles: Int, totalValhallaTiles: Int, - downloadedBasemapTiles: Int, downloadedValhallaTiles: Int + downloadedBasemapTiles: Int, downloadedValhallaTiles: Int, + processedTiles: Int, totalTilesToProcess: Int ): DownloadStage { return if (downloadedBasemapTiles != totalBasemapTiles) { DownloadStage.BASEMAP } else if (downloadedValhallaTiles != totalValhallaTiles) { DownloadStage.VALHALLA - } else { + } else if (processedTiles != totalTilesToProcess) { DownloadStage.PROCESSING + } else { + DownloadStage.DONE } } @@ -396,12 +404,14 @@ class TileDownloadManager( */ private fun getStageProgress( currentStage: DownloadStage, - downloadedBasemapTiles: Int, downloadedValhallaTiles: Int + downloadedBasemapTiles: Int, downloadedValhallaTiles: Int, processedTiles: Int ): Int { return when (currentStage) { DownloadStage.BASEMAP -> downloadedBasemapTiles DownloadStage.VALHALLA -> downloadedValhallaTiles - DownloadStage.PROCESSING -> 0 + DownloadStage.PROCESSING -> processedTiles + DownloadStage.DONE -> 0 + DownloadStage.ERROR -> 0 } } @@ -410,12 +420,14 @@ class TileDownloadManager( */ private fun getStageTotal( currentStage: DownloadStage, - totalBasemapTiles: Int, totalValhallaTiles: Int + totalBasemapTiles: Int, totalValhallaTiles: Int, totalTilesToProcess: Int, ): Int { return when (currentStage) { DownloadStage.BASEMAP -> totalBasemapTiles DownloadStage.VALHALLA -> totalValhallaTiles - DownloadStage.PROCESSING -> 1 + DownloadStage.PROCESSING -> totalTilesToProcess + DownloadStage.DONE -> 0 + DownloadStage.ERROR -> 0 } } @@ -516,7 +528,7 @@ class TileDownloadManager( /** * Calculate completion stats and log results */ - private suspend fun calculateAndLogCompletionStats( + private fun calculateAndLogCompletionStats( boundingBox: BoundingBox, minZoom: Int, maxZoom: Int, basemapResult: Pair, valhallaResult: Pair, downloadedBasemapTiles: Int, downloadedValhallaTiles: Int @@ -554,6 +566,8 @@ class TileDownloadManager( area.copy(status = DownloadStatus.PROCESSING_GEOCODER, fileSize = fileSize) offlineAreaDao.updateOfflineArea(processingArea) } + val toProcess = downloadedTileDao.getUnprocessedTileCountForArea(areaId) + Log.d(TAG, "Updating progress at beginning of processing phase, $toProcess tiles to process") // Update service progress - downloads completed, now processing progressReporter?.updateProgress( @@ -561,7 +575,7 @@ class TileDownloadManager( areaName = name, currentStage = DownloadStage.PROCESSING, stageProgress = 0, - stageTotal = 1, + stageTotal = toProcess, isCompleted = false, hasError = false ) @@ -582,7 +596,7 @@ class TileDownloadManager( progressReporter?.updateProgress( areaId = areaId, areaName = name, - currentStage = null, + currentStage = DownloadStage.DONE, stageProgress = 0, stageTotal = 0, isCompleted = true, @@ -598,7 +612,7 @@ class TileDownloadManager( progressReporter?.updateProgress( areaId = areaId, areaName = name, - currentStage = null, + currentStage = DownloadStage.ERROR, stageProgress = 0, stageTotal = 0, isCompleted = true, @@ -636,13 +650,13 @@ class TileDownloadManager( try { // Calculate total tiles - val totalTiles = calculateTotalTiles(boundingBox, minZoom, maxZoom) + val (totalTiles, totalTilesToProcess) = calculateTotalTiles(boundingBox, minZoom, maxZoom) // Materialize all tile coordinates first to simplify processing val tileCoordinates = generateTileCoordinates(boundingBox, minZoom, maxZoom) - Log.d(TAG, "Total basemap tiles to process: ${tileCoordinates.size}") + Log.d(TAG, "Total basemap tiles to process: $totalTilesToProcess") // Validate consistency between expected tiles and existing tiles in database val existingTileCount = @@ -969,17 +983,21 @@ class TileDownloadManager( */ fun calculateTotalTiles( boundingBox: BoundingBox, minZoom: Int, maxZoom: Int - ): Int { + ): Pair { var totalTiles = 0 + var z14Tiles = 0 for (zoom in minZoom..maxZoom) { val (minX, maxX, minY, maxY) = calculateTileRange(boundingBox, zoom) val zoomTileCount = (maxX - minX + 1) * (maxY - minY + 1) totalTiles += zoomTileCount + if (zoom == 14) { + z14Tiles += zoomTileCount + } Log.d( TAG, "Zoom $zoom: tiles from ($minX,$minY) to ($maxX,$maxY), count: $zoomTileCount" ) } - return totalTiles + return Pair(totalTiles, z14Tiles) } /** diff --git a/cardinal-android/app/src/test/java/earth/maps/cardinal/tileserver/TileDownloadManagerTest.kt b/cardinal-android/app/src/test/java/earth/maps/cardinal/tileserver/TileDownloadManagerTest.kt index b296574..a4f6edf 100644 --- a/cardinal-android/app/src/test/java/earth/maps/cardinal/tileserver/TileDownloadManagerTest.kt +++ b/cardinal-android/app/src/test/java/earth/maps/cardinal/tileserver/TileDownloadManagerTest.kt @@ -375,10 +375,26 @@ class TileDownloadManagerTest { val maxZoom = 12 // Act - val result = tileDownloadManager.calculateTotalTiles(boundingBox, minZoom, maxZoom) + val (totalTiles, totalTilesToProcess) = tileDownloadManager.calculateTotalTiles(boundingBox, minZoom, maxZoom) // Assert - assertEquals(284, result) + assertEquals(284, totalTiles) + assertEquals(0, totalTilesToProcess) + } + + @Test + fun `calculateTotalTiles should return correct tile count, including tiles to process, for zoom range up to 14`() { + // Arrange + val boundingBox = BoundingBox(40.0, 39.0, -74.0, -75.0) + val minZoom = 10 + val maxZoom = 14 + + // Act + val (totalTiles, totalTilesToProcess) = tileDownloadManager.calculateTotalTiles(boundingBox, minZoom, maxZoom) + + // Assert + assertEquals(3824, totalTiles) + assertEquals(2820, totalTilesToProcess) } @Test -- GitLab