Resume download on network becomes available
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user