Ability to skip error in downloader

This commit is contained in:
Koitharu
2023-12-02 15:03:15 +02:00
parent b1fa9d1d22
commit 1a7b1e7bdc
12 changed files with 79 additions and 50 deletions

View File

@@ -1,26 +0,0 @@
package org.koitharu.kotatsu.core.util.progress
import androidx.annotation.AnyThread
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.download.ui.worker.PausingHandle
class PausingProgressJob<P>(
job: Job,
progress: StateFlow<P>,
private val pausingHandle: PausingHandle,
) : ProgressJob<P>(job, progress) {
@get:AnyThread
val isPaused: Boolean
get() = pausingHandle.isPaused
@AnyThread
suspend fun awaitResumed() = pausingHandle.awaitResumed()
@AnyThread
fun pause() = pausingHandle.pause()
@AnyThread
fun resume() = pausingHandle.resume()
}

View File

@@ -44,7 +44,8 @@ fun downloadItemAD(
override fun onClick(v: View) {
when (v.id) {
R.id.button_cancel -> listener.onCancelClick(item)
R.id.button_resume -> listener.onResumeClick(item)
R.id.button_resume -> listener.onResumeClick(item, skip = false)
R.id.button_skip -> listener.onResumeClick(item, skip = true)
R.id.button_pause -> listener.onPauseClick(item)
R.id.imageView_expand -> listener.onExpandClick(item)
else -> listener.onItemClick(item, v)
@@ -62,6 +63,7 @@ fun downloadItemAD(
binding.buttonCancel.setOnClickListener(clickListener)
binding.buttonPause.setOnClickListener(clickListener)
binding.buttonResume.setOnClickListener(clickListener)
binding.buttonSkip.setOnClickListener(clickListener)
binding.imageViewExpand.setOnClickListener(clickListener)
itemView.setOnClickListener(clickListener)
itemView.setOnLongClickListener(clickListener)
@@ -120,6 +122,7 @@ fun downloadItemAD(
binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false
binding.buttonSkip.isVisible = false
binding.buttonPause.isVisible = false
}
@@ -134,9 +137,10 @@ fun downloadItemAD(
binding.progressBar.setProgressCompat(item.progress, payloads.isNotEmpty())
binding.textViewPercent.text = percentPattern.format((item.percent * 100f).format(1))
binding.textViewPercent.isVisible = true
binding.textViewDetails.textAndVisible = item.getEtaString()
binding.textViewDetails.textAndVisible = if (item.isPaused) item.error else item.getEtaString()
binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = item.isPaused
binding.buttonSkip.isVisible = item.isPaused && item.error != null
binding.buttonPause.isVisible = item.canPause
}
@@ -158,6 +162,7 @@ fun downloadItemAD(
}
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
binding.buttonSkip.isVisible = false
binding.buttonPause.isVisible = false
}
@@ -170,6 +175,7 @@ fun downloadItemAD(
binding.textViewDetails.textAndVisible = item.error
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
binding.buttonSkip.isVisible = false
binding.buttonPause.isVisible = false
}
@@ -182,6 +188,7 @@ fun downloadItemAD(
binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
binding.buttonSkip.isVisible = false
binding.buttonPause.isVisible = false
}
}

View File

@@ -8,7 +8,7 @@ interface DownloadItemListener : OnListItemClickListener<DownloadItemModel> {
fun onPauseClick(item: DownloadItemModel)
fun onResumeClick(item: DownloadItemModel)
fun onResumeClick(item: DownloadItemModel, skip: Boolean)
fun onExpandClick(item: DownloadItemModel)
}

View File

@@ -105,8 +105,8 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
sendBroadcast(PausingReceiver.getPauseIntent(this, item.id))
}
override fun onResumeClick(item: DownloadItemModel) {
sendBroadcast(PausingReceiver.getResumeIntent(this, item.id))
override fun onResumeClick(item: DownloadItemModel, skip: Boolean) {
sendBroadcast(PausingReceiver.getResumeIntent(this, item.id, skip))
}
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {

View File

@@ -143,7 +143,7 @@ class DownloadsViewModel @Inject constructor(
var isResumed = false
for (work in snapshot) {
if (work.workState == WorkInfo.State.RUNNING && work.isPaused) {
workScheduler.resume(work.id)
workScheduler.resume(work.id, skipError = false)
isResumed = true
}
}
@@ -156,7 +156,7 @@ class DownloadsViewModel @Inject constructor(
val snapshot = works.value ?: return
for (work in snapshot) {
if (work.id.mostSignificantBits in ids) {
workScheduler.resume(work.id)
workScheduler.resume(work.id, skipError = false)
}
}
onActionDone.call(ReversibleAction(R.string.downloads_resumed, null))

View File

@@ -82,7 +82,15 @@ class DownloadNotificationFactory @AssistedInject constructor(
NotificationCompat.Action(
R.drawable.ic_action_resume,
context.getString(R.string.resume),
PausingReceiver.createResumePendingIntent(context, uuid),
PausingReceiver.createResumePendingIntent(context, uuid, skipError = false),
)
}
private val actionSkip by lazy {
NotificationCompat.Action(
R.drawable.ic_action_skip,
context.getString(R.string.skip),
PausingReceiver.createResumePendingIntent(context, uuid, skipError = true),
)
}
@@ -163,6 +171,9 @@ class DownloadNotificationFactory @AssistedInject constructor(
builder.setSmallIcon(R.drawable.ic_stat_paused)
builder.addAction(actionCancel)
builder.addAction(actionResume)
if (state.error != null) {
builder.addAction(actionSkip)
}
}
state.error != null -> { // error, final state

View File

@@ -198,7 +198,7 @@ class DownloadWorker @AssistedInject constructor(
}
val pages = runFailsafe {
repo.getPages(chapter)
}
} ?: continue
val pageCounter = AtomicInteger(0)
channelFlow {
val semaphore = Semaphore(MAX_PAGES_PARALLELISM)
@@ -264,7 +264,7 @@ class DownloadWorker @AssistedInject constructor(
private suspend fun <R> runFailsafe(
block: suspend () -> R,
): R {
): R? {
checkIsPaused()
var countDown = MAX_FAILSAFE_ATTEMPTS
failsafe@ while (true) {
@@ -284,6 +284,9 @@ class DownloadWorker @AssistedInject constructor(
pausingHandle.pause()
try {
pausingHandle.awaitResumed()
if (pausingHandle.skipCurrentError()) {
return null
}
} finally {
publishState(currentState.copy(isPaused = false, error = null))
}
@@ -448,8 +451,8 @@ class DownloadWorker @AssistedInject constructor(
context.sendBroadcast(intent)
}
fun resume(id: UUID) {
val intent = PausingReceiver.getResumeIntent(context, id)
fun resume(id: UUID, skipError: Boolean) {
val intent = PausingReceiver.getResumeIntent(context, id, skipError)
context.sendBroadcast(intent)
}

View File

@@ -10,6 +10,7 @@ import kotlin.coroutines.CoroutineContext
class PausingHandle : AbstractCoroutineContextElement(PausingHandle) {
private val paused = MutableStateFlow(false)
private val isSkipError = MutableStateFlow(false)
@get:AnyThread
val isPaused: Boolean
@@ -26,7 +27,8 @@ class PausingHandle : AbstractCoroutineContextElement(PausingHandle) {
}
@AnyThread
fun resume() {
fun resume(skipError: Boolean) {
isSkipError.value = skipError
paused.value = false
}
@@ -36,6 +38,8 @@ class PausingHandle : AbstractCoroutineContextElement(PausingHandle) {
}
}
fun skipCurrentError(): Boolean = isSkipError.compareAndSet(expect = true, update = false)
companion object : CoroutineContext.Key<PausingHandle> {
suspend fun current() = checkNotNull(currentCoroutineContext()[this]) {

View File

@@ -21,7 +21,7 @@ class PausingReceiver(
return
}
when (intent.action) {
ACTION_RESUME -> pausingHandle.resume()
ACTION_RESUME -> pausingHandle.resume(intent.getBooleanExtra(EXTRA_SKIP_ERROR, false))
ACTION_PAUSE -> pausingHandle.pause()
}
}
@@ -31,6 +31,7 @@ class PausingReceiver(
private const val ACTION_PAUSE = "org.koitharu.kotatsu.download.PAUSE"
private const val ACTION_RESUME = "org.koitharu.kotatsu.download.RESUME"
private const val EXTRA_UUID = "uuid"
private const val EXTRA_SKIP_ERROR = "skip"
private const val SCHEME = "workuid"
fun createIntentFilter(id: UUID) = IntentFilter().apply {
@@ -45,10 +46,11 @@ class PausingReceiver(
.setPackage(context.packageName)
.putExtra(EXTRA_UUID, id.toString())
fun getResumeIntent(context: Context, id: UUID) = Intent(ACTION_RESUME)
fun getResumeIntent(context: Context, id: UUID, skipError: Boolean) = Intent(ACTION_RESUME)
.setData(Uri.parse("$SCHEME://$id"))
.setPackage(context.packageName)
.putExtra(EXTRA_UUID, id.toString())
.putExtra(EXTRA_SKIP_ERROR, skipError)
fun createPausePendingIntent(context: Context, id: UUID) = PendingIntentCompat.getBroadcast(
context,
@@ -58,12 +60,13 @@ class PausingReceiver(
false,
)
fun createResumePendingIntent(context: Context, id: UUID) = PendingIntentCompat.getBroadcast(
context,
0,
getResumeIntent(context, id),
0,
false,
)
fun createResumePendingIntent(context: Context, id: UUID, skipError: Boolean) =
PendingIntentCompat.getBroadcast(
context,
0,
getResumeIntent(context, id, skipError),
0,
false,
)
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M16,18H18V6H16M6,18L14.5,12L6,6V18Z" />
</vector>

View File

@@ -154,7 +154,7 @@
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/button_resume"
app:layout_constraintTop_toBottomOf="@id/recyclerView_chapters"
tools:visibility="visible" />
tools:visibility="gone" />
<Button
android:id="@+id/button_resume"
@@ -166,7 +166,21 @@
android:text="@string/resume"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/button_cancel"
app:layout_constraintTop_toBottomOf="@id/recyclerView_chapters" />
app:layout_constraintTop_toBottomOf="@id/recyclerView_chapters"
tools:visibility="visible" />
<Button
android:id="@+id/button_skip"
style="?materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="12dp"
android:text="@string/skip"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/button_resume"
app:layout_constraintTop_toBottomOf="@id/recyclerView_chapters"
tools:visibility="visible" />
<Button
android:id="@+id/button_cancel"

View File

@@ -534,4 +534,5 @@
<string name="error_multiple_states_not_supported">Filtering by multiple states is not supported by this manga source</string>
<string name="error_search_not_supported">Search is not supported by this manga source</string>
<string name="downloads_settings_info">You can enable download slowdown for each manga source individually in the source settings if you are having problems with server-side blocking</string>
<string name="skip">Skip</string>
</resources>