Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8f6e47ba authored by Ellen Poe's avatar Ellen Poe
Browse files

refactor: TileDownloadManager separation of concerns

parent c6f63cac
Loading
Loading
Loading
Loading
+53 −0
Original line number Diff line number Diff line
/*
 *     Cardinal Maps
 *     Copyright (C) 2025 Cardinal Maps Authors
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package earth.maps.cardinal.tileserver

/**
 * Interface for reporting download progress to external components
 */
interface DownloadProgressReporter {
    /**
     * Update download progress
     * @param areaId The ID of the area being downloaded
     * @param areaName The name of the area being downloaded
     * @param currentStage The current stage of the download
     * @param stageProgress Current progress within the current stage
     * @param stageTotal Total expected progress for the current stage
     * @param isCompleted Whether the download is fully completed
     * @param hasError Whether an error occurred during download
     */
    fun updateProgress(
        areaId: String,
        areaName: String,
        currentStage: DownloadStage?,
        stageProgress: Int,
        stageTotal: Int,
        isCompleted: Boolean,
        hasError: Boolean
    )
}

/**
 * Represents the different stages of the download process
 */
enum class DownloadStage {
    BASEMAP,
    VALHALLA,
    PROCESSING
}
+47 −0
Original line number Diff line number Diff line
/*
 *     Cardinal Maps
 *     Copyright (C) 2025 Cardinal Maps Authors
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package earth.maps.cardinal.tileserver

/**
 * Implementation of DownloadProgressReporter that delegates to the TileDownloadForegroundService
 */
class ServiceProgressReporter(
    private val service: TileDownloadForegroundService
) : DownloadProgressReporter {

    override fun updateProgress(
        areaId: String,
        areaName: String,
        currentStage: DownloadStage?,
        stageProgress: Int,
        stageTotal: Int,
        isCompleted: Boolean,
        hasError: Boolean
    ) {
        service.updateProgress(
            areaId = areaId,
            areaName = areaName,
            currentStage = currentStage,
            stageProgress = stageProgress,
            stageTotal = stageTotal,
            isCompleted = isCompleted,
            hasError = hasError
        )
    }
}
+8 −41
Original line number Diff line number Diff line
@@ -58,9 +58,6 @@ import javax.inject.Inject
@AndroidEntryPoint
class TileDownloadForegroundService : Service() {

    @Inject
    lateinit var tileDownloadManager: TileDownloadManager

    @Inject
    lateinit var offlineAreaDao: OfflineAreaDao

@@ -73,6 +70,8 @@ class TileDownloadForegroundService : Service() {
    @Inject
    lateinit var permissionRequestManager: PermissionRequestManager

    private lateinit var tileDownloadManager: TileDownloadManager

    private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private var downloadJob: Job? = null

@@ -140,18 +139,17 @@ class TileDownloadForegroundService : Service() {
        }
    }

    enum class DownloadStage {
        BASEMAP,
        VALHALLA,
        PROCESSING
    }

    inner class TileDownloadBinder : Binder() {
        fun getService(): TileDownloadForegroundService = this@TileDownloadForegroundService
    }

    override fun onCreate() {
        super.onCreate()
        tileDownloadManager = TileDownloadManager(
            this, downloadedTileDao, offlineAreaDao, tileProcessor,
            ServiceProgressReporter(this)
        )

        Log.d(TAG, "TileDownloadForegroundService created")
        createNotificationChannel()

@@ -368,10 +366,9 @@ class TileDownloadForegroundService : Service() {

                // Start the actual download
                tileDownloadManager.downloadTilesInternal(
                    boundingBox, minZoom, maxZoom, areaId, areaName,
                    boundingBox, minZoom, maxZoom, areaId, areaName
                )


            } catch (e: Exception) {
                Log.e(TAG, "Error during download", e)
                // Handle download failure
@@ -566,36 +563,6 @@ class TileDownloadForegroundService : Service() {
        }
    }

    private suspend fun resumeIncompleteDownloads() {
        Log.d(TAG, "Checking for incomplete downloads to resume")

        try {
            val areas = offlineAreaDao.getAllOfflineAreas().first()
            val incompleteAreas = areas.filter {
                it.shouldAutomaticallyResume()
            }

            Log.d(TAG, "Found ${incompleteAreas.size} incomplete downloads")

            if (incompleteAreas.isNotEmpty()) {
                // For now, resume the first incomplete download
                // In a more advanced implementation, you could queue multiple downloads
                val areaToResume = incompleteAreas.first()
                Log.d(TAG, "Resuming download for area: ${areaToResume.name}")

                startDownloadJob(
                    areaToResume.id,
                    areaToResume.name,
                    areaToResume.boundingBox(),
                    areaToResume.minZoom,
                    areaToResume.maxZoom
                )
            }
        } catch (e: Exception) {
            Log.e(TAG, "Error resuming incomplete downloads", e)
        }
    }

    fun pauseDownload() {
        Log.d(TAG, "Pausing download")

+33 −80
Original line number Diff line number Diff line
@@ -18,13 +18,10 @@

package earth.maps.cardinal.tileserver

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.os.IBinder
import android.util.Log
import earth.maps.cardinal.R
import earth.maps.cardinal.data.BoundingBox
@@ -63,7 +60,8 @@ class TileDownloadManager(
    private val context: Context,
    private val downloadedTileDao: DownloadedTileDao,
    private val offlineAreaDao: OfflineAreaDao,
    private val tileProcessor: TileProcessor? = null
    private val tileProcessor: TileProcessor? = null,
    private val progressReporter: DownloadProgressReporter? = null
) {
    private val TAG = "TileDownloadManager"
    private val coroutineScope = CoroutineScope(Dispatchers.IO + Job())
@@ -72,23 +70,6 @@ class TileDownloadManager(
        install(ContentNegotiation)
    }

    // Service binding infrastructure
    private var serviceBinder: TileDownloadForegroundService.TileDownloadBinder? = null
    private var isBound = false

    private val serviceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            Log.d(TAG, "Connected to TileDownloadForegroundService")
            serviceBinder = service as TileDownloadForegroundService.TileDownloadBinder
            isBound = true
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            Log.d(TAG, "Disconnected from TileDownloadForegroundService")
            serviceBinder = null
            isBound = false
        }
    }

    companion object {
        private const val MAX_BASEMAP_ZOOM = 14
@@ -157,7 +138,6 @@ class TileDownloadManager(
                Log.d(TAG, "Starting download for area: $name (ID: $areaId)")

                handleExistingArea(areaId, name, boundingBox, minZoom, maxZoom)
                bindToServiceWithTimeout()

                // Start the foreground service
                val intent = Intent(context, TileDownloadForegroundService::class.java).apply {
@@ -232,22 +212,6 @@ class TileDownloadManager(
        Log.d(TAG, "Created offline area: $areaId with status ${offlineArea.status}")
    }

    /**
     * Bind to the service with timeout
     */
    private suspend fun bindToServiceWithTimeout() {
        bindToService()

        val bindTimeout = 3000L // 3 seconds
        val bindStartTime = System.currentTimeMillis()
        while (!isBound && (System.currentTimeMillis() - bindStartTime) < bindTimeout) {
            delay(100)
        }

        if (!isBound) {
            Log.w(TAG, "Service binding timeout, starting service anyway")
        }
    }

    /**
     * Update area status
@@ -390,7 +354,7 @@ class TileDownloadManager(
            getStageProgress(currentStage, downloadedBasemapTiles, downloadedValhallaTiles)
        val stageTotal = getStageTotal(currentStage, totalBasemapTiles, totalValhallaTiles)

        serviceBinder?.getService()?.updateProgress(
        progressReporter?.updateProgress(
            areaId = areaId,
            areaName = name,
            currentStage = currentStage,
@@ -417,13 +381,13 @@ class TileDownloadManager(
    private fun determineCurrentStage(
        totalBasemapTiles: Int, totalValhallaTiles: Int,
        downloadedBasemapTiles: Int, downloadedValhallaTiles: Int
    ): TileDownloadForegroundService.DownloadStage {
    ): DownloadStage {
        return if (downloadedBasemapTiles != totalBasemapTiles) {
            TileDownloadForegroundService.DownloadStage.BASEMAP
            DownloadStage.BASEMAP
        } else if (downloadedValhallaTiles != totalValhallaTiles) {
            TileDownloadForegroundService.DownloadStage.VALHALLA
            DownloadStage.VALHALLA
        } else {
            TileDownloadForegroundService.DownloadStage.PROCESSING
            DownloadStage.PROCESSING
        }
    }

@@ -431,13 +395,13 @@ class TileDownloadManager(
     * Get progress for current stage
     */
    private fun getStageProgress(
        currentStage: TileDownloadForegroundService.DownloadStage,
        currentStage: DownloadStage,
        downloadedBasemapTiles: Int, downloadedValhallaTiles: Int
    ): Int {
        return when (currentStage) {
            TileDownloadForegroundService.DownloadStage.BASEMAP -> downloadedBasemapTiles
            TileDownloadForegroundService.DownloadStage.VALHALLA -> downloadedValhallaTiles
            TileDownloadForegroundService.DownloadStage.PROCESSING -> 0
            DownloadStage.BASEMAP -> downloadedBasemapTiles
            DownloadStage.VALHALLA -> downloadedValhallaTiles
            DownloadStage.PROCESSING -> 0
        }
    }

@@ -445,13 +409,13 @@ class TileDownloadManager(
     * Get total for current stage
     */
    private fun getStageTotal(
        currentStage: TileDownloadForegroundService.DownloadStage,
        currentStage: DownloadStage,
        totalBasemapTiles: Int, totalValhallaTiles: Int
    ): Int {
        return when (currentStage) {
            TileDownloadForegroundService.DownloadStage.BASEMAP -> totalBasemapTiles
            TileDownloadForegroundService.DownloadStage.VALHALLA -> totalValhallaTiles
            TileDownloadForegroundService.DownloadStage.PROCESSING -> 1
            DownloadStage.BASEMAP -> totalBasemapTiles
            DownloadStage.VALHALLA -> totalValhallaTiles
            DownloadStage.PROCESSING -> 1
        }
    }

@@ -479,10 +443,10 @@ class TileDownloadManager(
            updateAreaStatus(areaId, DownloadStatus.DOWNLOADING_VALHALLA)

            // Update progress to show basemap completion
            serviceBinder?.getService()?.updateProgress(
            progressReporter?.updateProgress(
                areaId = areaId,
                areaName = name,
                currentStage = TileDownloadForegroundService.DownloadStage.VALHALLA,
                currentStage = DownloadStage.VALHALLA,
                stageProgress = 0,
                stageTotal = totalValhallaTiles,
                isCompleted = false,
@@ -592,10 +556,10 @@ class TileDownloadManager(
        }

        // Update service progress - downloads completed, now processing
        serviceBinder?.getService()?.updateProgress(
        progressReporter?.updateProgress(
            areaId = areaId,
            areaName = name,
            currentStage = TileDownloadForegroundService.DownloadStage.PROCESSING,
            currentStage = DownloadStage.PROCESSING,
            stageProgress = 0,
            stageTotal = 1,
            isCompleted = false,
@@ -615,7 +579,7 @@ class TileDownloadManager(
        }

        // Update service progress - processing completed
        serviceBinder?.getService()?.updateProgress(
        progressReporter?.updateProgress(
            areaId = areaId,
            areaName = name,
            currentStage = null,
@@ -631,7 +595,7 @@ class TileDownloadManager(
     */
    private suspend fun handleDownloadError(areaId: String, name: String) {
        // Update service progress - download failed
        serviceBinder?.getService()?.updateProgress(
        progressReporter?.updateProgress(
            areaId = areaId,
            areaName = name,
            currentStage = null,
@@ -826,10 +790,10 @@ class TileDownloadManager(
                "Skipping already downloaded Valhalla tile $hierarchyLevel/$tileIndex for area $areaId"
            )
            // Update service progress without incrementing counters
            serviceBinder?.getService()?.updateProgress(
            progressReporter?.updateProgress(
                areaId = areaId,
                areaName = areaName,
                currentStage = TileDownloadForegroundService.DownloadStage.VALHALLA,
                currentStage = DownloadStage.VALHALLA,
                stageProgress = downloadedCount,
                stageTotal = totalValhallaTiles,
                isCompleted = false,
@@ -844,10 +808,10 @@ class TileDownloadManager(
            storeValhallaTileReference(db, hierarchyLevel, tileIndex, filePath, areaId)

            // Update service progress
            serviceBinder?.getService()?.updateProgress(
            progressReporter?.updateProgress(
                areaId = areaId,
                areaName = areaName,
                currentStage = TileDownloadForegroundService.DownloadStage.VALHALLA,
                currentStage = DownloadStage.VALHALLA,
                stageProgress = downloadedCount + 1, // Add 1 since we return before increment
                stageTotal = totalValhallaTiles,
                isCompleted = false,
@@ -1067,10 +1031,10 @@ class TileDownloadManager(
                // Don't increment downloadedCount here - only increment for actual new downloads

                // Update service progress with current count (not incremented)
                serviceBinder?.getService()?.updateProgress(
                progressReporter?.updateProgress(
                    areaId = areaId,
                    areaName = areaName,
                    currentStage = TileDownloadForegroundService.DownloadStage.BASEMAP,
                    currentStage = DownloadStage.BASEMAP,
                    stageProgress = downloadedCount.get(),
                    stageTotal = totalTiles,
                    isCompleted = false,
@@ -1102,10 +1066,10 @@ class TileDownloadManager(
                val currentProgress = downloadedCount.incrementAndGet()

                // Update service progress
                serviceBinder?.getService()?.updateProgress(
                progressReporter?.updateProgress(
                    areaId = areaId,
                    areaName = areaName,
                    currentStage = TileDownloadForegroundService.DownloadStage.BASEMAP,
                    currentStage = DownloadStage.BASEMAP,
                    stageProgress = currentProgress,
                    stageTotal = totalTiles,
                    isCompleted = false,
@@ -1527,17 +1491,6 @@ class TileDownloadManager(
        )
    }

    /**
     * Bind to the TileDownloadForegroundService
     */
    private fun bindToService() {
        if (!isBound) {
            Log.d(TAG, "Binding to TileDownloadForegroundService")
            val intent = Intent(context, TileDownloadForegroundService::class.java)
            context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
        }
    }

    /**
     * Get the total number of tiles in the database
     */
@@ -1671,10 +1624,10 @@ class TileDownloadManager(
                        tileBatch.clear()

                        // Update progress during processing phase
                        serviceBinder?.getService()?.updateProgress(
                        progressReporter?.updateProgress(
                            areaId = areaId,
                            areaName = "",
                            currentStage = TileDownloadForegroundService.DownloadStage.PROCESSING,
                            currentStage = DownloadStage.PROCESSING,
                            stageProgress = processedCount,
                            stageTotal = totalTilesToProcess,
                            isCompleted = false,
@@ -1693,10 +1646,10 @@ class TileDownloadManager(
                    failedCount += batchFailed

                    // Final progress update
                    serviceBinder?.getService()?.updateProgress(
                    progressReporter?.updateProgress(
                        areaId = areaId,
                        areaName = "",
                        currentStage = TileDownloadForegroundService.DownloadStage.PROCESSING,
                        currentStage = DownloadStage.PROCESSING,
                        stageProgress = processedCount,
                        stageTotal = totalTilesToProcess,
                        isCompleted = false,
+2 −2
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import earth.maps.cardinal.data.BoundingBox
import earth.maps.cardinal.data.room.OfflineArea
import earth.maps.cardinal.data.room.OfflineAreaRepository
import earth.maps.cardinal.tileserver.DownloadStage
import earth.maps.cardinal.tileserver.TileDownloadForegroundService
import earth.maps.cardinal.tileserver.calculateTileRange
import kotlinx.coroutines.Job
@@ -57,8 +58,7 @@ class OfflineAreasViewModel @Inject constructor(

    // New unified progress properties
    val unifiedProgress = mutableFloatStateOf(0f) // 0.0 to 1.0
    val currentStage =
        mutableStateOf(TileDownloadForegroundService.DownloadStage.BASEMAP)
    val currentStage = mutableStateOf(DownloadStage.BASEMAP)

    // Service binding infrastructure
    private var serviceBinder: TileDownloadForegroundService.TileDownloadBinder? = null