From 5ace8b2554818008c6a0bd628f61d584f2bb2fc2 Mon Sep 17 00:00:00 2001 From: gunishjain Date: Thu, 14 Nov 2024 12:10:44 +0530 Subject: [PATCH] Implement Getting Download Progress --- app/src/main/AndroidManifest.xml | 1 + .../com/gunishjain/sample/MainActivity.kt | 37 ++++++++- .../internal/download/DownloadDispatcher.kt | 3 +- .../grabbit/internal/download/DownloadTask.kt | 77 ++++++++++++++++--- .../internal/network/DefaultHttpClient.kt | 62 ++++++++------- 5 files changed, 137 insertions(+), 43 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1445a31..026c5a2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + // Handle error (optional) } ) + } else { + // Handle invalid directory (optional) + } } // Pause download @@ -99,6 +116,18 @@ fun DownloadUI() { Spacer(modifier = Modifier.height(32.dp)) + status = if(downloadComplete) { + "Download Completed" + } else if(isDownloading) { + "Downloading" + } else { + "Idle" + } + + Text("Status : $status" ) + + Spacer(modifier = Modifier.height(32.dp)) + // Buttons for control if (isDownloading) { if (isPaused) { @@ -118,8 +147,8 @@ fun DownloadUI() { Button(onClick = { startDownload( url = "https://www.learningcontainer.com/download/sample-50-mb-pdf-file/?wpdmdl=3675&refresh=6721f942bd70b1730279746", - dirPath = "/downloads", - fileName = "gunish.pdf" + dirPath = downloadsDirectory, + fileName = "himnashunew.pdf" ) }) { Text("Download") diff --git a/grabbit/src/main/java/com/gunishjain/grabbit/internal/download/DownloadDispatcher.kt b/grabbit/src/main/java/com/gunishjain/grabbit/internal/download/DownloadDispatcher.kt index 9b6ae07..840c823 100644 --- a/grabbit/src/main/java/com/gunishjain/grabbit/internal/download/DownloadDispatcher.kt +++ b/grabbit/src/main/java/com/gunishjain/grabbit/internal/download/DownloadDispatcher.kt @@ -6,11 +6,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap class DownloadDispatcher(private val httpClient: HttpClient) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - private val downloadTasks = mutableMapOf() + private val downloadTasks = ConcurrentHashMap() private fun executeOnMain(block: () -> Unit){ scope.launch { diff --git a/grabbit/src/main/java/com/gunishjain/grabbit/internal/download/DownloadTask.kt b/grabbit/src/main/java/com/gunishjain/grabbit/internal/download/DownloadTask.kt index b6d4bb8..5c18b84 100644 --- a/grabbit/src/main/java/com/gunishjain/grabbit/internal/download/DownloadTask.kt +++ b/grabbit/src/main/java/com/gunishjain/grabbit/internal/download/DownloadTask.kt @@ -1,17 +1,26 @@ package com.gunishjain.grabbit.internal.download +import android.util.Log import com.gunishjain.grabbit.internal.network.HttpClient import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import okhttp3.internal.notify +import okhttp3.internal.wait import java.io.File +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.resume class DownloadTask(private val request: DownloadRequest,private val httpClient: HttpClient) { - private var isPaused = false + private var isPaused = AtomicBoolean(false) + private var isCompleted = false private var file = File(request.dirPath, request.fileName) - private var downloadedBytes = 0L + private var downloadedBytes = AtomicLong(0L) // Use AtomicLong for thread safety + private val pauseLock = Object() suspend fun run( onStart: () -> Unit = {}, @@ -28,22 +37,32 @@ class DownloadTask(private val request: DownloadRequest,private val httpClient: // Get initial file size if resuming if (file.exists()) { - downloadedBytes = file.length() + downloadedBytes.set(file.length()) } if (request.totalBytes <= 0) { request.totalBytes = httpClient.getFileSize(request.url) + Log.d("Download Task",request.totalBytes.toString()) } - while (!isPaused) { + while (!isCompleted) { + if (isPaused.get()) { + Log.d("DownloadTask", "Download paused.") + onPause() + waitForResume() + Log.d("DownloadTask", "Download resumed.") + continue // Important: restart the loop after resume + } + try { + Log.d("DownloadTask", "Connecting to download from byte: ${downloadedBytes.get()}") httpClient.connect( url = request.url, file = file, - startByte = downloadedBytes, + startByte = downloadedBytes.get(), timeout = request.connectTimeout ) { currentBytes, totalBytes -> - downloadedBytes = currentBytes + downloadedBytes.set(currentBytes) // Calculate and report progress val progress = if (totalBytes > 0) { @@ -52,24 +71,33 @@ class DownloadTask(private val request: DownloadRequest,private val httpClient: -1 } onProgress(progress) + + // Check pause status during download + if (isPaused.get()) { + throw PauseException() + } } - // If we reach here, download is complete + isCompleted = true + onCompleted() + Log.d("DownloadTask", "Download completed.") break + } catch (e: PauseException) { + // Handle pause specifically + continue } catch (e: CancellationException) { throw e } catch (e: Exception) { - if (isPaused) { + if (isPaused.get()) { onPause() + waitForResume() continue } throw e } } - onCompleted() - } catch (e: CancellationException){ throw e } catch (e: Exception) { @@ -81,11 +109,36 @@ class DownloadTask(private val request: DownloadRequest,private val httpClient: } fun pauseDownload() { - isPaused = true + Log.d("DownloadTask", "pauseDownload() called.") + isPaused.set(true) } fun resumeDownload() { - isPaused = false + Log.d("DownloadTask", "resumeDownload() called.") + isPaused.set(false) + synchronized(pauseLock) { + pauseLock.notify() + } } + private suspend fun waitForResume() { + suspendCancellableCoroutine { continuation -> + synchronized(pauseLock) { + while (isPaused.get()) { + try { + Log.d("DownloadTask", "Waiting for resume.") + pauseLock.wait() + } catch (e: InterruptedException) { + continuation.resume(Unit) + return@synchronized + } + } + continuation.resume(Unit) + } + } + } + + private class PauseException : Exception() + + } \ No newline at end of file diff --git a/grabbit/src/main/java/com/gunishjain/grabbit/internal/network/DefaultHttpClient.kt b/grabbit/src/main/java/com/gunishjain/grabbit/internal/network/DefaultHttpClient.kt index a34a5ed..3b8f42f 100644 --- a/grabbit/src/main/java/com/gunishjain/grabbit/internal/network/DefaultHttpClient.kt +++ b/grabbit/src/main/java/com/gunishjain/grabbit/internal/network/DefaultHttpClient.kt @@ -1,5 +1,6 @@ package com.gunishjain.grabbit.internal.network +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -38,45 +39,54 @@ class DefaultHttpClient : HttpClient { } .build() - val response = okHttpClient.newCall(request).execute() + okHttpClient.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - throw IOException("Unexpected response code: ${response.code}") - } + if (!response.isSuccessful) { + throw IOException("Unexpected response code: ${response.code}") + } - // Get content length from header - val contentLength = response.header("Content-Length")?.toLong() ?: -1L - val totalBytes = if (contentLength != -1L) contentLength + startByte else -1L + // Get content length from header + val contentLength = response.header("Content-Length")?.toLong() ?: -1L + val totalBytes = if (contentLength != -1L) contentLength + startByte else -1L - // Create parent directories if they don't exist - file.parentFile?.mkdirs() + Log.d("Default HTTPCLIENT", "Total File Size: $totalBytes") - // Use response body to write to file - response.body?.let { body -> - val bufferedSink = file.sink(append = startByte > 0).buffer() - val source = body.source() - val buffer = Buffer() - var downloadedBytes = startByte + // Create parent directories if they don't exist + file.parentFile?.mkdirs() - while (true) { - val read = source.read(buffer, 8192L) // Read chunks of 8KB - if (read == -1L) break + // Use response body to write to file + response.body?.let { body -> - bufferedSink.write(buffer, read) - downloadedBytes += read + Log.d("HTTPCLIENT",body.toString()) + val bufferedSink = file.sink(append = startByte > 0).buffer() + val source = body.source() + val buffer = Buffer() + var downloadedBytes = startByte - // Report progress - onProgress(downloadedBytes, totalBytes) - } + while (true) { + val read = source.read(buffer, 8192L) // Read chunks of 8KB + if (read == -1L) break + + bufferedSink.write(buffer, read) + downloadedBytes += read + + // Report progress + +// Log.d("HTTPCLIENT",downloadedBytes.toString()) + + onProgress(downloadedBytes, totalBytes) + } - bufferedSink.close() - source.close() - } ?: throw IOException("Response body is null") + bufferedSink.close() + source.close() + } ?: throw IOException("Response body is null") + } } catch (e: CancellationException) { throw e } catch (e: Exception) { + Log.e("HTTPCLEINT", "Error occurred: ${e.message}", e) throw IOException("Download failed: ${e.message}", e) }