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 Original line 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 Original line 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 Original line Diff line number Diff line
@@ -58,9 +58,6 @@ import javax.inject.Inject
@AndroidEntryPoint
@AndroidEntryPoint
class TileDownloadForegroundService : Service() {
class TileDownloadForegroundService : Service() {


    @Inject
    lateinit var tileDownloadManager: TileDownloadManager

    @Inject
    @Inject
    lateinit var offlineAreaDao: OfflineAreaDao
    lateinit var offlineAreaDao: OfflineAreaDao


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


    private lateinit var tileDownloadManager: TileDownloadManager

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


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


    enum class DownloadStage {
        BASEMAP,
        VALHALLA,
        PROCESSING
    }

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


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

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


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


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



            } catch (e: Exception) {
            } catch (e: Exception) {
                Log.e(TAG, "Error during download", e)
                Log.e(TAG, "Error during download", e)
                // Handle download failure
                // 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() {
    fun pauseDownload() {
        Log.d(TAG, "Pausing download")
        Log.d(TAG, "Pausing download")


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


package earth.maps.cardinal.tileserver
package earth.maps.cardinal.tileserver


import android.content.ComponentName
import android.content.Context
import android.content.Context
import android.content.Intent
import android.content.Intent
import android.content.ServiceConnection
import android.database.Cursor
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabase
import android.os.IBinder
import android.util.Log
import android.util.Log
import earth.maps.cardinal.R
import earth.maps.cardinal.R
import earth.maps.cardinal.data.BoundingBox
import earth.maps.cardinal.data.BoundingBox
@@ -63,7 +60,8 @@ class TileDownloadManager(
    private val context: Context,
    private val context: Context,
    private val downloadedTileDao: DownloadedTileDao,
    private val downloadedTileDao: DownloadedTileDao,
    private val offlineAreaDao: OfflineAreaDao,
    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 TAG = "TileDownloadManager"
    private val coroutineScope = CoroutineScope(Dispatchers.IO + Job())
    private val coroutineScope = CoroutineScope(Dispatchers.IO + Job())
@@ -72,23 +70,6 @@ class TileDownloadManager(
        install(ContentNegotiation)
        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 {
    companion object {
        private const val MAX_BASEMAP_ZOOM = 14
        private const val MAX_BASEMAP_ZOOM = 14
@@ -157,7 +138,6 @@ class TileDownloadManager(
                Log.d(TAG, "Starting download for area: $name (ID: $areaId)")
                Log.d(TAG, "Starting download for area: $name (ID: $areaId)")


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


                // Start the foreground service
                // Start the foreground service
                val intent = Intent(context, TileDownloadForegroundService::class.java).apply {
                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}")
        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
     * Update area status
@@ -390,7 +354,7 @@ class TileDownloadManager(
            getStageProgress(currentStage, downloadedBasemapTiles, downloadedValhallaTiles)
            getStageProgress(currentStage, downloadedBasemapTiles, downloadedValhallaTiles)
        val stageTotal = getStageTotal(currentStage, totalBasemapTiles, totalValhallaTiles)
        val stageTotal = getStageTotal(currentStage, totalBasemapTiles, totalValhallaTiles)


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


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


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


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


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


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


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


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


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


                // Update service progress
                // Update service progress
                serviceBinder?.getService()?.updateProgress(
                progressReporter?.updateProgress(
                    areaId = areaId,
                    areaId = areaId,
                    areaName = areaName,
                    areaName = areaName,
                    currentStage = TileDownloadForegroundService.DownloadStage.BASEMAP,
                    currentStage = DownloadStage.BASEMAP,
                    stageProgress = currentProgress,
                    stageProgress = currentProgress,
                    stageTotal = totalTiles,
                    stageTotal = totalTiles,
                    isCompleted = false,
                    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
     * Get the total number of tiles in the database
     */
     */
@@ -1671,10 +1624,10 @@ class TileDownloadManager(
                        tileBatch.clear()
                        tileBatch.clear()


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


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


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


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