None of provided answers works correctly, here is a working solution which combine both approach correctly.
First create new retrofit object for download, this retrofit object shouldn't contain any log interceptor because this will cause java.lang.IllegalStateException: closed
fun interface ResponseBodyListener {
fun update(responseBody: ResponseBody)
}
fun getDownloaderRetrofit(listener: ResponseBodyListener): Retrofit = Retrofit.Builder()
.baseUrl("https://example.com")// <-- this is just a placeholder, we will not use it either way in the request.
.client(initHttpDownloadListenerClient(listener))
.build()
private fun initHttpDownloadListenerClient(listener: ResponseBodyListener): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.MINUTES)
.writeTimeout(0, TimeUnit.SECONDS)
.addNetworkInterceptor { chain ->
chain.proceed(chain.request()).also { originalResponse ->
originalResponse.body?.let { listener.update(it) }
}
}
.build()
}
in addNetworkInterceptor we get the response body as soon as it becomes available so that we track the actual download progress. It has stream of data being downloaded data from server.
Here is api interface
interface DownloadFilesApi {
@GET
@Streaming
suspend fun downloadFile(@Url url: String): ResponseBody
}
Here is the request itself
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun downloadFile(url: String, directory: File, fileName: String): Flow<DownloadFileState> =
callbackFlow {
val listener = ResponseBodyListener { responseBody: ResponseBody ->
this.launch {
responseBody.downloadToFileWithProgress(directory, fileName).collect {
trySend(it)
}
}
}
getDownloaderRetrofit(listener).create(DownloadFilesApi::class.java).downloadFile(url)
awaitClose()
}
Notice that downloadFile is suspend this suspends the coroutine until it finishes download.
here callbackFlow is used to work as bridge between the normal callback and flow result.
finally downloadToFileWithProgress
is the same as written by @Robert the difference is that here it shows progress of downloading the file instead of showing progress of writing the file on desk after the actual download finishes.
for reference here it's RetrofitExtentions.kt
fun ResponseBody.downloadToFileWithProgress(directory: File, filename: String): Flow<DownloadFileState> = flow {
emit(DownloadFileState.Progress(0))
// flag to delete file if download errors or is cancelled
var deleteFile = true
val file = File(directory, filename)
try {
byteStream().use { inputStream ->
file.outputStream().use { outputStream ->
val totalBytes = contentLength()
val data = ByteArray(8_192)
var progressBytes = 0L
while (true) {
val bytes = inputStream.read(data)
if (bytes == -1) {
break
}
outputStream.write(data, 0, bytes)
progressBytes += bytes
val progress = ((progressBytes * 100) / totalBytes).toInt()
emit(DownloadFileState.Progress(percent = progress))
}
when {
progressBytes < totalBytes ->
throw Exception("missing bytes")
progressBytes > totalBytes ->
throw Exception("too many bytes")
else ->
deleteFile = false
}
}
}
emit(DownloadFileState.Finished(file))
} finally {
if (deleteFile) {
file.delete()
}
}
}
For completness here is how to get root folder in which you are going to save the video
fun videosRootFolder(context: Context): File {
return File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
APP_DIRECTORY_NAME
).run {
if (exists()) {
this
} else {
this.mkdir()
this
}
}
}