Resume download on network becomes available

This commit is contained in:
Koitharu
2023-05-08 15:21:42 +03:00
parent 7b8bbf9fe1
commit 42df607f52
6 changed files with 110 additions and 34 deletions

View File

@@ -10,6 +10,7 @@ data class DownloadState2(
val manga: Manga,
val isIndeterminate: Boolean,
val isPaused: Boolean = false,
val isStopped: Boolean = false,
val error: String? = null,
val totalChapters: Int = 0,
val currentChapter: Int = 0,
@@ -17,6 +18,7 @@ data class DownloadState2(
val currentPage: Int = 0,
val eta: Long = -1L,
val localManga: LocalManga? = null,
val downloadedChapters: LongArray = LongArray(0),
val timestamp: Long = System.currentTimeMillis(),
) {
@@ -30,7 +32,7 @@ data class DownloadState2(
get() = localManga != null || (error != null && !isPaused)
val isParticularProgress: Boolean
get() = localManga == null && error == null && !isPaused && max > 0 && !isIndeterminate
get() = localManga == null && error == null && !isPaused && !isStopped && max > 0 && !isIndeterminate
fun toWorkData() = Data.Builder()
.putLong(DATA_MANGA_ID, manga.id)
@@ -39,7 +41,7 @@ data class DownloadState2(
.putLong(DATA_ETA, eta)
.putLong(DATA_TIMESTAMP, timestamp)
.putString(DATA_ERROR, error)
.putInt(DATA_CHAPTERS, totalChapters)
.putLongArray(DATA_CHAPTERS, downloadedChapters)
.putBoolean(DATA_INDETERMINATE, isIndeterminate)
.putBoolean(DATA_PAUSED, isPaused)
.build()
@@ -72,6 +74,6 @@ data class DownloadState2(
fun getTimestamp(data: Data): Date = Date(data.getLong(DATA_TIMESTAMP, 0L))
fun getTotalChapters(data: Data): Int = data.getInt(DATA_CHAPTERS, 0)
fun getDownloadedChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS) ?: LongArray(0)
}
}

View File

@@ -172,7 +172,7 @@ class DownloadsViewModel @Inject constructor(
}
private suspend fun WorkInfo.toUiModel(): DownloadItemModel? {
val workData = if (progress != Data.EMPTY) progress else outputData
val workData = if (outputData == Data.EMPTY) progress else outputData
val mangaId = DownloadState2.getMangaId(workData)
if (mangaId == 0L) return null
val manga = getManga(mangaId) ?: return null
@@ -187,7 +187,7 @@ class DownloadsViewModel @Inject constructor(
progress = DownloadState2.getProgress(workData),
eta = DownloadState2.getEta(workData),
timestamp = DownloadState2.getTimestamp(workData),
totalChapters = DownloadState2.getTotalChapters(workData),
totalChapters = DownloadState2.getDownloadedChapters(workData).size,
)
}

View File

@@ -133,11 +133,29 @@ class DownloadNotificationFactory @AssistedInject constructor(
builder.setWhen(System.currentTimeMillis())
}
state.isStopped -> {
builder.setProgress(0, 0, false)
builder.setContentText(context.getString(R.string.queued))
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
builder.setStyle(null)
builder.setOngoing(true)
builder.setSmallIcon(R.drawable.ic_stat_paused)
builder.addAction(actionCancel)
}
state.isPaused -> { // paused (with error or manually)
builder.setProgress(state.max, state.progress, false)
val percent = context.getString(R.string.percent_string_pattern, (state.percent * 100).format())
builder.setContentText(percent)
builder.setContentText(state.error)
val percent = if (state.percent >= 0) {
context.getString(R.string.percent_string_pattern, (state.percent * 100).format())
} else {
null
}
if (state.error != null) {
builder.setContentText(state.error)
builder.setSubText(percent)
} else {
builder.setContentText(percent)
}
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
builder.setStyle(null)
builder.setOngoing(true)
@@ -161,7 +179,11 @@ class DownloadNotificationFactory @AssistedInject constructor(
else -> {
builder.setProgress(state.max, state.progress, false)
val percent = context.getString(R.string.percent_string_pattern, (state.percent * 100).format())
val percent = if (state.percent >= 0f) {
context.getString(R.string.percent_string_pattern, (state.percent * 100).format())
} else {
null
}
if (state.eta > 0L) {
val eta = DateUtils.getRelativeTimeSpanString(
state.eta,

View File

@@ -48,6 +48,7 @@ import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.mapToSet
@@ -82,7 +83,9 @@ class DownloadWorker @AssistedInject constructor(
private val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@Volatile
private lateinit var currentState: DownloadState2
private var lastPublishedState: DownloadState2? = null
private val currentState: DownloadState2
get() = checkNotNull(lastPublishedState)
private val pausingHandle = PausingHandle()
private val timeLeftEstimator = TimeLeftEstimator()
@@ -94,30 +97,44 @@ class DownloadWorker @AssistedInject constructor(
val mangaId = inputData.getLong(MANGA_ID, 0L)
val manga = mangaDataRepository.findMangaById(mangaId) ?: return Result.failure()
val chaptersIds = inputData.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() }
currentState = DownloadState2(manga, isIndeterminate = true)
val downloadedIds = getDoneChapters()
lastPublishedState = DownloadState2(manga, isIndeterminate = true)
return try {
downloadMangaImpl(chaptersIds)
downloadMangaImpl(chaptersIds, downloadedIds)
Result.success(currentState.toWorkData())
} catch (e: CancellationException) {
withContext(NonCancellable) {
val notification = notificationFactory.create(currentState.copy(isStopped = true))
notificationManager.notify(id.hashCode(), notification)
}
throw e
} catch (e: IOException) {
e.printStackTraceDebug()
Result.retry()
} catch (e: Exception) {
e.printStackTraceDebug()
currentState = currentState.copy(error = e.getDisplayMessage(applicationContext.resources), eta = -1L)
Result.failure(currentState.toWorkData())
Result.failure(
currentState.copy(
error = e.getDisplayMessage(applicationContext.resources),
eta = -1L,
).toWorkData(),
)
} finally {
notificationManager.cancel(id.hashCode())
}
}
override suspend fun getForegroundInfo() = ForegroundInfo(
id.hashCode(),
notificationFactory.create(null),
notificationFactory.create(lastPublishedState),
)
private suspend fun downloadMangaImpl(chaptersIds: LongArray?) {
private suspend fun downloadMangaImpl(
includedIds: LongArray?,
excludedIds: LongArray,
) {
var manga = currentState.manga
val chaptersIdsSet = chaptersIds?.toMutableSet()
val chaptersToSkip = excludedIds.toMutableSet()
withMangaLock(manga) {
ContextCompat.registerReceiver(
applicationContext,
@@ -135,26 +152,24 @@ class DownloadWorker @AssistedInject constructor(
?: error("Cannot obtain remote manga instance")
}
val repo = mangaRepositoryFactory.create(manga.source)
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = LocalMangaOutput.getOrCreate(destination, data)
val coverUrl = data.largeCoverUrl.ifNullOrEmpty { data.coverUrl }
val mangaDetails = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = LocalMangaOutput.getOrCreate(destination, mangaDetails)
val coverUrl = mangaDetails.largeCoverUrl.ifNullOrEmpty { mangaDetails.coverUrl }
if (coverUrl.isNotEmpty()) {
downloadFile(coverUrl, destination, tempFileName, repo.source).let { file ->
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
}
}
val chapters = checkNotNull(
if (chaptersIdsSet == null) {
data.chapters
} else {
data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) }
},
) { "Chapters list must not be null" }
check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
check(chaptersIdsSet.isNullOrEmpty()) {
"${chaptersIdsSet?.size} of ${chaptersIds?.size} requested chapters not found in manga"
}
val chapters = getChapters(mangaDetails, includedIds)
for ((chapterIndex, chapter) in chapters.withIndex()) {
if (chaptersToSkip.remove(chapter.id)) {
publishState(
currentState.copy(
downloadedChapters = currentState.downloadedChapters + chapter.id,
),
)
continue
}
val pages = runFailsafe(pausingHandle) {
repo.getPages(chapter)
}
@@ -190,6 +205,11 @@ class DownloadWorker @AssistedInject constructor(
localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga())
}.onFailure(Throwable::printStackTraceDebug)
}
publishState(
currentState.copy(
downloadedChapters = currentState.downloadedChapters + chapter.id,
),
)
}
publishState(currentState.copy(isIndeterminate = true, eta = -1L))
output.mergeWithExisting()
@@ -273,7 +293,7 @@ class DownloadWorker @AssistedInject constructor(
private suspend fun publishState(state: DownloadState2) {
val previousState = currentState
currentState = state
lastPublishedState = state
if (previousState.isParticularProgress && state.isParticularProgress) {
timeLeftEstimator.tick(state.progress, state.max)
} else {
@@ -291,6 +311,30 @@ class DownloadWorker @AssistedInject constructor(
setProgress(state.toWorkData())
}
private suspend fun getDoneChapters(): LongArray {
val work = WorkManagerHelper(WorkManager.getInstance(applicationContext)).getWorkInfoById(id)
?: return LongArray(0)
return DownloadState2.getDownloadedChapters(work.progress)
}
private fun getChapters(
manga: Manga,
includedIds: LongArray?,
): List<MangaChapter> {
val chapters = checkNotNull(manga.chapters?.toMutableList()) {
"Chapters list must not be null"
}
if (includedIds != null) {
val chaptersIdsSet = includedIds.toMutableSet()
chapters.retainAll { x -> chaptersIdsSet.remove(x.id) }
check(chaptersIdsSet.isEmpty()) {
"${chaptersIdsSet.size} of ${includedIds.size} requested chapters not found in manga"
}
}
check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
return chapters
}
private suspend inline fun <T> withMangaLock(manga: Manga, block: () -> T) = try {
localMangaRepository.lockManga(manga.id)
block()

View File

@@ -57,6 +57,10 @@ class WorkManagerHelper(
return workManagerImpl.getWorkInfos(query).await()
}
suspend fun getWorkInfoById(id: UUID): WorkInfo? {
return workManagerImpl.getWorkInfoById(id).await()
}
suspend fun updateWork(request: WorkRequest): WorkManager.UpdateResult {
return workManagerImpl.updateWork(request).await()
}