Resume download on network becomes available

pull/359/head
Koitharu 3 years ago
parent 7b8bbf9fe1
commit 42df607f52
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -103,8 +103,12 @@ dependencies {
//noinspection LifecycleAnnotationProcessorWithJava8 //noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1' kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'androidx.work:work-runtime-ktx:2.8.1'
// implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' //noinspection GradleDependency
implementation 'com.google.guava:guava:31.1-android' implementation('com.google.guava:guava:31.1-android') {
exclude group: 'com.google.guava', module: 'failureaccess'
exclude group: 'org.checkerframework', module: 'checker-qual'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
}
implementation 'androidx.room:room-runtime:2.5.1' implementation 'androidx.room:room-runtime:2.5.1'
implementation 'androidx.room:room-ktx:2.5.1' implementation 'androidx.room:room-ktx:2.5.1'

@ -10,6 +10,7 @@ data class DownloadState2(
val manga: Manga, val manga: Manga,
val isIndeterminate: Boolean, val isIndeterminate: Boolean,
val isPaused: Boolean = false, val isPaused: Boolean = false,
val isStopped: Boolean = false,
val error: String? = null, val error: String? = null,
val totalChapters: Int = 0, val totalChapters: Int = 0,
val currentChapter: Int = 0, val currentChapter: Int = 0,
@ -17,6 +18,7 @@ data class DownloadState2(
val currentPage: Int = 0, val currentPage: Int = 0,
val eta: Long = -1L, val eta: Long = -1L,
val localManga: LocalManga? = null, val localManga: LocalManga? = null,
val downloadedChapters: LongArray = LongArray(0),
val timestamp: Long = System.currentTimeMillis(), val timestamp: Long = System.currentTimeMillis(),
) { ) {
@ -30,7 +32,7 @@ data class DownloadState2(
get() = localManga != null || (error != null && !isPaused) get() = localManga != null || (error != null && !isPaused)
val isParticularProgress: Boolean 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() fun toWorkData() = Data.Builder()
.putLong(DATA_MANGA_ID, manga.id) .putLong(DATA_MANGA_ID, manga.id)
@ -39,7 +41,7 @@ data class DownloadState2(
.putLong(DATA_ETA, eta) .putLong(DATA_ETA, eta)
.putLong(DATA_TIMESTAMP, timestamp) .putLong(DATA_TIMESTAMP, timestamp)
.putString(DATA_ERROR, error) .putString(DATA_ERROR, error)
.putInt(DATA_CHAPTERS, totalChapters) .putLongArray(DATA_CHAPTERS, downloadedChapters)
.putBoolean(DATA_INDETERMINATE, isIndeterminate) .putBoolean(DATA_INDETERMINATE, isIndeterminate)
.putBoolean(DATA_PAUSED, isPaused) .putBoolean(DATA_PAUSED, isPaused)
.build() .build()
@ -72,6 +74,6 @@ data class DownloadState2(
fun getTimestamp(data: Data): Date = Date(data.getLong(DATA_TIMESTAMP, 0L)) 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? { 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) val mangaId = DownloadState2.getMangaId(workData)
if (mangaId == 0L) return null if (mangaId == 0L) return null
val manga = getManga(mangaId) ?: return null val manga = getManga(mangaId) ?: return null
@ -187,7 +187,7 @@ class DownloadsViewModel @Inject constructor(
progress = DownloadState2.getProgress(workData), progress = DownloadState2.getProgress(workData),
eta = DownloadState2.getEta(workData), eta = DownloadState2.getEta(workData),
timestamp = DownloadState2.getTimestamp(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()) 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) state.isPaused -> { // paused (with error or manually)
builder.setProgress(state.max, state.progress, false) 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 >= 0) {
builder.setContentText(percent) context.getString(R.string.percent_string_pattern, (state.percent * 100).format())
} else {
null
}
if (state.error != null) {
builder.setContentText(state.error) builder.setContentText(state.error)
builder.setSubText(percent)
} else {
builder.setContentText(percent)
}
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
builder.setStyle(null) builder.setStyle(null)
builder.setOngoing(true) builder.setOngoing(true)
@ -161,7 +179,11 @@ class DownloadNotificationFactory @AssistedInject constructor(
else -> { else -> {
builder.setProgress(state.max, state.progress, false) 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) { if (state.eta > 0L) {
val eta = DateUtils.getRelativeTimeSpanString( val eta = DateUtils.getRelativeTimeSpanString(
state.eta, 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.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga 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.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.mapToSet 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 private val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@Volatile @Volatile
private lateinit var currentState: DownloadState2 private var lastPublishedState: DownloadState2? = null
private val currentState: DownloadState2
get() = checkNotNull(lastPublishedState)
private val pausingHandle = PausingHandle() private val pausingHandle = PausingHandle()
private val timeLeftEstimator = TimeLeftEstimator() private val timeLeftEstimator = TimeLeftEstimator()
@ -94,30 +97,44 @@ class DownloadWorker @AssistedInject constructor(
val mangaId = inputData.getLong(MANGA_ID, 0L) val mangaId = inputData.getLong(MANGA_ID, 0L)
val manga = mangaDataRepository.findMangaById(mangaId) ?: return Result.failure() val manga = mangaDataRepository.findMangaById(mangaId) ?: return Result.failure()
val chaptersIds = inputData.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() } val chaptersIds = inputData.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() }
currentState = DownloadState2(manga, isIndeterminate = true) val downloadedIds = getDoneChapters()
lastPublishedState = DownloadState2(manga, isIndeterminate = true)
return try { return try {
downloadMangaImpl(chaptersIds) downloadMangaImpl(chaptersIds, downloadedIds)
Result.success(currentState.toWorkData()) Result.success(currentState.toWorkData())
} catch (e: CancellationException) { } catch (e: CancellationException) {
withContext(NonCancellable) {
val notification = notificationFactory.create(currentState.copy(isStopped = true))
notificationManager.notify(id.hashCode(), notification)
}
throw e throw e
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTraceDebug() e.printStackTraceDebug()
Result.retry() Result.retry()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTraceDebug() e.printStackTraceDebug()
currentState = currentState.copy(error = e.getDisplayMessage(applicationContext.resources), eta = -1L) Result.failure(
Result.failure(currentState.toWorkData()) currentState.copy(
error = e.getDisplayMessage(applicationContext.resources),
eta = -1L,
).toWorkData(),
)
} finally {
notificationManager.cancel(id.hashCode())
} }
} }
override suspend fun getForegroundInfo() = ForegroundInfo( override suspend fun getForegroundInfo() = ForegroundInfo(
id.hashCode(), 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 var manga = currentState.manga
val chaptersIdsSet = chaptersIds?.toMutableSet() val chaptersToSkip = excludedIds.toMutableSet()
withMangaLock(manga) { withMangaLock(manga) {
ContextCompat.registerReceiver( ContextCompat.registerReceiver(
applicationContext, applicationContext,
@ -135,26 +152,24 @@ class DownloadWorker @AssistedInject constructor(
?: error("Cannot obtain remote manga instance") ?: error("Cannot obtain remote manga instance")
} }
val repo = mangaRepositoryFactory.create(manga.source) val repo = mangaRepositoryFactory.create(manga.source)
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga val mangaDetails = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = LocalMangaOutput.getOrCreate(destination, data) output = LocalMangaOutput.getOrCreate(destination, mangaDetails)
val coverUrl = data.largeCoverUrl.ifNullOrEmpty { data.coverUrl } val coverUrl = mangaDetails.largeCoverUrl.ifNullOrEmpty { mangaDetails.coverUrl }
if (coverUrl.isNotEmpty()) { if (coverUrl.isNotEmpty()) {
downloadFile(coverUrl, destination, tempFileName, repo.source).let { file -> downloadFile(coverUrl, destination, tempFileName, repo.source).let { file ->
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl)) output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
} }
} }
val chapters = checkNotNull( val chapters = getChapters(mangaDetails, includedIds)
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"
}
for ((chapterIndex, chapter) in chapters.withIndex()) { for ((chapterIndex, chapter) in chapters.withIndex()) {
if (chaptersToSkip.remove(chapter.id)) {
publishState(
currentState.copy(
downloadedChapters = currentState.downloadedChapters + chapter.id,
),
)
continue
}
val pages = runFailsafe(pausingHandle) { val pages = runFailsafe(pausingHandle) {
repo.getPages(chapter) repo.getPages(chapter)
} }
@ -190,6 +205,11 @@ class DownloadWorker @AssistedInject constructor(
localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga()) localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga())
}.onFailure(Throwable::printStackTraceDebug) }.onFailure(Throwable::printStackTraceDebug)
} }
publishState(
currentState.copy(
downloadedChapters = currentState.downloadedChapters + chapter.id,
),
)
} }
publishState(currentState.copy(isIndeterminate = true, eta = -1L)) publishState(currentState.copy(isIndeterminate = true, eta = -1L))
output.mergeWithExisting() output.mergeWithExisting()
@ -273,7 +293,7 @@ class DownloadWorker @AssistedInject constructor(
private suspend fun publishState(state: DownloadState2) { private suspend fun publishState(state: DownloadState2) {
val previousState = currentState val previousState = currentState
currentState = state lastPublishedState = state
if (previousState.isParticularProgress && state.isParticularProgress) { if (previousState.isParticularProgress && state.isParticularProgress) {
timeLeftEstimator.tick(state.progress, state.max) timeLeftEstimator.tick(state.progress, state.max)
} else { } else {
@ -291,6 +311,30 @@ class DownloadWorker @AssistedInject constructor(
setProgress(state.toWorkData()) 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 { private suspend inline fun <T> withMangaLock(manga: Manga, block: () -> T) = try {
localMangaRepository.lockManga(manga.id) localMangaRepository.lockManga(manga.id)
block() block()

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

Loading…
Cancel
Save