Ability to skip error in downloader
This commit is contained in:
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
12
app/src/main/res/drawable/ic_action_skip.xml
Normal file
12
app/src/main/res/drawable/ic_action_skip.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user