From 6ad4b425e49b3a69863fe97c77dc930ceb08e778 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Fri, 2 Apr 2021 21:41:06 +0200 Subject: [PATCH 1/5] Better error handling of terminated channels when loading feed --- app/build.gradle | 2 +- .../schabi/newpipe/local/feed/FeedFragment.kt | 75 ++++++++++++++++++- .../local/feed/service/FeedLoadService.kt | 21 +++++- app/src/main/res/values/strings.xml | 5 ++ 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ed9fad797..054d91fa5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -183,7 +183,7 @@ dependencies { /** NewPipe libraries **/ // You can use a local version by uncommenting a few lines in settings.gradle implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' - implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.4' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:d4186d100b6c6dddfcf3cf4b004f5960a8bf441d' /** Checkstyle **/ checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index 1df999144..f0637e6ba 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -19,6 +19,7 @@ package org.schabi.newpipe.local.feed +import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.os.Parcelable @@ -28,6 +29,7 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.annotation.Nullable import androidx.appcompat.app.AlertDialog import androidx.core.content.edit import androidx.core.os.bundleOf @@ -35,15 +37,24 @@ import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import icepick.State +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.schedulers.Schedulers +import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.databinding.FragmentFeedBinding import org.schabi.newpipe.error.ErrorInfo import org.schabi.newpipe.error.UserAction +import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException +import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.fragments.list.BaseListFragment import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling import org.schabi.newpipe.local.feed.service.FeedLoadService +import org.schabi.newpipe.local.subscription.SubscriptionManager import org.schabi.newpipe.util.Localization import java.time.OffsetDateTime @@ -51,6 +62,8 @@ class FeedFragment : BaseListFragment() { private var _feedBinding: FragmentFeedBinding? = null private val feedBinding get() = _feedBinding!! + private val disposables = CompositeDisposable() + private lateinit var viewModel: FeedViewModel @State @JvmField @@ -158,6 +171,7 @@ class FeedFragment : BaseListFragment() { } override fun onDestroy() { + disposables.dispose() super.onDestroy() activity?.supportActionBar?.subtitle = null } @@ -243,9 +257,9 @@ class FeedFragment : BaseListFragment() { oldestSubscriptionUpdate = loadedState.oldestUpdate - val loadedCount = loadedState.notLoadedCount > 0 - feedBinding.refreshSubtitleText.isVisible = loadedCount - if (loadedCount) { + val feedsNotLoaded = loadedState.notLoadedCount > 0 + feedBinding.refreshSubtitleText.isVisible = feedsNotLoaded + if (feedsNotLoaded) { feedBinding.refreshSubtitleText.text = getString( R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount @@ -264,11 +278,64 @@ class FeedFragment : BaseListFragment() { hideLoading() false } else { - showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed")) + if (errorState.error is FeedLoadService.RequestException) { + disposables.add( + Single.fromCallable { + NewPipeDatabase.getInstance(requireContext()).subscriptionDAO() + .getSubscription(errorState.error.subscriptionId) + }.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + subscriptionEntity -> + handleFeedNotAvailable( + subscriptionEntity, + errorState.error.cause?.cause + ) + }, + { throwable -> throwable.printStackTrace() } + ) + ) + } else { + showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed")) + } true } } + private fun handleFeedNotAvailable( + subscriptionEntity: SubscriptionEntity, + @Nullable cause: Throwable? + ) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val isFastFeedModeEnabled = sharedPreferences.getBoolean( + getString(R.string.feed_use_dedicated_fetch_method_key), false + ) + val builder = AlertDialog.Builder(requireContext()) + .setTitle(R.string.feed_load_error) + .setPositiveButton( + R.string.unsubscribe, + DialogInterface.OnClickListener { + _, _ -> + SubscriptionManager(requireContext()).deleteSubscription( + subscriptionEntity.serviceId, subscriptionEntity.url + ).subscribe() + } + ) + .setNegativeButton(R.string.cancel, DialogInterface.OnClickListener { _, _ -> }) + if (cause is AccountTerminatedException) { + builder.setMessage(R.string.feed_load_error_terminated) + } else if (cause is ContentNotAvailableException && isFastFeedModeEnabled) { + builder.setMessage(R.string.feed_load_error_fast_unknown) + .setNeutralButton(R.string.feed_use_dedicated_fetch_method_disable_button) { _, _ -> + sharedPreferences.edit { + putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false) + } + } + } + builder.create().show() + } + private fun updateRelativeTimeViews() { updateRefreshViewState() infoListAdapter.notifyDataSetChanged() diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt index 5ed7998d2..7ca583317 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt @@ -48,6 +48,7 @@ import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity 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.ktx.isNetworkRelated @@ -162,7 +163,7 @@ class FeedLoadService : Service() { // Loading & Handling // ///////////////////////////////////////////////////////////////////////// - private class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) { + public class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) { companion object { fun wrapList(subscriptionId: Long, info: ListInfo): List { val toReturn = ArrayList(info.errors.size) @@ -334,8 +335,9 @@ class FeedLoadService : Service() { private val errorHandlingConsumer: Consumer>>> get() = Consumer { if (it.isOnError) { - var error = it.error!! - if (error is RequestException) error = error.cause!! + var maybeWrapper = it.error!! + val error = if (maybeWrapper is RequestException) maybeWrapper.cause!! + else maybeWrapper val cause = error.cause when { @@ -345,6 +347,19 @@ class FeedLoadService : Service() { 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 + } + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fb4019bd..6fc4e73cd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -690,11 +690,16 @@ Feed update threshold Time after last update before a subscription is considered outdated — %s Always update + Error loading feed + Could not load feed for \'%s\'. + The author\'s account has been terminated.\nNewPipe will not be able to load this feed in the future.\Do you want to unsubscribe from this channel? + The fast feed mode does not provide more info on this. Fetch from dedicated feed when available Available in some services, it is usually much faster but may return a limited amount of items and often incomplete information (e.g. no duration, item type, no live status). Enable fast mode Disable fast mode Do you think feed loading is too slow? If so, try enabling fast loading (you can change it in settings or by pressing the button below).\n\nNewPipe offers two feed loading strategies:\n• Fetching the whole subscription channel, which is slow but complete.\n• Using a dedicated service endpoint, which is fast but usually not complete.\n\nThe difference between the two is that the fast one usually lacks some information, like the item\'s duration or type (can\'t distinguish between live videos and normal ones) and it may return less items.\n\nYouTube is an example of a service that offers this fast method with its RSS feed.\n\nSo the choice boils down to what you prefer: speed or precise information. + This content is not yet supported by NewPipe.\n\nIt will hopefully be supported in a future version. Channel\'s avatar thumbnail Created by %s From ccc46971b4990f4e28eec09e6db3c7fe94e7ec2d Mon Sep 17 00:00:00 2001 From: TobiGr Date: Wed, 31 Mar 2021 22:46:52 +0200 Subject: [PATCH 2/5] Show detailed error message when an account has been terminated by the service --- .../org/schabi/newpipe/error/ErrorInfo.kt | 2 ++ .../schabi/newpipe/error/ErrorPanelHelper.kt | 33 +++++++++++++++++++ app/src/main/res/layout/error_panel.xml | 24 +++++++++++++- app/src/main/res/values/strings.xml | 2 ++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt index e1249bc83..487e7c7fb 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt @@ -6,6 +6,7 @@ import kotlinx.android.parcel.Parcelize import org.schabi.newpipe.R import org.schabi.newpipe.extractor.Info import org.schabi.newpipe.extractor.NewPipe +import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException import org.schabi.newpipe.extractor.exceptions.ExtractionException @@ -95,6 +96,7 @@ class ErrorInfo( action: UserAction ): Int { return when { + throwable is AccountTerminatedException -> R.string.account_terminated throwable is ContentNotAvailableException -> R.string.content_not_available throwable != null && throwable.isNetworkRelated -> R.string.network_error throwable is ContentNotSupportedException -> R.string.content_not_supported diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt index 49bcfa926..e790c5fc5 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt @@ -13,6 +13,8 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable import org.schabi.newpipe.MainActivity import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.NewPipe +import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException @@ -22,9 +24,11 @@ import org.schabi.newpipe.extractor.exceptions.PrivateContentException import org.schabi.newpipe.extractor.exceptions.ReCaptchaException import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException +import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.isInterruptedCaused import org.schabi.newpipe.ktx.isNetworkRelated +import org.schabi.newpipe.util.ServiceHelper import java.util.concurrent.TimeUnit class ErrorPanelHelper( @@ -35,6 +39,8 @@ class ErrorPanelHelper( private val context: Context = rootView.context!! private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel) private val errorTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_view) + private val errorServiceInfoTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_info_view) + private val errorServiceExplenationTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_explenation_view) private val errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action) private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry) @@ -70,13 +76,40 @@ class ErrorPanelHelper( errorButtonAction.setOnClickListener(null) } errorTextView.setText(R.string.recaptcha_request_toast) + // additional info is only provided by AccountTerminatedException + errorServiceInfoTextView.isVisible = false + errorServiceExplenationTextView.isVisible = false errorButtonRetry.isVisible = true + } else if (errorInfo.throwable is AccountTerminatedException) { + errorButtonRetry.isVisible = false + errorButtonAction.isVisible = false + errorTextView.setText(R.string.account_terminated) + if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) { + errorServiceInfoTextView.setText( + context.resources.getString( + R.string.service_provides_reason, + NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context)) + ) + ) + errorServiceExplenationTextView.setText( + (errorInfo.throwable as AccountTerminatedException).message + ) + errorServiceInfoTextView.isVisible = true + errorServiceExplenationTextView.isVisible = true + } else { + errorServiceInfoTextView.isVisible = false + errorServiceExplenationTextView.isVisible = false + } } else { errorButtonAction.setText(R.string.error_snackbar_action) errorButtonAction.setOnClickListener { ErrorActivity.reportError(context, errorInfo) } + // additional info is only provided by AccountTerminatedException + errorServiceInfoTextView.isVisible = false + errorServiceExplenationTextView.isVisible = false + // hide retry button by default, then show only if not unavailable/unsupported content errorButtonRetry.isVisible = false errorTextView.setText( diff --git a/app/src/main/res/layout/error_panel.xml b/app/src/main/res/layout/error_panel.xml index 5141b66b8..355dd17e3 100644 --- a/app/src/main/res/layout/error_panel.xml +++ b/app/src/main/res/layout/error_panel.xml @@ -15,7 +15,29 @@ android:text="@string/general_error" android:textSize="16sp" android:textStyle="bold" - tools:text="Network error" /> + tools:text="Account terminated" /> + + + + +