khushpanchal / Ketch

An Android File downloader library based on WorkManager with pause and resume support.
https://medium.com/@khush.panchal123/ketch-android-file-downloader-library-7369f7b93bd1
440 stars 33 forks source link

onSuccess event firing several times for same request #8

Closed chavu closed 2 months ago

chavu commented 3 months ago

I have created a custom Downloader below class using Ketch to download many files one after the other and I'm noticing a few problems below:

  1. onSuccess event is firing several times, thereby unitentionally executing the callback code several times
  2. It's not waiting for each request before moving to teh next even though I'm only starting the next download in the Success, Failure and Cancel events. I tried using a delay but it seems to be ignored
  3. It's taking a bit of time for the files to appear in teh file system. That's why I wanted to put a delay to give it more time on each file.
import android.content.Context
import android.os.Environment
import android.util.Log
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import com.ketch.DownloadConfig
import com.ketch.DownloadModel
import com.ketch.Ketch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rff.digitres.elklauncher.model.DownloadEntry
import org.rff.digitres.elklauncher.ui.main.MainViewModel
import java.util.concurrent.TimeUnit

class Downloader {
    val tag: String = "XXXX"
    lateinit var context: Context
    lateinit var viewModel: MainViewModel
    lateinit var lifecycleScope: CoroutineScope
    lateinit var ketch: Ketch

    private val maxRetryCount = 2
    private var currentRequestId =  mutableIntStateOf(0)
    private var downloadEntries =   mutableStateOf<List<DownloadEntry>>(listOf())

    fun initialize(context: Context,
                   viewModel: MainViewModel,
                   lifecycleScope: CoroutineScope,
     ){
        this.context = context
        this.viewModel = viewModel
        this.lifecycleScope = lifecycleScope
        val downloadConfig = DownloadConfig(
            connectTimeOutInMs = 20000L, //Default: 10000L
            readTimeOutInMs = 15000L //Default: 10000L
        )
        this.ketch = Ketch.init(context,downloadConfig)
    }

    fun startDownloading(downloadEntries: List<DownloadEntry> ) {
        if (downloadEntries.isNotEmpty()) {
            this.downloadEntries.value = downloadEntries
            onDownloaderStatusChanged(DownloaderStatus.STARTED)
        }
    }

    val onDownloaderStatusChanged: (DownloaderStatus) -> Unit = { status ->
        if (status == DownloaderStatus.STARTED) {
            viewModel.downloadResult.value = null
            lifecycleScope.launch {
                ketch.observeDownloads().collect { items: List<DownloadModel> ->
                    if (items.isNotEmpty()) {
                        val download = items.firstOrNull {it.id == currentRequestId.intValue  }
                        if (download != null) {
                            viewModel.currentDownload.value = download  // used to pass the DownloadModel object to the screen displayign the download progress
                        }
                    }
                }
            }
            // Get first item to download
            val entry: DownloadEntry? = downloadEntries.value.find { !it.isDownloaded && !it.isDownloading && it.attempts <= maxRetryCount }
            if (entry != null) {
                if (!viewModel.isFirstTimeRun()) {
                    viewModel.showDownloadWithProgress.value = true // display the screen
                }
                downloadEntry(entry)
            }
        } else {  // Stopped

            currentRequestId.intValue = 0
            ketch.stopObserving()
        }
    }

    private val onDownloadNextEntry: () -> Unit = {
        CoroutineScope(Dispatchers.IO).launch {
            delay(TimeUnit.SECONDS.toMillis(4))   // delay for 3 seconds
            withContext(Dispatchers.Main) {
                val entry: DownloadEntry? = downloadEntries.value.find { !it.isDownloaded && !it.isDownloading && it.attempts <= maxRetryCount }
                if (entry != null) {
                    downloadEntry(entry)
                } else { // Nothing to download SET download as completed
                    onDownloaderStatusChanged (DownloaderStatus.COMPLETED)
                }
            }
        }

    }

    private fun downloadEntry(entry: DownloadEntry) {
        val folder = entry.folder
        val fileName = "${entry.name}.${entry.extension}"
        val filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).toString() + folder
        viewModel.currentDownloadEntry.value = entry
        val index = downloadEntries.value.indexOf(entry)
        downloadEntries.value[index].isDownloading = true
        downloadEntries.value[index].attempts += 1

        val request = ketch.download(
            url = entry.downloadId,
            path = filePath,
            fileName = fileName,
            tag = tag,
            onFailure = { errorMessage ->
                downloadEntries.value[index].isDownloading = false
                onDownloadNextEntry()
            },
            onSuccess = {
                Log.d(tag, "FINISHED DOWNLOADING: ${entry.name}")
                downloadEntries.value[index].isDownloaded = true
                downloadEntries.value[index].isDownloading = false
                onDownloadNextEntry()
            },
            onCancel = {
                downloadEntries.value[index].isDownloading = false
                onDownloadNextEntry()
            },
        )
        currentRequestId.intValue = request.id
    }
}
data class DownloadEntry(
    var name: String,
    var type: String,
    var extension: String,
    var downloadId: String,
    var folder: String,
    var isDownloaded: Boolean = false,
    var isDownloading: Boolean = false,
    var attempts: Int = 0
)
khushpanchal commented 2 months ago

@chavu onSuccess will trigger multiple time as it is been fired by workmanager workinfos directly. There is major update in Ketch, with support of pause and resume functionality. Please upgrade to version 2.0.0 for better experience. https://github.com/khushpanchal/Ketch Please note in this version, I have completely removed the callback system, no you have to observe flow directly from db which ensures more reliability.

chavu commented 2 months ago

Thanks. I will try v2.0.0