Fix feed loading and show a dialog for each invalid subscription

This commit is contained in:
Stypox 2021-04-02 18:16:24 +02:00 committed by TobiGr
parent c5dd3dc7a9
commit 89317d4abc
2 changed files with 61 additions and 75 deletions

View file

@ -19,7 +19,6 @@
package org.schabi.newpipe.local.feed package org.schabi.newpipe.local.feed
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
@ -256,8 +255,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
listState = null listState = null
} }
oldestSubscriptionUpdate = loadedState.oldestUpdate
val feedsNotLoaded = loadedState.notLoadedCount > 0 val feedsNotLoaded = loadedState.notLoadedCount > 0
feedBinding.refreshSubtitleText.isVisible = feedsNotLoaded feedBinding.refreshSubtitleText.isVisible = feedsNotLoaded
if (feedsNotLoaded) { if (feedsNotLoaded) {
@ -267,6 +264,12 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
) )
} }
if (oldestSubscriptionUpdate != loadedState.oldestUpdate) {
// ignore errors if they have already been handled for the current update
handleItemsErrors(loadedState.itemsErrors)
}
oldestSubscriptionUpdate = loadedState.oldestUpdate
if (loadedState.items.isEmpty()) { if (loadedState.items.isEmpty()) {
showEmptyState() showEmptyState()
} else { } else {
@ -279,11 +282,19 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
hideLoading() hideLoading()
false false
} else { } else {
if (errorState.error is FeedLoadService.RequestException) { showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed"))
disposables.add( true
}
}
private fun handleItemsErrors(errors: List<Throwable>) {
errors.forEachIndexed() { i, t ->
if (t is FeedLoadService.RequestException &&
t.cause is ContentNotAvailableException
) {
Single.fromCallable { Single.fromCallable {
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO() NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
.getSubscription(errorState.error.subscriptionId) .getSubscription(t.subscriptionId)
}.subscribeOn(Schedulers.io()) }.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
@ -291,39 +302,39 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
subscriptionEntity -> subscriptionEntity ->
handleFeedNotAvailable( handleFeedNotAvailable(
subscriptionEntity, subscriptionEntity,
errorState.error.cause?.cause t.cause?.cause,
errors.subList(i + 1, errors.size)
) )
}, },
{ throwable -> throwable.printStackTrace() } { throwable -> throwable.printStackTrace() }
) )
) return // this will be called on the remaining errors by handleFeedNotAvailable()
} else {
showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed"))
} }
true
} }
} }
private fun handleFeedNotAvailable( private fun handleFeedNotAvailable(
subscriptionEntity: SubscriptionEntity, subscriptionEntity: SubscriptionEntity,
@Nullable cause: Throwable? @Nullable cause: Throwable?,
nextItemsErrors: List<Throwable>
) { ) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val isFastFeedModeEnabled = sharedPreferences.getBoolean( val isFastFeedModeEnabled = sharedPreferences.getBoolean(
getString(R.string.feed_use_dedicated_fetch_method_key), false getString(R.string.feed_use_dedicated_fetch_method_key), false
) )
val builder = AlertDialog.Builder(requireContext()) val builder = AlertDialog.Builder(requireContext())
.setTitle(R.string.feed_load_error) .setTitle(R.string.feed_load_error)
.setPositiveButton( .setPositiveButton(
R.string.unsubscribe, R.string.unsubscribe
DialogInterface.OnClickListener { ) { _, _ ->
_, _ ->
SubscriptionManager(requireContext()).deleteSubscription( SubscriptionManager(requireContext()).deleteSubscription(
subscriptionEntity.serviceId, subscriptionEntity.url subscriptionEntity.serviceId, subscriptionEntity.url
).subscribe() ).subscribe()
handleItemsErrors(nextItemsErrors)
} }
) .setNegativeButton(R.string.cancel) { _, _ -> }
.setNegativeButton(R.string.cancel, DialogInterface.OnClickListener { _, _ -> })
var message = getString(R.string.feed_load_error_account_info, subscriptionEntity.name) var message = getString(R.string.feed_load_error_account_info, subscriptionEntity.name)
if (cause is AccountTerminatedException) { if (cause is AccountTerminatedException) {
message += "\n" + getString(R.string.feed_load_error_terminated) message += "\n" + getString(R.string.feed_load_error_terminated)

View file

@ -48,10 +48,7 @@ import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.ListInfo import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
@ -59,7 +56,6 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResul
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import org.schabi.newpipe.local.subscription.SubscriptionManager import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ExtractorHelper import org.schabi.newpipe.util.ExtractorHelper
import java.io.IOException
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -163,7 +159,7 @@ class FeedLoadService : Service() {
// Loading & Handling // Loading & Handling
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
public class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) { class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
companion object { companion object {
fun wrapList(subscriptionId: Long, info: ListInfo<StreamInfoItem>): List<Throwable> { fun wrapList(subscriptionId: Long, info: ListInfo<StreamInfoItem>): List<Throwable> {
val toReturn = ArrayList<Throwable>(info.errors.size) val toReturn = ArrayList<Throwable>(info.errors.size)
@ -210,29 +206,40 @@ class FeedLoadService : Service() {
.filter { !cancelSignal.get() } .filter { !cancelSignal.get() }
.map { subscriptionEntity -> .map { subscriptionEntity ->
var error: Throwable? = null
try { try {
val listInfo = if (useFeedExtractor) { val listInfo = if (useFeedExtractor) {
ExtractorHelper ExtractorHelper
.getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url) .getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url)
.onErrorReturn {
error = it // store error, otherwise wrapped into RuntimeException
throw it
}
.blockingGet() .blockingGet()
} else { } else {
ExtractorHelper ExtractorHelper
.getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true) .getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true)
.onErrorReturn {
error = it // store error, otherwise wrapped into RuntimeException
throw it
}
.blockingGet() .blockingGet()
} as ListInfo<StreamInfoItem> } as ListInfo<StreamInfoItem>
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo)) return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo))
} catch (e: Throwable) { } catch (e: Throwable) {
if (error == null) {
// do this to prevent blockingGet() from wrapping into RuntimeException
error = e
}
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}" val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
val wrapper = RequestException(subscriptionEntity.uid, request, e) val wrapper = RequestException(subscriptionEntity.uid, request, error!!)
return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper) return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper)
} }
} }
.sequential() .sequential()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(errorHandlingConsumer)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext(notificationsConsumer) .doOnNext(notificationsConsumer)
@ -332,38 +339,6 @@ class FeedLoadService : Service() {
} }
} }
private val errorHandlingConsumer: Consumer<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>
get() = Consumer {
if (it.isOnError) {
var maybeWrapper = it.error!!
val error = if (maybeWrapper is RequestException) maybeWrapper.cause!!
else maybeWrapper
val cause = error.cause
when {
error is ReCaptchaException -> throw error
cause is ReCaptchaException -> throw cause
error is IOException -> throw error
cause is IOException -> throw cause
error.isNetworkRelated -> throw IOException(error)
cause is ContentNotAvailableException -> {
// maybeWrapper is definitely a RequestException,
// because this is an exception thrown in the extractor
if (maybeWrapper is RequestException) {
throw maybeWrapper
} else {
if (DEBUG) {
Log.d(TAG, "Cause is ContentNotAvailableException, but maybeWrapper is not a RequestException")
}
throw cause // should never be the case
}
}
}
}
}
private val notificationsConsumer: Consumer<Notification<Pair<Long, ListInfo<StreamInfoItem>>>> private val notificationsConsumer: Consumer<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>
get() = Consumer { onItemCompleted(it.value?.second?.name) } get() = Consumer { onItemCompleted(it.value?.second?.name) }