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/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/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index 1df999144..0d7a9a11f 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 @@ -28,6 +28,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 +36,25 @@ 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.extractor.utils.Utils.isNullOrEmpty 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 } @@ -241,17 +255,23 @@ class FeedFragment : BaseListFragment() { listState = null } - 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 ) } + if (oldestSubscriptionUpdate != loadedState.oldestUpdate || + (oldestSubscriptionUpdate == null && loadedState.oldestUpdate == null) + ) { + // ignore errors if they have already been handled for the current update + handleItemsErrors(loadedState.itemsErrors) + } + oldestSubscriptionUpdate = loadedState.oldestUpdate + if (loadedState.items.isEmpty()) { showEmptyState() } else { @@ -269,6 +289,72 @@ class FeedFragment : BaseListFragment() { } } + private fun handleItemsErrors(errors: List) { + errors.forEachIndexed() { i, t -> + if (t is FeedLoadService.RequestException && + t.cause is ContentNotAvailableException + ) { + Single.fromCallable { + NewPipeDatabase.getInstance(requireContext()).subscriptionDAO() + .getSubscription(t.subscriptionId) + }.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + subscriptionEntity -> + handleFeedNotAvailable( + subscriptionEntity, + t.cause, + errors.subList(i + 1, errors.size) + ) + }, + { throwable -> throwable.printStackTrace() } + ) + return // this will be called on the remaining errors by handleFeedNotAvailable() + } + } + } + + private fun handleFeedNotAvailable( + subscriptionEntity: SubscriptionEntity, + @Nullable cause: Throwable?, + nextItemsErrors: List + ) { + 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 + ) { _, _ -> + SubscriptionManager(requireContext()).deleteSubscription( + subscriptionEntity.serviceId, subscriptionEntity.url + ).subscribe() + handleItemsErrors(nextItemsErrors) + } + .setNegativeButton(R.string.cancel) { _, _ -> } + + var message = getString(R.string.feed_load_error_account_info, subscriptionEntity.name) + if (cause is AccountTerminatedException) { + message += "\n" + getString(R.string.feed_load_error_terminated) + } else if (cause is ContentNotAvailableException) { + if (isFastFeedModeEnabled) { + message += "\n" + getString(R.string.feed_load_error_fast_unknown) + builder.setNeutralButton(R.string.feed_use_dedicated_fetch_method_disable_button) { _, _ -> + sharedPreferences.edit { + putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false) + } + } + } else if (!isNullOrEmpty(cause.message)) { + message += "\n" + cause.message + } + } + builder.setMessage(message).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..3638b4c0e 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,9 +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.ReCaptchaException 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.service.FeedEventManager.Event.ErrorResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent @@ -58,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.subscription.SubscriptionManager import org.schabi.newpipe.util.ExtractorHelper -import java.io.IOException import java.time.OffsetDateTime import java.time.ZoneOffset import java.util.concurrent.TimeUnit @@ -162,7 +159,7 @@ class FeedLoadService : Service() { // Loading & Handling // ///////////////////////////////////////////////////////////////////////// - private 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 { fun wrapList(subscriptionId: Long, info: ListInfo): List { val toReturn = ArrayList(info.errors.size) @@ -209,29 +206,40 @@ class FeedLoadService : Service() { .filter { !cancelSignal.get() } .map { subscriptionEntity -> + var error: Throwable? = null try { val listInfo = if (useFeedExtractor) { ExtractorHelper .getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url) + .onErrorReturn { + error = it // store error, otherwise wrapped into RuntimeException + throw it + } .blockingGet() } else { ExtractorHelper .getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true) + .onErrorReturn { + error = it // store error, otherwise wrapped into RuntimeException + throw it + } .blockingGet() } as ListInfo return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo)) } catch (e: Throwable) { + if (error == null) { + // do this to prevent blockingGet() from wrapping into RuntimeException + error = e + } + val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}" - val wrapper = RequestException(subscriptionEntity.uid, request, e) + val wrapper = RequestException(subscriptionEntity.uid, request, error!!) return@map Notification.createOnError>>(wrapper) } } .sequential() - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext(errorHandlingConsumer) - .observeOn(AndroidSchedulers.mainThread()) .doOnNext(notificationsConsumer) @@ -331,24 +339,6 @@ class FeedLoadService : Service() { } } - private val errorHandlingConsumer: Consumer>>> - get() = Consumer { - if (it.isOnError) { - var error = it.error!! - if (error is RequestException) error = error.cause!! - 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) - } - } - } - private val notificationsConsumer: Consumer>>> get() = Consumer { onItemCompleted(it.value?.second?.name) } 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" /> + + + + +