diff --git a/.gitignore b/.gitignore index a8c7b78f4..d4fcf16ce 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml +/.idea/kotlinScripting.xml .DS_Store /build /captures diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt index dd3fa3035..528908bfb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt @@ -6,6 +6,7 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.os.Build +import android.text.format.DateUtils import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -49,7 +50,7 @@ class DownloadNotification(private val context: Context, startId: Int) { builder.setSilent(true) } - fun create(state: DownloadState): Notification { + fun create(state: DownloadState, timeLeft: Long): Notification { builder.setContentTitle(state.manga.title) builder.setContentText(context.getString(R.string.manga_downloading_)) builder.setProgress(1, 0, true) @@ -117,7 +118,13 @@ class DownloadNotification(private val context: Context, startId: Int) { } is DownloadState.Progress -> { builder.setProgress(state.max, state.progress, false) - builder.setContentText((state.percent * 100).format() + "%") + if (timeLeft > 0L) { + val eta = DateUtils.getRelativeTimeSpanString(timeLeft, 0L, DateUtils.SECOND_IN_MILLIS) + builder.setContentText(eta) + } else { + val percent = (state.percent * 100).format() + builder.setContentText(context.getString(R.string.percent_string_pattern, percent)) + } builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) builder.setStyle(null) builder.setOngoing(true) diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt index 13365094c..a88562989 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt @@ -11,10 +11,7 @@ import android.widget.Toast import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.transformWhile +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.plus import org.koin.android.ext.android.get @@ -32,6 +29,7 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.ext.connectivityManager import org.koitharu.kotatsu.utils.ext.throttle import org.koitharu.kotatsu.utils.progress.ProgressJob +import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator import java.util.concurrent.TimeUnit class DownloadService : BaseService() { @@ -99,13 +97,22 @@ class DownloadService : BaseService() { private fun listenJob(job: ProgressJob) { lifecycleScope.launch { val startId = job.progressValue.startId + val timeLeftEstimator = TimeLeftEstimator() val notification = DownloadNotification(this@DownloadService, startId) - notificationSwitcher.notify(startId, notification.create(job.progressValue)) + notificationSwitcher.notify(startId, notification.create(job.progressValue, -1L)) job.progressAsFlow() + .onEach { state -> + if (state is DownloadState.Progress) { + timeLeftEstimator.tick(value = state.progress, total = state.max) + } else { + timeLeftEstimator.emptyTick() + } + } .throttle { state -> if (state is DownloadState.Progress) 400L else 0L } .whileActive() .collect { state -> - notificationSwitcher.notify(startId, notification.create(state)) + val timeLeft = timeLeftEstimator.getEstimatedTimeLeft() + notificationSwitcher.notify(startId, notification.create(state, timeLeft)) } job.join() (job.progressValue as? DownloadState.Done)?.let { @@ -119,7 +126,7 @@ class DownloadService : BaseService() { if (job.isCancelled) { null } else { - notification.create(job.progressValue) + notification.create(job.progressValue, -1L) } ) stopSelf(startId) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/Progress.kt b/app/src/main/java/org/koitharu/kotatsu/utils/progress/Progress.kt index 7dff7fbf5..5723cae17 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/Progress.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/progress/Progress.kt @@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize @Parcelize data class Progress( val value: Int, - val total: Int + val total: Int, ) : Parcelable, Comparable { override fun compareTo(other: Progress): Int { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt b/app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt new file mode 100644 index 000000000..5cb7aafc5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt @@ -0,0 +1,47 @@ +package org.koitharu.kotatsu.utils.progress + +import android.os.SystemClock +import kotlin.math.roundToInt +import kotlin.math.roundToLong + +private const val MIN_ESTIMATE_TICKS = 4 +private const val NO_TIME = -1L + +class TimeLeftEstimator { + + private var times = ArrayList() + private var lastTick: Tick? = null + + fun tick(value: Int, total: Int) { + if (total < 0) { + emptyTick() + return + } + val tick = Tick(value, total, SystemClock.elapsedRealtime()) + lastTick?.let { + val ticksCount = value - it.value + times.add(((tick.time - it.time) / ticksCount.toDouble()).roundToInt()) + } + lastTick = tick + } + + fun emptyTick() { + lastTick = null + } + + fun getEstimatedTimeLeft(): Long { + val progress = lastTick ?: return NO_TIME + if (times.size < MIN_ESTIMATE_TICKS) { + return NO_TIME + } + val timePerTick = times.average() + val ticksLeft = progress.total - progress.value + return (ticksLeft * timePerTick).roundToLong() + } + + private class Tick( + val value: Int, + val total: Int, + val time: Long, + ) +} \ No newline at end of file