From 0f4021117f0ca33d8f765255a2213828605973fb Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Mon, 6 Nov 2023 20:40:54 +0530 Subject: [PATCH] DownloadWorker: Progress improvements * Handle split apk download progress * Also share download speed and time remaining Signed-off-by: Aayush Gupta --- .../java/com/aurora/extensions/InputStream.kt | 23 +++++++++--- .../aurora/store/data/model/DownloadInfo.kt | 7 ++++ .../aurora/store/data/work/DownloadWorker.kt | 35 +++++++++++++++++-- 3 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/data/model/DownloadInfo.kt diff --git a/app/src/main/java/com/aurora/extensions/InputStream.kt b/app/src/main/java/com/aurora/extensions/InputStream.kt index 789491151..27545b07e 100644 --- a/app/src/main/java/com/aurora/extensions/InputStream.kt +++ b/app/src/main/java/com/aurora/extensions/InputStream.kt @@ -1,24 +1,37 @@ package com.aurora.extensions +import com.aurora.store.data.model.DownloadInfo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import java.io.InputStream import java.io.OutputStream +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlin.concurrent.fixedRateTimer -fun InputStream.copyTo(out: OutputStream, streamSize: Long): Flow { +fun InputStream.copyTo(out: OutputStream, streamSize: Long): Flow { return flow { var bytesCopied: Long = 0 val buffer = ByteArray(DEFAULT_BUFFER_SIZE) var bytes = read(buffer) + + var lastTotalBytesRead = 0L + var speed: Long = 0 + @Suppress("KotlinConstantConditions") // False-positive for bytesCopied always being zero + val timer = fixedRateTimer("timer", true, 0L, 1000) { + val totalBytesRead = bytesCopied + speed = totalBytesRead - lastTotalBytesRead + lastTotalBytesRead = totalBytesRead + } + while (bytes >= 0) { out.write(buffer, 0, bytes) bytesCopied += bytes - bytes = read(buffer) // Emit stream progress in percentage - emit((bytesCopied * 100 / streamSize).toInt()) + emit(DownloadInfo((bytesCopied * 100 / streamSize).toInt(), bytesCopied, speed)) + bytes = read(buffer) } - }.flowOn(Dispatchers.IO).distinctUntilChanged() + timer.cancel() + }.flowOn(Dispatchers.IO).distinctUntilChangedBy { it.progress } } diff --git a/app/src/main/java/com/aurora/store/data/model/DownloadInfo.kt b/app/src/main/java/com/aurora/store/data/model/DownloadInfo.kt new file mode 100644 index 000000000..8c9a6a5d5 --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/model/DownloadInfo.kt @@ -0,0 +1,7 @@ +package com.aurora.store.data.model + +data class DownloadInfo( + val progress: Int = 0, + val bytesCopied: Long = 0, + val speed: Long = 0 +) diff --git a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt index ccab8d80d..d1d56d51c 100644 --- a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt @@ -22,6 +22,7 @@ import com.aurora.extensions.copyAndRemove import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.store.AuroraApplication +import com.aurora.store.data.model.DownloadInfo import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.model.Request import com.aurora.store.data.network.HttpClient @@ -39,6 +40,7 @@ import kotlinx.coroutines.flow.update import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.createDirectories import kotlin.io.path.deleteRecursively +import kotlin.properties.Delegates import com.aurora.gplayapi.data.models.File as GPlayFile class DownloadWorker(private val appContext: Context, workerParams: WorkerParameters) : @@ -47,6 +49,8 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame companion object { const val DOWNLOAD_WORKER = "DOWNLOAD_WORKER" const val DOWNLOAD_PROGRESS = "DOWNLOAD_PROGRESS" + const val DOWNLOAD_TIME = "DOWNLOAD_TIME" + const val DOWNLOAD_SPEED = "DOWNLOAD_SPEED" fun enqueueApp(app: App) { AuroraApplication.enqueuedDownloads.update { it.copyAndAdd(app) } @@ -73,6 +77,10 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame private lateinit var notificationManager: NotificationManager private var downloading = false + private var totalBytes by Delegates.notNull() + private var totalProgress = 0 + private var downloadedBytes = 0L + private val TAG = DownloadWorker::class.java.simpleName private val notificationID = 200 @@ -105,6 +113,7 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame } // Download and verify all files exists + totalBytes = files.sumOf { it.size } PathUtil.getAppDownloadDir(appContext, app.packageName, app.versionCode).createDirectories() val requestList = getDownloadRequest(files) @@ -196,10 +205,30 @@ class DownloadWorker(private val appContext: Context, workerParams: WorkerParame } } - private suspend fun onProgress(progress: Int) { + private suspend fun onProgress(downloadInfo: DownloadInfo) { if (!isStopped) { - setProgress(Data.Builder().putInt(DOWNLOAD_PROGRESS, progress).build()) - notifyStatus(DownloadStatus.DOWNLOADING, progress, notificationID) + val progress = ((downloadedBytes + downloadInfo.bytesCopied) * 100 / totalBytes).toInt() + + // Individual file progress can be negligible in contrast to total progress + // Only notify the UI if progress is greater to avoid being rate-limited by Android + if (progress > totalProgress) { + val bytesRemaining = totalBytes - (downloadedBytes + downloadInfo.bytesCopied) + val speed = if (downloadInfo.speed == 0L) 1 else downloadInfo.speed + + if (downloadInfo.progress == 100) { + downloadedBytes += downloadInfo.bytesCopied + } + + val data = Data.Builder() + .putInt(DOWNLOAD_PROGRESS, progress) + .putLong(DOWNLOAD_SPEED, downloadInfo.speed) + .putLong(DOWNLOAD_TIME, bytesRemaining / speed * 1000) + .build() + + setProgress(data) + notifyStatus(DownloadStatus.DOWNLOADING, progress, notificationID) + totalProgress = progress + } } }