Skip to content

Instantly share code, notes, and snippets.

@PrashamTrivedi
Last active May 28, 2024 10:50
Show Gist options
  • Save PrashamTrivedi/c7398c8f6bbf3d7cedb7919cd563302f to your computer and use it in GitHub Desktop.
Save PrashamTrivedi/c7398c8f6bbf3d7cedb7919cd563302f to your computer and use it in GitHub Desktop.
Download File with progress indicator, written in Kotlin with Co-routines
suspend fun downloadFile(url: String,
downloadFile: File,
downloadProgressFun: (bytesRead: Long, contentLength: Long, isDone: Boolean) -> Unit) {
async(CommonPool) {
val request = with(Request.Builder()) {
url(url)
}.build()
val client = with(OkHttpClient.Builder()) {
addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
val responseBody = originalResponse.body()
responseBody?.let {
originalResponse.newBuilder().body(ProgressResponseBody(it,
downloadProgressFun)).build()
}
}
}.build()
try {
val execute = client.newCall(request).execute()
val outputStream = FileOutputStream(downloadFile)
val body = execute.body()
body?.let {
with(outputStream) {
write(body.bytes())
close()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
//Coroutines and callbacks do not fit nice... So this function.
suspend fun downloadFile(url: String, downloadFile: File): Channel<ApiResponse<FileProgress>> {
val channel = Channel<ApiResponse<FileProgress>>()
async(CommonPool) {
val request = with(Request.Builder()) {
url(url)
}.build()
val client = with(OkHttpClient.Builder()) {
addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
if (originalResponse.isSuccessful) {
val responseBody = originalResponse.body()
responseBody?.let {
val progressResponseBody = ProgressResponseBody(it) { bytesRead, contentLength, isDone ->
launch(UI) {
channel.send(SuccessData(FileProgress(bytesRead = bytesRead,
contentLength = contentLength,
isDone = isDone)))
if (isDone) {
channel.close()
}
}
}
originalResponse.newBuilder().body(progressResponseBody).build()
}
} else {
launch(UI) {
channel.send(ApiError(errorCode = originalResponse.code(),
errorMessage = getCommonErrorMessage(originalResponse.code())))
channel.close()
downloadFile.deleteRecursively()
}
originalResponse
}
}
}.build()
try {
val execute = client.newCall(request).execute()
val outputStream = FileOutputStream(downloadFile)
val body = execute.body()
body?.let {
with(outputStream) {
write(body.bytes())
close()
}
}
} catch (e: Exception) {
launch(UI) {
channel.send(ApiError<FileProgress>(errorMessage = e.message ?: "Something gone wrong during download"))
channel.close()
downloadFile.deleteRecursively()
}
e.printStackTrace()
}
}
return channel
}
fun getCommonErrorMessage(code: Int): String = when (code) {
400 -> "This request is wrong"
401 -> "You are not authorised to access this file"
404 -> "The file/page is not found"
else -> "Something gone wrong"
}
private fun downloadZip(zipUrl: String) {
launch(UI) {
try {
initProductDir()
val url = URL(zipUrl)
val fileName = url.path.substringAfterLast("/")
val kotlinExternalFileWriter = KotlinExternalFileWriter()
val createFile = if (!kotlinExternalFileWriter.isFileExists(fileName, productDir)) {
kotlinExternalFileWriter.createFile(fileName, parent = productDir)
} else {
File(productDir, fileName)
}
downloadFile(url = zipUrl,
downloadFile = createFile) { bytesRead, contentLength, isDone ->
launch(UI) {
try {
val percentage = (bytesRead * 100) / contentLength
txtProgress.text = "$percentage%"
progress.progress = percentage.toInt()
if (isDone) {
extractZipHere(createFile)
afterDataComes()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun downloadZipWithoutUrl(zipUrl: String) {
launch(UI) {
try {
initProductDir()
val fileName = "$productId.zip"
// val fileName = url.path.substringAfterLast("/")
val kotlinExternalFileWriter = KotlinExternalFileWriter()
val parentDirectory = productDir.parentFile
if (!kotlinExternalFileWriter.isFileExists(fileName, parentDirectory)) {
val createFile: File = kotlinExternalFileWriter.createFile(fileName,
parent = parentDirectory)
launch(UI) {
for (data in downloadFile(url = zipUrl, downloadFile = createFile)) {
when (data) {
is SuccessData<FileProgress> -> {
data.data?.let {
val (bytesRead, contentLength, isDone) = it
val percentage = (bytesRead * 100) / contentLength
if (isDone || percentage == 100L) {
progress.isIndeterminate = true
delay(2, SECONDS)
extractZipHere(createFile) {
afterExtract()
}
}
txtProgress.text = "$percentage%"
progress.progress = percentage.toInt()
}
}
is ApiError<FileProgress> -> {
progress.showSnackBar(data.errorMessage)
}
}
}
}
} else {
val createFile = File(parentDirectory, fileName)
progress.isIndeterminate = true
if (productDir.listFiles().isNotEmpty()) {
afterExtract()
} else {
extractZipHere(createFile) {
afterExtract()
}
}
}
} catch (e: Exception) {
e.printStackTrace()
progress.showSnackBar(e.message.toString())
}
}
}
data class FileProgress(val bytesRead: Long, val contentLength: Long, val isDone: Boolean)
import okhttp3.MediaType
import okhttp3.ResponseBody
import okio.*
class ProgressResponseBody(val responseBody: ResponseBody,
val downloadProgressFun: (bytesRead: Long, contentLength: Long, isDone: Boolean) -> Unit) : ResponseBody() {
lateinit var bufferedSource: BufferedSource
override fun contentLength(): Long = responseBody.contentLength()
override fun contentType(): MediaType? = responseBody.contentType()
override fun source(): BufferedSource {
if (!::bufferedSource.isInitialized) {
bufferedSource = Okio.buffer(source(responseBody.source()))
}
return bufferedSource
}
private fun source(source: Source): Source {
return object : ForwardingSource(source) {
var totalBytesRead: Long = 0
override fun read(sink: Buffer, byteCount: Long): Long {
val read: Long = super.read(sink, byteCount)
totalBytesRead += if (read != -1L) read else 0
downloadProgressFun(totalBytesRead, responseBody.contentLength(), read == -1L)
return read
}
}
}
}
@PrashamTrivedi
Copy link
Author

Inspired from Okhttp's recipe Progress.java

@mochadwi
Copy link

mochadwi commented Feb 7, 2019

Nice and awesome @PrashamTrivedi

@marinat
Copy link

marinat commented May 22, 2019

good example

@zeroarst
Copy link

I often got file is empty problem. I found the channel should be closed after finished writing bytes to file. Moved channel.close() down after close outputSteram fixes the problem for me.

body?.let {
    with(outputStream) {
        write(body.bytes())
        close()
     }
}
channel.close() // move to here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment