-
-
Save toantran-ea/4126681b5b3d69b9aad2e763519a5fed to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@file:Suppress("BlockingMethodInNonBlockingContext") | |
package features.file.download | |
import android.content.Context | |
import androidx.hilt.Assisted | |
import androidx.hilt.work.WorkerInject | |
import androidx.lifecycle.Observer | |
import androidx.work.Constraints | |
import androidx.work.CoroutineWorker | |
import androidx.work.Data | |
import androidx.work.NetworkType.CONNECTED | |
import androidx.work.OneTimeWorkRequestBuilder | |
import androidx.work.WorkInfo | |
import androidx.work.WorkInfo.State.RUNNING | |
import androidx.work.WorkInfo.State.SUCCEEDED | |
import androidx.work.WorkManager | |
import androidx.work.WorkerParameters | |
import androidx.work.workDataOf | |
import dagger.hilt.android.qualifiers.ApplicationContext | |
import features.file.data.repo.FileApi | |
import features.file.download.DownloadState.Status.Failed | |
import features.file.download.DownloadState.Status.NotFound | |
import features.file.download.DownloadState.Status.Running | |
import features.file.download.DownloadState.Status.Succeed | |
import kotlinx.coroutines.ExperimentalCoroutinesApi | |
import kotlinx.coroutines.channels.awaitClose | |
import kotlinx.coroutines.flow.Flow | |
import kotlinx.coroutines.flow.callbackFlow | |
import kotlinx.coroutines.flow.catch | |
import kotlinx.coroutines.flow.collect | |
import kotlinx.coroutines.flow.map | |
import okhttp3.ResponseBody | |
import retrofit2.Call | |
import retrofit2.Callback | |
import retrofit2.Response | |
import java.io.File | |
import java.io.IOException | |
import java.util.UUID | |
import javax.inject.Inject | |
import javax.inject.Singleton | |
const val URL = "url" | |
const val PROGRESS = "progress" | |
const val OUTPUT_PATH = "outputPath" | |
data class DownloadState( | |
val status: Status, | |
val progress: Float = 0.0f, | |
val url: String? = null, | |
val outputPath: String? = null | |
) { | |
enum class Status { | |
Succeed, | |
Failed, | |
Running, | |
NotFound | |
} | |
} | |
@ExperimentalCoroutinesApi | |
@Singleton | |
class DownloadManager @Inject constructor( | |
@ApplicationContext private val context: Context, | |
private val api: FileApi | |
) { | |
@ExperimentalCoroutinesApi | |
class ProgressWorker @WorkerInject constructor( | |
@Assisted context: Context, | |
@Assisted parameters: WorkerParameters, | |
private val api: FileApi | |
) : CoroutineWorker(context, parameters) { | |
private val url: String = inputData.getString(URL)!! | |
private val outputPath: String = inputData.getString(OUTPUT_PATH)!! | |
override suspend fun doWork(): Result { | |
val downloadRequest: Call<ResponseBody> = api.downloadFile(url) | |
download(downloadRequest, outputPath).collect { | |
println("doWork => $it") | |
val update = workDataOf(PROGRESS to it) | |
setProgress(update) | |
} | |
return Result.success() | |
} | |
} | |
private class RequestData( | |
val uuid: UUID, | |
val url: String, | |
val outputPath: String | |
) | |
private val requests = mutableMapOf<String, RequestData>() | |
fun downloadWorker( | |
url: String, | |
outputPath: String | |
) { | |
val data = Data.Builder() | |
.putString(URL, url) | |
.putString(OUTPUT_PATH, outputPath) | |
.build() | |
val request = OneTimeWorkRequestBuilder<ProgressWorker>() | |
.setConstraints( | |
Constraints | |
.Builder() | |
.setRequiredNetworkType(CONNECTED) | |
.build() | |
) | |
.setInputData(data) | |
.build() | |
requests[url] = RequestData(request.id, url, outputPath) | |
WorkManager.getInstance(context) | |
.enqueue(request) | |
} | |
fun downloadFlow( | |
url: String, | |
outputPath: String | |
): Flow<DownloadState> { | |
val downloadRequest: Call<ResponseBody> = api.downloadFile(url) | |
return download(downloadRequest, outputPath) | |
.map { | |
if (it < 1.0) { | |
DownloadState(Running, it, url, outputPath) | |
} else { | |
DownloadState(Succeed, 1.0f, url, outputPath) | |
} | |
} | |
.catch { | |
it.printStackTrace() | |
emit(DownloadState(Failed)) | |
} | |
} | |
fun getState(url: String): Flow<DownloadState> = callbackFlow { | |
val request: RequestData? = requests[url] | |
if (request == null) { | |
offer(DownloadState(NotFound)) | |
awaitClose { } | |
} else { | |
val workInfoLiveData = WorkManager.getInstance(context) | |
// requestId is the WorkRequest id | |
.getWorkInfoByIdLiveData(request.uuid) | |
val observer: Observer<WorkInfo> = Observer { workInfo -> | |
if (workInfo.state.isFinished) { | |
when (workInfo.state) { | |
SUCCEEDED -> offer( | |
DownloadState(Succeed, progress = 1.0f, request.url, request.outputPath) | |
) | |
else -> offer(DownloadState(Failed, progress = 0.0f, request.url, request.outputPath)) | |
} | |
requests.remove(url) | |
close() | |
} else { | |
if (workInfo.state == RUNNING) { | |
val progress = workInfo.progress.getFloat(PROGRESS, 0f) | |
offer( | |
DownloadState( | |
Running, progress = progress, request.url, request.outputPath | |
) | |
) | |
} | |
} | |
} | |
workInfoLiveData.observeForever(observer) | |
awaitClose { workInfoLiveData.removeObserver(observer) } | |
} | |
} | |
} | |
@ExperimentalCoroutinesApi | |
fun download( | |
downloadRequest: Call<ResponseBody>, | |
output: String | |
) = callbackFlow<Float> { | |
downloadRequest.enqueue(object : Callback<ResponseBody> { | |
override fun onResponse( | |
call: Call<ResponseBody>, | |
response: Response<ResponseBody> | |
) { | |
if (response.isSuccessful) { | |
// make2h container directory | |
mkdirContainerIfNotExists(output) | |
val downloading = File("$output.downloading") | |
// delete the old downloading if exists | |
deleteIfExists(downloading) | |
response.body()?.let { | |
val total = it.contentLength().toFloat() | |
it.byteStream().use { inputStream -> | |
downloading.outputStream().use { outputStream -> | |
var count = 0 | |
val buffer = ByteArray(2048) | |
while (true) { | |
val read = inputStream.read(buffer) | |
if (read == -1) { | |
break | |
} | |
outputStream.write(buffer, 0, read) | |
count += read | |
val progress = count / total | |
if (progress < 1.0) { | |
offer(progress) // This will make sure we only send progress 1.0 once. | |
} | |
} | |
} | |
} | |
} | |
val outputFile = File(output) | |
// delete output if existed | |
deleteIfExists(outputFile) | |
// rename output.downloading to output then callback.onSuccess | |
if (!downloading.renameTo(outputFile)) { | |
close(IOException("can't rename file")) | |
} else { | |
offer(1.0f) | |
close() | |
} | |
} else { | |
close() | |
} | |
} | |
override fun onFailure( | |
call: Call<ResponseBody>, | |
t: Throwable | |
) { | |
close(t) | |
} | |
}) | |
awaitClose { downloadRequest.cancel() } | |
} | |
@Throws(IOException::class) | |
internal fun deleteIfExists(downloading: File) { | |
if (downloading.exists() && !downloading.delete()) { | |
throw IOException("can't delete file " + downloading.absolutePath) | |
} | |
} | |
@Throws(IOException::class) | |
internal fun mkdirContainerIfNotExists(output: String) { | |
val container = File(output).parentFile ?: return | |
if (container.isFile) { | |
// If the container is a file then delete is to create a directory with given name. | |
container.delete() | |
} | |
if (!container.exists() && !container.mkdirs()) { | |
throw IOException("can not create " + container.absolutePath) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment