Allow ErrorInfo messages with formatArgs
- ErrorInfo.getMessage() now returns an ErrorMessage instance that can be formatted into a string using a context (this allows the construction of an ErrorInfo to remain independent of a Context) - now the service ID is used in ErrorInfo.getMessage() to customize some messages based on the currently selected service - player HTTP invalid statuses are now included in the message - building a custom error message for AccountTerminatedException was moved from ErrorPanelHelper to ErrorInfo
This commit is contained in:
parent
1bde2dcd9f
commit
a369deeef4
72 changed files with 154 additions and 146 deletions
|
|
@ -276,7 +276,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||
|| errorInfo.getThrowable() instanceof ContentNotSupportedException) {
|
||||
// this exception does not usually indicate a problem that should be reported,
|
||||
// so just show a toast instead of the notification
|
||||
Toast.makeText(context, errorInfo.getMessageStringId(), Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(context, errorInfo.getMessage(context), Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
ErrorUtil.createNotification(context, errorInfo);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public class AcraReportSender implements ReportSender {
|
|||
ErrorUtil.openActivity(context, new ErrorInfo(
|
||||
new String[]{report.getString(ReportField.STACK_TRACE)},
|
||||
UserAction.UI_ERROR,
|
||||
ErrorInfo.SERVICE_NONE,
|
||||
null,
|
||||
"ACRA report",
|
||||
R.string.app_ui_crash));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ public class ErrorActivity extends AppCompatActivity {
|
|||
|
||||
// normal bugreport
|
||||
buildInfo(errorInfo);
|
||||
activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId());
|
||||
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage(this));
|
||||
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
|
||||
|
||||
// print stack trace once again for debugging:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package org.schabi.newpipe.error
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.exoplayer2.ExoPlaybackException
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource
|
||||
import com.google.android.exoplayer2.upstream.Loader
|
||||
|
|
@ -9,6 +11,8 @@ import kotlinx.parcelize.IgnoredOnParcel
|
|||
import kotlinx.parcelize.Parcelize
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Info
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.ServiceList.YouTube
|
||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
|
|
@ -18,22 +22,21 @@ import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException
|
|||
import org.schabi.newpipe.extractor.exceptions.PaidContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.PrivateContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.extractor.exceptions.SignInConfirmNotBotException
|
||||
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.UnsupportedContentInCountryException
|
||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.YoutubeSignInConfirmNotBotException
|
||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||
import org.schabi.newpipe.player.mediasource.FailedMediaSource
|
||||
import org.schabi.newpipe.player.resolver.PlaybackResolver
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
|
||||
@Parcelize
|
||||
class ErrorInfo(
|
||||
class ErrorInfo private constructor(
|
||||
val stackTraces: Array<String>,
|
||||
val userAction: UserAction,
|
||||
val serviceName: String,
|
||||
val serviceId: Int?,
|
||||
val request: String,
|
||||
val messageStringId: Int
|
||||
private val message: ErrorMessage,
|
||||
) : Parcelable {
|
||||
|
||||
// no need to store throwable, all data for report is in other variables
|
||||
|
|
@ -44,14 +47,14 @@ class ErrorInfo(
|
|||
private constructor(
|
||||
throwable: Throwable,
|
||||
userAction: UserAction,
|
||||
serviceName: String,
|
||||
serviceId: Int?,
|
||||
request: String
|
||||
) : this(
|
||||
throwableToStringList(throwable),
|
||||
userAction,
|
||||
serviceName,
|
||||
serviceId,
|
||||
request,
|
||||
getMessageStringId(throwable, userAction)
|
||||
getMessage(throwable, userAction, serviceId)
|
||||
) {
|
||||
this.throwable = throwable
|
||||
}
|
||||
|
|
@ -59,97 +62,176 @@ class ErrorInfo(
|
|||
private constructor(
|
||||
throwable: List<Throwable>,
|
||||
userAction: UserAction,
|
||||
serviceName: String,
|
||||
serviceId: Int?,
|
||||
request: String
|
||||
) : this(
|
||||
throwableListToStringList(throwable),
|
||||
userAction,
|
||||
serviceName,
|
||||
serviceId,
|
||||
request,
|
||||
getMessageStringId(throwable.firstOrNull(), userAction)
|
||||
getMessage(throwable.firstOrNull(), userAction, serviceId)
|
||||
) {
|
||||
this.throwable = throwable.firstOrNull()
|
||||
}
|
||||
|
||||
// constructor to manually build ErrorInfo
|
||||
constructor(stackTraces: Array<String>, userAction: UserAction, serviceId: Int?, request: String, @StringRes message: Int) :
|
||||
this(stackTraces, userAction, serviceId, request, ErrorMessage(message))
|
||||
|
||||
// constructors with single throwable
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String) :
|
||||
this(throwable, userAction, SERVICE_NONE, request)
|
||||
this(throwable, userAction, null, request)
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) :
|
||||
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
|
||||
this(throwable, userAction, serviceId, request)
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) :
|
||||
this(throwable, userAction, getInfoServiceName(info), request)
|
||||
this(throwable, userAction, info?.serviceId, request)
|
||||
|
||||
// constructors with list of throwables
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String) :
|
||||
this(throwable, userAction, SERVICE_NONE, request)
|
||||
this(throwable, userAction, null, request)
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, serviceId: Int) :
|
||||
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
|
||||
this(throwable, userAction, serviceId, request)
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?) :
|
||||
this(throwable, userAction, getInfoServiceName(info), request)
|
||||
this(throwable, userAction, info?.serviceId, request)
|
||||
|
||||
fun getServiceName(): String {
|
||||
return getServiceName(serviceId)
|
||||
}
|
||||
|
||||
fun getMessage(context: Context): String {
|
||||
return message.getString(context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SERVICE_NONE = "none"
|
||||
@Parcelize
|
||||
class ErrorMessage(
|
||||
@StringRes
|
||||
private val stringRes: Int,
|
||||
private vararg val formatArgs: String,
|
||||
) : Parcelable {
|
||||
fun getString(context: Context): String {
|
||||
return if (formatArgs.isEmpty()) {
|
||||
// use ContextCompat.getString() just in case context is not AppCompatActivity
|
||||
ContextCompat.getString(context, stringRes)
|
||||
} else {
|
||||
// ContextCompat.getString() with formatArgs does not exist, so we just
|
||||
// replicate its source code but with formatArgs
|
||||
ContextCompat.getContextForLanguage(context).getString(stringRes, *formatArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const val SERVICE_NONE = "<unknown_service>"
|
||||
|
||||
private fun getServiceName(serviceId: Int?) =
|
||||
// not using getNameOfServiceById since we want to accept a nullable serviceId and we
|
||||
// want to default to SERVICE_NONE
|
||||
ServiceList.all()?.firstOrNull { it.serviceId == serviceId }?.serviceInfo?.name
|
||||
?: SERVICE_NONE
|
||||
|
||||
fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
|
||||
|
||||
fun throwableListToStringList(throwableList: List<Throwable>) =
|
||||
throwableList.map { it.stackTraceToString() }.toTypedArray()
|
||||
|
||||
private fun getInfoServiceName(info: Info?) =
|
||||
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)
|
||||
|
||||
@StringRes
|
||||
fun getMessageStringId(
|
||||
fun getMessage(
|
||||
throwable: Throwable?,
|
||||
action: UserAction?
|
||||
): Int {
|
||||
action: UserAction?,
|
||||
serviceId: Int?,
|
||||
): ErrorMessage {
|
||||
return when {
|
||||
// player exceptions
|
||||
// some may be IOException, so do these checks before isNetworkRelated!
|
||||
throwable is ExoPlaybackException -> {
|
||||
val cause = throwable.cause
|
||||
when {
|
||||
cause is HttpDataSource.InvalidResponseCodeException && cause.responseCode == 403 -> R.string.player_error_403
|
||||
cause is Loader.UnexpectedLoaderException && cause.cause is ExtractionException -> getMessageStringId(throwable, action)
|
||||
throwable.type == ExoPlaybackException.TYPE_SOURCE -> R.string.player_stream_failure
|
||||
throwable.type == ExoPlaybackException.TYPE_UNEXPECTED -> R.string.player_recoverable_failure
|
||||
else -> R.string.player_unrecoverable_failure
|
||||
cause is HttpDataSource.InvalidResponseCodeException -> {
|
||||
if (cause.responseCode == 403) {
|
||||
if (serviceId == YouTube.serviceId) {
|
||||
ErrorMessage(R.string.youtube_player_http_403)
|
||||
} else {
|
||||
ErrorMessage(R.string.player_http_403)
|
||||
}
|
||||
} else {
|
||||
ErrorMessage(R.string.player_http_invalid_status, cause.responseCode.toString())
|
||||
}
|
||||
}
|
||||
cause is Loader.UnexpectedLoaderException && cause.cause is ExtractionException ->
|
||||
getMessage(throwable, action, serviceId)
|
||||
throwable.type == ExoPlaybackException.TYPE_SOURCE ->
|
||||
ErrorMessage(R.string.player_stream_failure)
|
||||
throwable.type == ExoPlaybackException.TYPE_UNEXPECTED ->
|
||||
ErrorMessage(R.string.player_recoverable_failure)
|
||||
else ->
|
||||
ErrorMessage(R.string.player_unrecoverable_failure)
|
||||
}
|
||||
}
|
||||
throwable is FailedMediaSource.FailedMediaSourceException -> getMessageStringId(throwable.cause, action)
|
||||
throwable is PlaybackResolver.ResolverException -> R.string.player_stream_failure
|
||||
throwable is FailedMediaSource.FailedMediaSourceException ->
|
||||
getMessage(throwable.cause, action, serviceId)
|
||||
throwable is PlaybackResolver.ResolverException ->
|
||||
ErrorMessage(R.string.player_stream_failure)
|
||||
|
||||
// content not available exceptions
|
||||
throwable is AccountTerminatedException -> R.string.account_terminated
|
||||
throwable is AgeRestrictedContentException -> R.string.restricted_video_no_stream
|
||||
throwable is GeographicRestrictionException -> R.string.georestricted_content
|
||||
throwable is PaidContentException -> R.string.paid_content
|
||||
throwable is PrivateContentException -> R.string.private_content
|
||||
throwable is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
|
||||
throwable is UnsupportedContentInCountryException -> R.string.unsupported_content_in_country
|
||||
throwable is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
|
||||
throwable is YoutubeSignInConfirmNotBotException -> R.string.youtube_sign_in_confirm_not_bot_error
|
||||
throwable is ContentNotAvailableException -> R.string.content_not_available
|
||||
throwable is AccountTerminatedException ->
|
||||
throwable.message
|
||||
?.takeIf { reason -> !reason.isEmpty() }
|
||||
?.let { reason ->
|
||||
ErrorMessage(
|
||||
R.string.account_terminated_service_provides_reason,
|
||||
getServiceName(serviceId),
|
||||
reason
|
||||
)
|
||||
}
|
||||
?: ErrorMessage(R.string.account_terminated)
|
||||
throwable is AgeRestrictedContentException ->
|
||||
ErrorMessage(R.string.restricted_video_no_stream)
|
||||
throwable is GeographicRestrictionException ->
|
||||
ErrorMessage(R.string.georestricted_content)
|
||||
throwable is PaidContentException ->
|
||||
ErrorMessage(R.string.paid_content)
|
||||
throwable is PrivateContentException ->
|
||||
ErrorMessage(R.string.private_content)
|
||||
throwable is SoundCloudGoPlusContentException ->
|
||||
ErrorMessage(R.string.soundcloud_go_plus_content)
|
||||
throwable is UnsupportedContentInCountryException ->
|
||||
ErrorMessage(R.string.unsupported_content_in_country)
|
||||
throwable is YoutubeMusicPremiumContentException ->
|
||||
ErrorMessage(R.string.youtube_music_premium_content)
|
||||
throwable is SignInConfirmNotBotException ->
|
||||
ErrorMessage(R.string.sign_in_confirm_not_bot_error, getServiceName(serviceId))
|
||||
throwable is ContentNotAvailableException ->
|
||||
ErrorMessage(R.string.content_not_available)
|
||||
|
||||
// other extractor exceptions
|
||||
throwable is ContentNotSupportedException -> R.string.content_not_supported
|
||||
throwable is ContentNotSupportedException ->
|
||||
ErrorMessage(R.string.content_not_supported)
|
||||
// ReCaptchas should have already been handled elsewhere,
|
||||
// but return an error message here just in case
|
||||
throwable is ReCaptchaException -> R.string.recaptcha_request_toast
|
||||
throwable is ReCaptchaException ->
|
||||
ErrorMessage(R.string.recaptcha_request_toast)
|
||||
// test this at the end as many exceptions could be a subclass of IOException
|
||||
throwable != null && throwable.isNetworkRelated -> R.string.network_error
|
||||
throwable != null && throwable.isNetworkRelated ->
|
||||
ErrorMessage(R.string.network_error)
|
||||
// an extraction exception unrelated to the network
|
||||
// is likely an issue with parsing the website
|
||||
throwable is ExtractionException -> R.string.parsing_error
|
||||
throwable is ExtractionException ->
|
||||
ErrorMessage(R.string.parsing_error)
|
||||
|
||||
// user actions (in case the exception is null or unrecognizable)
|
||||
action == UserAction.UI_ERROR -> R.string.app_ui_crash
|
||||
action == UserAction.REQUESTED_COMMENTS -> R.string.error_unable_to_load_comments
|
||||
action == UserAction.SUBSCRIPTION_CHANGE -> R.string.subscription_change_failed
|
||||
action == UserAction.SUBSCRIPTION_UPDATE -> R.string.subscription_update_failed
|
||||
action == UserAction.LOAD_IMAGE -> R.string.could_not_load_thumbnails
|
||||
action == UserAction.DOWNLOAD_OPEN_DIALOG -> R.string.could_not_setup_download_menu
|
||||
else -> R.string.error_snackbar_message
|
||||
action == UserAction.UI_ERROR ->
|
||||
ErrorMessage(R.string.app_ui_crash)
|
||||
action == UserAction.REQUESTED_COMMENTS ->
|
||||
ErrorMessage(R.string.error_unable_to_load_comments)
|
||||
action == UserAction.SUBSCRIPTION_CHANGE ->
|
||||
ErrorMessage(R.string.subscription_change_failed)
|
||||
action == UserAction.SUBSCRIPTION_UPDATE ->
|
||||
ErrorMessage(R.string.subscription_update_failed)
|
||||
action == UserAction.LOAD_IMAGE ->
|
||||
ErrorMessage(R.string.could_not_load_thumbnails)
|
||||
action == UserAction.DOWNLOAD_OPEN_DIALOG ->
|
||||
ErrorMessage(R.string.could_not_setup_download_menu)
|
||||
else ->
|
||||
ErrorMessage(R.string.error_snackbar_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,14 +14,11 @@ 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.exceptions.AccountTerminatedException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.ktx.isInterruptedCaused
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
|
@ -99,20 +96,6 @@ class ErrorPanelHelper(
|
|||
|
||||
errorRetryButton.isVisible = retryShouldBeShown
|
||||
showAndSetOpenInBrowserButtonAction(errorInfo)
|
||||
} else if (errorInfo.throwable is AccountTerminatedException) {
|
||||
errorTextView.setText(R.string.account_terminated)
|
||||
|
||||
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
|
||||
errorServiceInfoTextView.text = context.resources.getString(
|
||||
R.string.service_provides_reason,
|
||||
ServiceHelper.getSelectedService(context)?.serviceInfo?.name ?: "<unknown>"
|
||||
)
|
||||
errorServiceInfoTextView.isVisible = true
|
||||
|
||||
errorServiceExplanationTextView.text =
|
||||
(errorInfo.throwable as AccountTerminatedException).message
|
||||
errorServiceExplanationTextView.isVisible = true
|
||||
}
|
||||
} else {
|
||||
showAndSetErrorButtonAction(
|
||||
R.string.error_snackbar_action
|
||||
|
|
@ -120,7 +103,7 @@ class ErrorPanelHelper(
|
|||
ErrorUtil.openActivity(context, errorInfo)
|
||||
}
|
||||
|
||||
errorTextView.setText(errorInfo.messageStringId)
|
||||
errorTextView.text = errorInfo.getMessage(context)
|
||||
|
||||
if (errorInfo.throwable !is ContentNotAvailableException &&
|
||||
errorInfo.throwable !is ContentNotSupportedException
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ class ErrorUtil {
|
|||
)
|
||||
.setSmallIcon(R.drawable.ic_bug_report)
|
||||
.setContentTitle(context.getString(R.string.error_report_notification_title))
|
||||
.setContentText(context.getString(errorInfo.messageStringId))
|
||||
.setContentText(errorInfo.getMessage(context))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(
|
||||
PendingIntentCompat.getActivity(
|
||||
|
|
@ -156,7 +156,7 @@ class ErrorUtil {
|
|||
// fallback to showing a notification if no root view is available
|
||||
createNotification(context, errorInfo)
|
||||
} else {
|
||||
Snackbar.make(rootView, errorInfo.messageStringId, Snackbar.LENGTH_LONG)
|
||||
Snackbar.make(rootView, errorInfo.getMessage(context), Snackbar.LENGTH_LONG)
|
||||
.setActionTextColor(Color.YELLOW)
|
||||
.setAction(context.getString(R.string.error_snackbar_action).uppercase()) {
|
||||
openActivity(context, errorInfo)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ public enum UserAction {
|
|||
CHECK_FOR_NEW_APP_VERSION("check for new app version"),
|
||||
OPEN_INFO_ITEM_DIALOG("open info item dialog"),
|
||||
GETTING_MAIN_SCREEN_TAB("getting main screen tab"),
|
||||
PLAY_ON_POPUP("play on popup");
|
||||
PLAY_ON_POPUP("play on popup"),
|
||||
SUBSCRIPTIONS("loading subscriptions"),;
|
||||
|
||||
private final String message;
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
|||
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
||||
ErrorUtil.showSnackbar(activity,
|
||||
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
|
||||
ServiceHelper.getNameOfServiceById(currentServiceId),
|
||||
currentServiceId,
|
||||
"Service does not support importing subscriptions",
|
||||
R.string.general_error));
|
||||
activity.finish();
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ class MediaBrowserPlaybackPreparer(
|
|||
|
||||
private fun onPrepareError(throwable: Throwable) {
|
||||
setMediaSessionError.accept(
|
||||
ContextCompat.getString(context, ErrorInfo.getMessageStringId(throwable, null)),
|
||||
ErrorInfo.getMessage(throwable, null, null).getString(context),
|
||||
PlaybackStateCompat.ERROR_CODE_APP_ERROR
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -563,11 +563,11 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
}
|
||||
request.append("]");
|
||||
|
||||
String service;
|
||||
Integer service;
|
||||
try {
|
||||
service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName();
|
||||
service = NewPipe.getServiceByUrl(mission.source).getServiceId();
|
||||
} catch (Exception e) {
|
||||
service = ErrorInfo.SERVICE_NONE;
|
||||
service = null;
|
||||
}
|
||||
|
||||
ErrorUtil.createNotification(mContext,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue