Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show improved error panel instead of annoying snackbar or crashing #5148

Merged
merged 11 commits into from
Mar 14, 2021
Prev Previous commit
Next Next commit
Add report/solve-recaptcha button in error panel
It will be shown even when nothing could be loaded not due to a network error, and the user can choose to ignore or report it.

Also improve error reporting arguments
Also completely refactor error activity
Also improve some code here and there
Stypox committed Mar 12, 2021

Verified

This commit was signed with the committer’s verified signature.
weblate Weblate (bot)
commit c43bca6007584c2cca28ddf984ad090baf370c68
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
@@ -155,7 +155,7 @@ task formatKtlint(type: JavaExec) {
}

afterEvaluate {
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
//preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
}

dependencies {
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ public void errorInfoTestParcelable() {
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
assertEquals("youtube", infoFromParcel.getServiceName());
assertEquals("request", infoFromParcel.getRequest());
assertEquals(R.string.general_error, infoFromParcel.getMessage());
assertEquals(R.string.general_error, infoFromParcel.getMessageStringId());

parcel.recycle();
}
47 changes: 0 additions & 47 deletions app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java

This file was deleted.

12 changes: 4 additions & 8 deletions app/src/main/java/org/schabi/newpipe/App.java
Original file line number Diff line number Diff line change
@@ -225,14 +225,10 @@ protected void initACRA() {
.setBuildConfigClass(BuildConfig.class)
.build();
ACRA.init(this, acraConfig);
} catch (final ACRAConfigurationException ace) {
ace.printStackTrace();
ErrorActivity.reportError(this,
ace,
null,
null,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not initialize ACRA crash report", R.string.app_ui_crash));
} catch (final ACRAConfigurationException exception) {
exception.printStackTrace();
ErrorActivity.reportError(this, null, null, new ErrorInfo(exception,
UserAction.SOMETHING_ELSE, "Could not initialize ACRA crash report"));
}
}

15 changes: 6 additions & 9 deletions app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
Original file line number Diff line number Diff line change
@@ -63,9 +63,8 @@ private static String getCertificateSHA1Fingerprint(@NonNull final Application a
packageInfo = application.getPackageManager().getPackageInfo(
application.getPackageName(), PackageManager.GET_SIGNATURES);
} catch (final PackageManager.NameNotFoundException e) {
ErrorActivity.reportError(application, e, null, null,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not find package info", R.string.app_ui_crash));
ErrorActivity.reportError(application, null, null, new ErrorInfo(e,
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
return "";
}

@@ -77,9 +76,8 @@ private static String getCertificateSHA1Fingerprint(@NonNull final Application a
final CertificateFactory cf = CertificateFactory.getInstance("X509");
c = (X509Certificate) cf.generateCertificate(input);
} catch (final CertificateException e) {
ErrorActivity.reportError(application, e, null, null,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Certificate error", R.string.app_ui_crash));
ErrorActivity.reportError(application, null, null, new ErrorInfo(e,
UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error"));
return "";
}

@@ -88,9 +86,8 @@ private static String getCertificateSHA1Fingerprint(@NonNull final Application a
final byte[] publicKey = md.digest(c.getEncoded());
return byte2HexFormatted(publicKey);
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
ErrorActivity.reportError(application, e, null, null,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not retrieve SHA1 key", R.string.app_ui_crash));
ErrorActivity.reportError(application, null, null, new ErrorInfo(e,
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key"));
return "";
}
}
10 changes: 5 additions & 5 deletions app/src/main/java/org/schabi/newpipe/MainActivity.java
Original file line number Diff line number Diff line change
@@ -153,7 +153,7 @@ protected void onCreate(final Bundle savedInstanceState) {
try {
setupDrawer();
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiError(this, null, "Setting up drawer", e);
}

if (DeviceUtils.isTv(this)) {
@@ -238,7 +238,7 @@ private boolean drawerItemSelected(final MenuItem item) {
try {
tabSelected(item);
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiError(this, null, "Selecting main page tab", e);
}
break;
case R.id.menu_options_about_group:
@@ -340,7 +340,7 @@ private void toggleServices() {
try {
showTabs();
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiError(this, null, "Showing main page tabs", e);
}
}
}
@@ -487,7 +487,7 @@ protected void onResume() {
drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
getString(R.string.drawer_header_description) + selectedServiceName);
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiError(this, null, "Setting up service toggle", e);
}

final SharedPreferences sharedPreferences
@@ -799,7 +799,7 @@ private void handleIntent(final Intent intent) {
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
}
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiError(this, null, "Handling intent", e);
}
}

94 changes: 72 additions & 22 deletions app/src/main/java/org/schabi/newpipe/RouterActivity.java
Original file line number Diff line number Diff line change
@@ -33,23 +33,36 @@
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.StreamingService.LinkType;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
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.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -84,13 +97,6 @@
* Get the url from the intent and open it in the chosen preferred player.
*/
public class RouterActivity extends AppCompatActivity {
public static final String INTERNAL_ROUTE_KEY = "internalRoute";
/**
* Removes invisible separators (\p{Z}) and punctuation characters including
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
* more details.
*/
private static final String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
protected final CompositeDisposable disposables = new CompositeDisposable();
@State
protected int currentServiceId = -1;
@@ -100,7 +106,6 @@ public class RouterActivity extends AppCompatActivity {
protected int selectedRadioPosition = -1;
protected int selectedPreviously = -1;
protected String currentUrl;
protected boolean internalRoute = false;
private StreamingService currentService;
private boolean selectionIsDownload = false;

@@ -123,7 +128,7 @@ protected void onCreate(final Bundle savedInstanceState) {
}

@Override
protected void onSaveInstanceState(final Bundle outState) {
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
@@ -164,18 +169,61 @@ private void handleUrl(final String url) {
} else {
showUnsupportedUrlDialog(url);
}
}, throwable -> handleError(throwable, url)));
}, throwable -> handleError(this,
new ErrorInfo(throwable, UserAction.SHARE_TO_NEWPIPE, url))));
}

private void handleError(final Throwable throwable, final String url) {
throwable.printStackTrace();
/**
* @param context the context. If instance of {@link RouterActivity} it will be finished at the
* end, and if needed {@link #showUnsupportedUrlDialog(String)} will be called
* on it.
* @param errorInfo The error information. The field {@link ErrorInfo#getRequest()} has to
* contain the url, if context is instance of {@link RouterActivity}, since it
* could be used to call {@link #showUnsupportedUrlDialog(String)}.
*/
private static void handleError(final Context context, final ErrorInfo errorInfo) {
if (errorInfo.getThrowable() != null) {
errorInfo.getThrowable().printStackTrace();
}

if (throwable instanceof ExtractionException) {
showUnsupportedUrlDialog(url);
if (errorInfo.getThrowable() instanceof ReCaptchaException) {
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
final Intent intent = new Intent(context, ReCaptchaActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
} else if (errorInfo.getThrowable() != null
&& ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) {
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) {
Toast.makeText(context, R.string.restricted_video_no_stream,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) {
Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof PaidContentException) {
Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof PrivateContentException) {
Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) {
Toast.makeText(context, R.string.soundcloud_go_plus_content,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) {
Toast.makeText(context, R.string.youtube_music_premium_content,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) {
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof ExtractionException
&& context instanceof RouterActivity) {
// unfortunately we cannot tell if the error is really caused by an unsupported url
((RouterActivity) context).showUnsupportedUrlDialog(errorInfo.getRequest());
} else {
ExtractorHelper.handleGeneralException(this, -1, url, throwable,
UserAction.SOMETHING_ELSE, null);
finish();
ErrorActivity.reportError(context, MainActivity.class, null, errorInfo);
}

if (context instanceof RouterActivity) {
((RouterActivity) context).finish();
}
}

@@ -500,7 +548,8 @@ private void handleChoice(final String selectedChoiceKey) {
.subscribe(intent -> {
startActivity(intent);
finish();
}, throwable -> handleError(throwable, currentUrl))
}, throwable -> handleError(this,
new ErrorInfo(throwable, UserAction.SHARE_TO_NEWPIPE, currentUrl)))
);
return;
}
@@ -580,6 +629,7 @@ private static class Choice implements Serializable {
this.playerChoice = playerChoice;
}

@NonNull
@Override
public String toString() {
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
@@ -646,9 +696,9 @@ public void handleChoice(final Choice choice) {
if (fetcher != null) {
fetcher.dispose();
}
}, throwable -> ExtractorHelper.handleGeneralException(this,
choice.serviceId, choice.url, throwable, finalUserAction,
", opened with " + choice.playerChoice));
}, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
choice.url + " opened with " + choice.playerChoice,
choice.serviceId)));
}
}

14 changes: 2 additions & 12 deletions app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
Original file line number Diff line number Diff line change
@@ -591,17 +591,6 @@ private void showFailedDialog(@StringRes final int msg) {
.show();
}

private void showErrorActivity(final Exception e) {
ErrorActivity.reportError(
context,
Collections.singletonList(e),
null,
null,
ErrorInfo
.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
);
}

private void prepareSelectedDownload() {
final StoredDirectoryHelper mainStorage;
final MediaFormat format;
@@ -705,7 +694,8 @@ private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
mainStorage.getTag());
}
} catch (final Exception e) {
showErrorActivity(e);
ErrorActivity.reportError(context, null, null,
new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"));
return;
}

11 changes: 8 additions & 3 deletions app/src/main/java/org/schabi/newpipe/error/AcraReportSender.java
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

import androidx.annotation.NonNull;

import org.acra.ReportField;
import org.acra.data.CrashReportData;
import org.acra.sender.ReportSender;
import org.schabi.newpipe.R;
@@ -32,8 +33,12 @@ public class AcraReportSender implements ReportSender {

@Override
public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
ErrorActivity.reportError(context, report,
ErrorInfo.make(UserAction.UI_ERROR, "none",
"App crash, UI failure", R.string.app_ui_crash));
ErrorActivity.reportError(context, null, null, new ErrorInfo(
new String[]{report.getString(ReportField.STACK_TRACE)},
UserAction.UI_ERROR,
ErrorInfo.SERVICE_NONE,
"ACRA report",
R.string.app_ui_crash,
null));
}
}
177 changes: 59 additions & 118 deletions app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java
Original file line number Diff line number Diff line change
@@ -23,9 +23,6 @@
import com.google.android.material.snackbar.Snackbar;
import com.grack.nanojson.JsonWriter;

import org.acra.ReportField;
import org.acra.data.CrashReportData;
import org.schabi.newpipe.ActivityCommunicator;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
@@ -34,14 +31,9 @@
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.Vector;

import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;

@@ -70,7 +62,6 @@ public class ErrorActivity extends AppCompatActivity {
public static final String TAG = ErrorActivity.class.toString();
// BUNDLE TAGS
public static final String ERROR_INFO = "error_info";
public static final String ERROR_LIST = "error_list";

public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT
@@ -79,100 +70,68 @@ public class ErrorActivity extends AppCompatActivity {
public static final String ERROR_GITHUB_ISSUE_URL
= "https://github.com/TeamNewPipe/NewPipe/issues";

private String[] errorList;
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");


/**
* Singleton:
* Used to send data between certain Activity/Services within the same process.
* This can be considered as an ugly hack inside the Android universe.
**/
@Nullable private static Class savedReturnActivity = null;

private ErrorInfo errorInfo;
private Class returnActivity;
private String currentTimeStamp;

private ActivityErrorBinding activityErrorBinding;

public static void reportUiError(final AppCompatActivity activity, final Throwable el) {
reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UserAction.UI_ERROR,
"none", "", R.string.app_ui_crash));
public static void reportUiError(final Context context,
@Nullable final View rootView,
final String request,
final Throwable throwable) {
reportError(context, (context instanceof Activity ? context.getClass() : null), rootView,
new ErrorInfo(throwable, UserAction.UI_ERROR, request));
}

public static void reportError(final Context context, final List<Throwable> el,
final Class returnActivity, final View rootView,
public static void reportError(final Context context,
final Class returnActivity,
@Nullable final View rootView,
final ErrorInfo errorInfo) {
if (rootView != null) {
Snackbar.make(rootView, R.string.error_snackbar_message, 3 * 1000)
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
.setActionTextColor(Color.YELLOW)
.setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v ->
startErrorActivity(returnActivity, context, errorInfo, el)).show();
startErrorActivity(returnActivity, context, errorInfo)).show();
} else {
startErrorActivity(returnActivity, context, errorInfo, el);
}
}

private static void startErrorActivity(final Class returnActivity, final Context context,
final ErrorInfo errorInfo, final List<Throwable> el) {
final ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
ac.setReturnActivity(returnActivity);
final Intent intent = new Intent(context, ErrorActivity.class);
intent.putExtra(ERROR_INFO, errorInfo);
intent.putExtra(ERROR_LIST, elToSl(el));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}

public static void reportError(final Context context, final Throwable e,
final Class returnActivity, final View rootView,
final ErrorInfo errorInfo) {
List<Throwable> el = null;
if (e != null) {
el = new Vector<>();
el.add(e);
startErrorActivity(returnActivity, context, errorInfo);
}
reportError(context, el, returnActivity, rootView, errorInfo);
}

// async call
public static void reportError(final Handler handler, final Context context,
final Throwable e, final Class returnActivity,
final View rootView, final ErrorInfo errorInfo) {

List<Throwable> el = null;
if (e != null) {
el = new Vector<>();
el.add(e);
}
reportError(handler, context, el, returnActivity, rootView, errorInfo);
public static void reportError(final Handler handler,
final Context context,
final Class returnActivity,
final View rootView,
final ErrorInfo errorInfo) {
handler.post(() -> reportError(context, returnActivity, rootView, errorInfo));
}

// async call
public static void reportError(final Handler handler, final Context context,
final List<Throwable> el, final Class returnActivity,
final View rootView, final ErrorInfo errorInfo) {
handler.post(() -> reportError(context, el, returnActivity, rootView, errorInfo));
}

public static void reportError(final Context context, final CrashReportData report,
final ErrorInfo errorInfo) {
final String[] el = {report.getString(ReportField.STACK_TRACE)};
////////////////////////////////////////////////////////////////////////
// UTILS
////////////////////////////////////////////////////////////////////////

private static void startErrorActivity(@Nullable final Class returnActivity,
final Context context,
final ErrorInfo errorInfo) {
savedReturnActivity = returnActivity;
final Intent intent = new Intent(context, ErrorActivity.class);
intent.putExtra(ERROR_INFO, errorInfo);
intent.putExtra(ERROR_LIST, el);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}

private static String getStackTrace(final Throwable throwable) {
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw, true);
throwable.printStackTrace(pw);
return sw.getBuffer().toString();
}

// errorList to StringList
private static String[] elToSl(final List<Throwable> stackTraces) {
final String[] out = new String[stackTraces.size()];
for (int i = 0; i < stackTraces.size(); i++) {
out[i] = getStackTrace(stackTraces.get(i));
}
return out;
}

@Override
protected void onCreate(final Bundle savedInstanceState) {
assureCorrectAppLanguage(this);
@@ -193,38 +152,28 @@ protected void onCreate(final Bundle savedInstanceState) {
actionBar.setDisplayShowTitleEnabled(true);
}

final ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
returnActivity = ac.getReturnActivity();
errorInfo = intent.getParcelableExtra(ERROR_INFO);
errorList = intent.getStringArrayExtra(ERROR_LIST);

// important add guru meditation
addGuruMeditation();
currentTimeStamp = getCurrentTimeStamp();
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());

activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "EMAIL"));

activityErrorBinding.errorReportCopyButton.setOnClickListener(v -> {
ShareUtils.copyToClipboard(this, buildMarkdown());
});
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
ShareUtils.copyToClipboard(this, buildMarkdown()));

activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "GITHUB"));

// normal bugreport
buildInfo(errorInfo);
if (errorInfo.getMessage() != 0) {
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage());
} else {
activityErrorBinding.errorMessageView.setVisibility(View.GONE);
activityErrorBinding.messageWhatHappenedView.setVisibility(View.GONE);
}

activityErrorBinding.errorView.setText(formErrorText(errorList));
activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId());
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));

// print stack trace once again for debugging:
for (final String e : errorList) {
for (final String e : errorInfo.getStackTraces()) {
Log.e(TAG, e);
}
}
@@ -239,15 +188,14 @@ public boolean onCreateOptionsMenu(final Menu menu) {
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId();
switch (id) {
case android.R.id.home:
goToReturnActivity();
break;
case R.id.menu_item_share_error:
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
break;
if (id == android.R.id.home) {
goToReturnActivity();
} else if (id == R.id.menu_item_share_error) {
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
} else {
return false;
}
return false;
return true;
}

private void openPrivacyPolicyDialog(final Context context, final String action) {
@@ -311,7 +259,7 @@ static Class<? extends Activity> getReturnActivity(final Class<?> returnActivity
}

private void goToReturnActivity() {
final Class<? extends Activity> checkedReturnActivity = getReturnActivity(returnActivity);
final Class<? extends Activity> checkedReturnActivity = getReturnActivity(savedReturnActivity);
if (checkedReturnActivity == null) {
super.onBackPressed();
} else {
@@ -355,7 +303,7 @@ private String buildJson() {
.value("version", BuildConfig.VERSION_NAME)
.value("os", getOsString())
.value("time", currentTimeStamp)
.array("exceptions", Arrays.asList(errorList))
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
.toString())
.end()
@@ -393,27 +341,27 @@ private String buildMarkdown() {

// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorList.length > 1) {
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport
.append("<details><summary><b>Exceptions (")
.append(errorList.length)
.append(errorInfo.getStackTraces().length)
.append(")</b></summary><p>\n");
}

// add the logs
for (int i = 0; i < errorList.length; i++) {
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
htmlErrorReport.append("<details><summary><b>Crash log ");
if (errorList.length > 1) {
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append(i + 1);
}
htmlErrorReport.append("</b>")
.append("</summary><p>\n")
.append("\n```\n").append(errorList[i]).append("\n```\n")
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
.append("</details>\n");
}

// make sure to close everything
if (errorList.length > 1) {
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append("</p></details>\n");
}
htmlErrorReport.append("<hr>\n");
@@ -466,11 +414,4 @@ public void onBackPressed() {
//super.onBackPressed();
goToReturnActivity();
}

public String getCurrentTimeStamp() {
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df.format(new Date());
}

}
110 changes: 99 additions & 11 deletions app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
Original file line number Diff line number Diff line change
@@ -3,21 +3,109 @@ package org.schabi.newpipe.error
import android.os.Parcelable
import androidx.annotation.StringRes
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.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
import org.schabi.newpipe.extractor.exceptions.ExtractionException
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
import org.schabi.newpipe.ktx.isNetworkRelated
import java.io.PrintWriter
import java.io.StringWriter

@Parcelize
class ErrorInfo(
val userAction: UserAction?,
val serviceName: String,
val request: String,
@field:StringRes @param:StringRes val message: Int
val stackTraces: Array<String>,
val userAction: UserAction,
val serviceName: String,
val request: String,
val messageStringId: Int,
@Transient // no need to store throwable, all data for report is in other variables
var throwable: Throwable? = null
) : Parcelable {
companion object {
@JvmStatic
fun make(
userAction: UserAction?,

private constructor(
throwable: Throwable,
userAction: UserAction,
serviceName: String,
request: String
) : this(
throwableToStringList(throwable),
userAction,
serviceName,
request,
getMessageStringId(throwable, userAction),
throwable
)

private constructor(
throwable: List<Throwable>,
userAction: UserAction,
serviceName: String,
request: String,
@StringRes message: Int
) = ErrorInfo(userAction, serviceName, request, message)
request: String
) : this(
throwableListToStringList(throwable),
userAction,
serviceName,
request,
getMessageStringId(throwable.firstOrNull(), userAction),
throwable.firstOrNull()
)

// constructors with single throwable
constructor(throwable: Throwable, userAction: UserAction, request: String)
: this(throwable, userAction, SERVICE_NONE, request)
constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int)
: this(throwable, userAction, NewPipe.getNameOfService(serviceId), request)
constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?)
: this(throwable, userAction, getInfoServiceName(info), request)

// constructors with list of throwables
constructor(throwable: List<Throwable>, userAction: UserAction, request: String)
: this(throwable, userAction, SERVICE_NONE, request)
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, serviceId: Int)
: this(throwable, userAction, NewPipe.getNameOfService(serviceId), request)
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?)
: this(throwable, userAction, getInfoServiceName(info), request)

companion object {
const val SERVICE_NONE = "none"

private fun getStackTrace(throwable: Throwable): String {
StringWriter().use { stringWriter ->
PrintWriter(stringWriter, true).use { printWriter ->
throwable.printStackTrace(printWriter)
return stringWriter.buffer.toString()
}
}
}

fun throwableToStringList(throwable: Throwable) = arrayOf(getStackTrace(throwable))

fun throwableListToStringList(throwable: List<Throwable>) =
Array(throwable.size) { i -> getStackTrace(throwable[i]) }

private fun getInfoServiceName(info: Info?) =
if (info == null) SERVICE_NONE else NewPipe.getNameOfService(info.serviceId)

@StringRes
private fun getMessageStringId(throwable: Throwable?,
action: UserAction): Int {
return when {
throwable is ContentNotAvailableException -> R.string.content_not_available
throwable != null && throwable.isNetworkRelated -> R.string.network_error
throwable is ContentNotSupportedException -> R.string.content_not_supported
throwable is DeobfuscateException -> R.string.youtube_signature_deobfuscation_error
throwable is ExtractionException -> R.string.parsing_error
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.general_error
}
}
}
}
136 changes: 136 additions & 0 deletions app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.schabi.newpipe.error

import android.content.Context
import android.content.Intent
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.jakewharton.rxbinding4.view.clicks
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.AgeRestrictedContentException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
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.SoundCloudGoPlusContentException
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.isInterruptedCaused
import org.schabi.newpipe.ktx.isNetworkRelated
import java.util.concurrent.TimeUnit

class ErrorPanelHelper(
private val fragment: Fragment,
rootView: View,
onRetry: Runnable
) {
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 errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action)
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry)

private var errorDisposable: Disposable? = null

init {
errorDisposable = errorButtonRetry.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onRetry.run() }
}

fun showError(errorInfo: ErrorInfo) {

if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
if (DEBUG) {
Log.w(TAG, "onError() isInterruptedCaused! = [$errorInfo.throwable]")
}
return
}

errorButtonAction.isVisible = true
if (errorInfo.throwable is ReCaptchaException) {
errorButtonAction.setText(R.string.recaptcha_solve)
errorButtonAction.setOnClickListener {
// Starting ReCaptcha Challenge Activity
val intent = Intent(context, ReCaptchaActivity::class.java)
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA,
(errorInfo.throwable as ReCaptchaException).url)
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
errorButtonAction.setOnClickListener(null)
}
errorTextView.setText(R.string.recaptcha_request_toast)
errorButtonRetry.isVisible = true

} else {
errorButtonAction.setText(R.string.error_snackbar_action)
errorButtonAction.setOnClickListener {
ErrorActivity.reportError(
context,
MainActivity::class.java,
null,
errorInfo
)
}

// hide retry button by default, then show only if not unavailable/unsupported content
errorButtonRetry.isVisible = false
errorTextView.setText(
when (errorInfo.throwable) {
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
is GeographicRestrictionException -> R.string.georestricted_content
is PaidContentException -> R.string.paid_content
is PrivateContentException -> R.string.private_content
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
is ContentNotAvailableException -> R.string.content_not_available
is ContentNotSupportedException -> R.string.content_not_supported
else -> {
// show retry button only for content which is not unavailable or unsupported
errorButtonRetry.isVisible = true
if (errorInfo.throwable != null && errorInfo.throwable!!.isNetworkRelated) {
R.string.network_error
} else {
R.string.error_snackbar_message
}
}
}
)
}
errorPanelRoot.animate(true, 300)
}

fun showTextError(errorString: String) {
errorButtonAction.isVisible = false
errorButtonRetry.isVisible = false
errorTextView.text = errorString
}

fun hide() {
errorButtonAction.setOnClickListener(null)
errorPanelRoot.animate(false, 150)
}

fun isVisible(): Boolean {
return errorPanelRoot.isVisible
}

fun dispose() {
errorButtonAction.setOnClickListener(null)
errorButtonRetry.setOnClickListener(null)
errorDisposable?.dispose()
}

companion object {
val TAG: String = ErrorPanelHelper::class.simpleName!!
val DEBUG: Boolean = MainActivity.DEBUG
}
}
15 changes: 11 additions & 4 deletions app/src/main/java/org/schabi/newpipe/error/UserAction.java
Original file line number Diff line number Diff line change
@@ -6,9 +6,12 @@
public enum UserAction {
USER_REPORT("user report"),
UI_ERROR("ui error"),
SUBSCRIPTION("subscription"),
SUBSCRIPTION_CHANGE("subscription change"),
SUBSCRIPTION_UPDATE("subscription update"),
SUBSCRIPTION_GET("get subscription"),
SUBSCRIPTION_IMPORT_EXPORT("subscription import or export"),
LOAD_IMAGE("load image"),
SOMETHING_ELSE("something"),
SOMETHING_ELSE("something else"),
SEARCHED("searched"),
GET_SUGGESTIONS("get suggestions"),
REQUESTED_STREAM("requested stream"),
@@ -17,11 +20,15 @@ public enum UserAction {
REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"),
REQUESTED_FEED("requested feed"),
REQUESTED_BOOKMARK("bookmark"),
DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("Play stream"),
PLAY_STREAM("play stream"),
DOWNLOAD_OPEN_DIALOG("download open dialog"),
DOWNLOAD_POSTPROCESSING("download post-processing"),
DOWNLOAD_FAILED("download failed"),
PREFERENCES_MIGRATION("migration of preferences");
PREFERENCES_MIGRATION("migration of preferences"),
SHARE_TO_NEWPIPE("share to newpipe"),
CHECK_FOR_NEW_APP_VERSION("check for new app version");


private final String message;
203 changes: 36 additions & 167 deletions app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java
Original file line number Diff line number Diff line change
@@ -1,48 +1,25 @@
package org.schabi.newpipe.fragments;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;

import com.jakewharton.rxbinding4.view.RxView;

import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
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.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.error.ErrorPanelHelper;
import org.schabi.newpipe.util.InfoCache;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;

import static org.schabi.newpipe.ktx.ViewUtils.animate;

@@ -55,12 +32,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
private View emptyStateView;
@Nullable
private ProgressBar loadingProgressBar;

private Disposable errorDisposable;

protected View errorPanelRoot;
private Button errorButtonRetry;
private TextView errorTextView;
private ErrorPanelHelper errorPanelHelper;

@Override
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
@@ -77,9 +49,7 @@ public void onPause() {
@Override
public void onDestroy() {
super.onDestroy();
if (errorDisposable != null) {
errorDisposable.dispose();
}
errorPanelHelper.dispose();
}

/*//////////////////////////////////////////////////////////////////////////
@@ -89,22 +59,9 @@ public void onDestroy() {
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);

emptyStateView = rootView.findViewById(R.id.empty_state_view);
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);

errorPanelRoot = rootView.findViewById(R.id.error_panel);
errorButtonRetry = rootView.findViewById(R.id.error_button_retry);
errorTextView = rootView.findViewById(R.id.error_message_view);
}

@Override
protected void initListeners() {
super.initListeners();
errorDisposable = RxView.clicks(errorButtonRetry)
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(o -> onRetryButtonClicked());
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
}

protected void onRetryButtonClicked() {
@@ -143,7 +100,7 @@ public void showLoading() {
if (loadingProgressBar != null) {
animate(loadingProgressBar, true, 400);
}
animate(errorPanelRoot, false, 150);
errorPanelHelper.hide();
}

@Override
@@ -154,10 +111,9 @@ public void hideLoading() {
if (loadingProgressBar != null) {
animate(loadingProgressBar, false, 0);
}
animate(errorPanelRoot, false, 150);
errorPanelHelper.hide();
}

@Override
public void showEmptyState() {
isLoading.set(false);
if (emptyStateView != null) {
@@ -166,168 +122,81 @@ public void showEmptyState() {
if (loadingProgressBar != null) {
animate(loadingProgressBar, false, 0);
}
animate(errorPanelRoot, false, 150);
errorPanelHelper.hide();
}

@Override
public void showError(final String message, final boolean showRetryButton) {
public void handleResult(final I result) {
if (DEBUG) {
Log.d(TAG, "showError() called with: "
+ "message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
Log.d(TAG, "handleResult() called with: result = [" + result + "]");
}
isLoading.set(false);
InfoCache.getInstance().clearCache();
hideLoading();

errorTextView.setText(message);
if (showRetryButton) {
animate(errorButtonRetry, true, 600);
} else {
animate(errorButtonRetry, false, 0);
}
animate(errorPanelRoot, true, 300);
}

@Override
public void handleResult(final I result) {
if (DEBUG) {
Log.d(TAG, "handleResult() called with: result = [" + result + "]");
}
public void handleError() {
isLoading.set(false);
InfoCache.getInstance().clearCache();
hideLoading();
}

/*//////////////////////////////////////////////////////////////////////////
// Error handling
//////////////////////////////////////////////////////////////////////////*/

/**
* Default implementation handles some general exceptions.
*
* @param exception The exception that should be handled
* @return If the exception was handled
*/
protected boolean onError(final Throwable exception) {
if (DEBUG) {
Log.d(TAG, "onError() called with: exception = [" + exception + "]");
}
isLoading.set(false);
public final void showError(final ErrorInfo errorInfo) {
handleError();

if (isDetached() || isRemoving()) {
if (DEBUG) {
Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
}
return true;
}

if (ExceptionUtils.isInterruptedCaused(exception)) {
if (DEBUG) {
Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
Log.w(TAG, "showError() is detached or removing = [" + errorInfo + "]");
}
return true;
}

if (exception instanceof ReCaptchaException) {
onReCaptchaException((ReCaptchaException) exception);
return true;
} else if (ExceptionUtils.isNetworkRelated(exception)) {
showError(getString(R.string.network_error), true);
return true;
} else if (exception instanceof AgeRestrictedContentException) {
showError(getString(R.string.restricted_video_no_stream), false);
return true;
} else if (exception instanceof GeographicRestrictionException) {
showError(getString(R.string.georestricted_content), false);
return true;
} else if (exception instanceof PaidContentException) {
showError(getString(R.string.paid_content), false);
return true;
} else if (exception instanceof PrivateContentException) {
showError(getString(R.string.private_content), false);
return true;
} else if (exception instanceof SoundCloudGoPlusContentException) {
showError(getString(R.string.soundcloud_go_plus_content), false);
return true;
} else if (exception instanceof YoutubeMusicPremiumContentException) {
showError(getString(R.string.youtube_music_premium_content), false);
return true;
} else if (exception instanceof ContentNotAvailableException) {
showError(getString(R.string.content_not_available), false);
return true;
} else if (exception instanceof ContentNotSupportedException) {
showError(getString(R.string.content_not_supported), false);
return true;
return;
}

return false;
errorPanelHelper.showError(errorInfo);
}

public void onReCaptchaException(final ReCaptchaException exception) {
if (DEBUG) {
Log.d(TAG, "onReCaptchaException() called");
}
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
final Intent intent = new Intent(activity, ReCaptchaActivity.class);
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, exception.getUrl());
startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST);
public final void showTextError(final @NonNull String errorString) {
handleError();

showError(getString(R.string.recaptcha_request_toast), false);
}
if (isDetached() || isRemoving()) {
if (DEBUG) {
Log.w(TAG, "showTextError() is detached or removing = [" + errorString + "]");
}
return;
}

public void onUnrecoverableError(final Throwable exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName,
request, errorId);
errorPanelHelper.showTextError(errorString);
}

public void onUnrecoverableError(final List<Throwable> exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
if (DEBUG) {
Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
}

ErrorActivity.reportError(getContext(), exception, MainActivity.class, null,
ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
request == null ? "none" : request, errorId));
public final void hideErrorPanel() {
errorPanelHelper.hide();
}

public void showSnackBarError(final Throwable exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request,
errorId);
public final boolean isErrorPanelVisible() {
return errorPanelHelper.isVisible();
}

/**
* Show a SnackBar and only call
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorInfo)}
* {@link ErrorActivity#reportError(Context, Class, View, ErrorInfo)}
* IF we a find a valid view (otherwise the error screen appears).
*
* @param exception List of the exceptions to show
* @param userAction The user action that caused the exception
* @param serviceName The service where the exception happened
* @param request The page that was requested
* @param errorId The ID of the error
* @param errorInfo The error information
*/
public void showSnackBarError(final List<Throwable> exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
public void showSnackBarError(final ErrorInfo errorInfo) {
if (DEBUG) {
Log.d(TAG, "showSnackBarError() called with: "
+ "exception = [" + exception + "], userAction = [" + userAction + "], "
+ "request = [" + request + "], errorId = [" + errorId + "]");
Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]");
}
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null;
if (rootView == null && getView() != null) {
if (rootView == null) {
rootView = getView();
}
if (rootView == null) {
return;
}

ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
ErrorInfo.make(userAction, serviceName, request, errorId));
ErrorActivity.reportError(requireContext(), MainActivity.class, rootView, errorInfo);
}
}
30 changes: 13 additions & 17 deletions app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java
Original file line number Diff line number Diff line change
@@ -14,7 +14,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
@@ -25,10 +24,8 @@
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentMainBinding;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.NavigationHelper;
@@ -128,7 +125,8 @@ public void onDestroy() {
//////////////////////////////////////////////////////////////////////////*/

@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
@@ -144,15 +142,14 @@ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {

@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_search:
try {
NavigationHelper.openSearchFragment(getFM(),
ServiceHelper.getSelectedServiceId(activity), "");
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
}
return true;
if (item.getItemId() == R.id.action_search) {
try {
NavigationHelper.openSearchFragment(getFM(),
ServiceHelper.getSelectedServiceId(activity), "");
} catch (final Exception e) {
ErrorActivity.reportUiError(getActivity(), null, "Opening search fragment", e);
}
return true;
}
return super.onOptionsItemSelected(item);
}
@@ -241,8 +238,7 @@ public Fragment getItem(final int position) {
}

if (throwable != null) {
ErrorActivity.reportError(context, throwable, null, null, ErrorInfo
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
ErrorActivity.reportUiError(context, null, "Getting fragment item", throwable);
return new BlankFragment();
}

@@ -254,7 +250,7 @@ public Fragment getItem(final int position) {
}

@Override
public int getItemPosition(final Object object) {
public int getItemPosition(@NonNull final Object object) {
// Causes adapter to reload all Fragments when
// notifyDataSetChanged is called
return POSITION_NONE;
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ public interface ViewContract<I> {

void showEmptyState();

void showError(String message, boolean showRetryButton);

void handleResult(I result);

void handleError();
}
Original file line number Diff line number Diff line change
@@ -37,7 +37,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
@@ -64,9 +63,7 @@
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
@@ -526,7 +523,7 @@ private void openChannel(final String subChannelUrl, final String subChannelName
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
subChannelUrl, subChannelName);
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
ErrorActivity.reportUiError(getActivity(), null, "Opening channel fragment", e);
}
}

@@ -684,13 +681,12 @@ private void initThumbnailViews(@NonNull final StreamInfo info) {
binding.detailThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);

if (!isEmpty(info.getThumbnailUrl())) {
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
@Override
public void onLoadingFailed(final String imageUri, final View view,
final FailReason failReason) {
showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE,
infoServiceName, imageUri, R.string.could_not_load_thumbnails);
showSnackBarError(new ErrorInfo(failReason.getCause(), UserAction.LOAD_IMAGE,
imageUri, info));
}
};

@@ -906,10 +902,8 @@ private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
openVideoPlayer();
}
}
}, throwable -> {
isLoading.set(false);
onError(throwable);
});
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
url == null ? "no url" : url, serviceId)));
}

/*//////////////////////////////////////////////////////////////////////////
@@ -1327,8 +1321,8 @@ private void setErrorImage(final int imageResource) {
}

@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
public void handleError() {
super.handleError();
setErrorImage(R.drawable.not_available_monkey);

if (binding.relatedStreamsLayout != null) { // hide related streams for tablets
@@ -1341,8 +1335,8 @@ public void showError(final String message, final boolean showRetryButton) {
}

private void hideAgeRestrictedContent() {
showError(getString(R.string.restricted_video,
getString(R.string.show_age_restricted_content_title)), false);
showTextError(getString(R.string.restricted_video,
getString(R.string.show_age_restricted_content_title)));
}

private void setupBroadcastReceiver() {
@@ -1548,11 +1542,8 @@ public void handleResult(@NonNull final StreamInfo info) {
}

if (!info.getErrors().isEmpty()) {
showSnackBarError(info.getErrors(),
UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(info.getServiceId()),
info.getUrl(),
0);
showSnackBarError(new ErrorInfo(info.getErrors(),
UserAction.REQUESTED_STREAM, info.getUrl(), info));
}

binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM
@@ -1592,6 +1583,10 @@ private void displayBothUploaderAndSubChannel(final StreamInfo info) {
}

public void openDownloadDialog() {
if (currentInfo == null) {
return;
}

try {
final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
downloadDialog.setVideoStreams(sortedVideoStreams);
@@ -1601,43 +1596,17 @@ public void openDownloadDialog() {

downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (final Exception e) {
final ErrorInfo info = ErrorInfo.make(UserAction.UI_ERROR,
ServiceList.all()
.get(currentInfo
.getServiceId())
.getServiceInfo()
.getName(), "",
R.string.could_not_setup_download_menu);

ErrorActivity.reportError(activity,
e,
activity.getClass(),
activity.findViewById(android.R.id.content), info);
ErrorActivity.reportError(activity, activity.getClass(),
activity.findViewById(android.R.id.content),
new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, "Showing download dialog",
currentInfo));
}
}

/*//////////////////////////////////////////////////////////////////////////
// Stream Results
//////////////////////////////////////////////////////////////////////////*/

@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}

final int errorId = exception instanceof YoutubeStreamExtractor.DeobfuscateException
? R.string.youtube_signature_deobfuscation_error
: exception instanceof ExtractionException
? R.string.parsing_error
: R.string.general_error;

onUnrecoverableError(exception, UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId), url, errorId);

return true;
}

private void updateProgressInfo(@NonNull final StreamInfo info) {
if (positionSubscriber != null) {
positionSubscriber.dispose();
Original file line number Diff line number Diff line change
@@ -14,7 +14,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -292,7 +291,7 @@ public void selected(final ChannelInfoItem selectedItem) {
selectedItem.getUrl(),
selectedItem.getName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
ErrorActivity.reportUiError(getActivity(), null, "Opening channel fragment", e);
}
}
});
@@ -307,7 +306,7 @@ public void selected(final PlaylistInfoItem selectedItem) {
selectedItem.getUrl(),
selectedItem.getName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
ErrorActivity.reportUiError(getActivity(), null, "Opening playlist fragment", e);
}
}
});
@@ -412,13 +411,6 @@ public void hideLoading() {
animate(itemsList, true, 300);
}

@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);
animate(itemsList, false, 200);
}

@Override
public void showEmptyState() {
super.showEmptyState();
@@ -439,6 +431,13 @@ public void handleNextItems(final N result) {
isLoading.set(false);
}

@Override
public void handleError() {
super.handleError();
showListFooter(false);
animate(itemsList, false, 200);
}

@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String key) {
Original file line number Diff line number Diff line change
@@ -7,12 +7,17 @@

import androidx.annotation.NonNull;

import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.views.NewPipeRecyclerView;

import java.util.ArrayList;
import java.util.List;
import java.util.Queue;

import icepick.State;
@@ -30,10 +35,15 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
@State
protected String url;

private final UserAction errorUserAction;
protected I currentInfo;
protected Page currentNextPage;
protected Disposable currentWorker;

protected BaseListInfoFragment(final UserAction errorUserAction) {
this.errorUserAction = errorUserAction;
}

@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
@@ -133,7 +143,9 @@ public void startLoading(final boolean forceLoad) {
currentInfo = result;
currentNextPage = result.getNextPage();
handleResult(result);
}, this::onError);
}, throwable ->
showError(new ErrorInfo(throwable, errorUserAction,
"Start loading: " + url, serviceId)));
}

/**
@@ -161,10 +173,9 @@ protected void loadMoreItems() {
.subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
isLoading.set(false);
handleNextItems(InfoItemsPage);
}, (@NonNull Throwable throwable) -> {
isLoading.set(false);
onError(throwable);
});
}, (@NonNull Throwable throwable) ->
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
errorUserAction, "Loading more items: " + url, serviceId)));
}

private void forbidDownwardFocusScroll() {
@@ -182,10 +193,16 @@ private void allowDownwardFocusScroll() {
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);

currentNextPage = result.getNextPage();
infoListAdapter.addInfoItemList(result.getItems());

showListFooter(hasMoreItems());

if (!result.getErrors().isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction,
"Get next items of: " + url, serviceId));
}
}

@Override
@@ -213,6 +230,18 @@ public void handleResult(@NonNull final I result) {
showEmptyState();
}
}

if (!result.getErrors().isEmpty()) {
final List<Throwable> errors = new ArrayList<>(result.getErrors());
// handling ContentNotSupportedException not to show the error but an appropriate string
// so that crashes won't be sent uselessly and the user will understand what happened
errors.removeIf(throwable -> throwable instanceof ContentNotSupportedException);

if (!errors.isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
errorUserAction, "Start loading: " + url, serviceId));
}
}
}

/*//////////////////////////////////////////////////////////////////////////
@@ -224,4 +253,14 @@ protected void setInitialData(final int sid, final String u, final String title)
this.url = u;
this.name = !TextUtils.isEmpty(title) ? title : "";
}

private void dynamicallyShowErrorPanelOrSnackbar(final ErrorInfo errorInfo) {
if (infoListAdapter.getItemCount() == 0) {
// show error panel only if no items already visible
showError(errorInfo);
} else {
isLoading.set(false);
showSnackBarError(errorInfo);
}
}
}
Original file line number Diff line number Diff line change
@@ -16,7 +16,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.viewbinding.ViewBinding;

@@ -27,20 +26,19 @@
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
import org.schabi.newpipe.databinding.FragmentChannelBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
@@ -91,6 +89,10 @@ public static ChannelFragment getInstance(final int serviceId, final String url,
return instance;
}

public ChannelFragment() {
super(UserAction.REQUESTED_CHANNEL);
}

@Override
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
@@ -217,9 +219,8 @@ public boolean onOptionsItemSelected(final MenuItem item) {
private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> {
animate(headerBinding.channelSubscribeButton, false, 100);
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(currentInfo.getServiceId()),
"Get subscription status", 0);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
"Get subscription status", currentInfo));
};

final Observable<List<SubscriptionEntity>> observable = subscriptionManager
@@ -269,11 +270,8 @@ private void updateSubscription(final ChannelInfo info) {
};

final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
onUnrecoverableError(throwable,
UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(info.getServiceId()),
"Updating Subscription for " + info.getUrl(),
R.string.subscription_update_failed);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE,
"Updating subscription for " + info.getUrl(), info));

disposables.add(subscriptionManager.updateChannelInfo(info)
.subscribeOn(Schedulers.io())
@@ -290,11 +288,8 @@ private Disposable monitorSubscribeButton(final Button subscribeButton,
};

final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
onUnrecoverableError(throwable,
UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(currentInfo.getServiceId()),
"Subscription Change",
R.string.subscription_change_failed);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_CHANGE,
"Changing subscription for " + currentInfo.getUrl(), currentInfo));

/* Emit clicks from main thread unto io thread */
return RxView.clicks(subscribeButton)
@@ -408,7 +403,7 @@ public void onClick(final View v) {
currentInfo.getParentChannelUrl(),
currentInfo.getParentChannelName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
ErrorActivity.reportUiError(getActivity(), null, "Opening channel fragment", e);
}
} else if (DEBUG) {
Log.i(TAG, "Can't open parent channel because we got no channel URL");
@@ -469,21 +464,9 @@ public void handleResult(@NonNull final ChannelInfo result) {

playlistControlBinding.getRoot().setVisibility(View.VISIBLE);

final List<Throwable> errors = new ArrayList<>(result.getErrors());
if (!errors.isEmpty()) {

// handling ContentNotSupportedException not to show the error but an appropriate string
// so that crashes won't be sent uselessly and the user will understand what happened
errors.removeIf(throwable -> {
if (throwable instanceof ContentNotSupportedException) {
showContentNotSupported();
}
return throwable instanceof ContentNotSupportedException;
});

if (!errors.isEmpty()) {
showSnackBarError(errors, UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
for (final Throwable throwable : result.getErrors()) {
if (throwable instanceof ContentNotSupportedException) {
showContentNotSupported();
}
}

@@ -537,38 +520,6 @@ private PlayQueue getPlayQueue(final int index) {
currentInfo.getNextPage(), streamItems, index);
}

@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);

if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(serviceId),
"Get next page of: " + url,
R.string.general_error);
}
}

/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/

@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}

final int errorId = exception instanceof ExtractionException
? R.string.parsing_error : R.string.general_error;

onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(serviceId), url, errorId);

return true;
}

/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
Original file line number Diff line number Diff line change
@@ -11,12 +11,11 @@
import androidx.annotation.Nullable;

import org.schabi.newpipe.R;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;

import io.reactivex.rxjava3.core.Single;
@@ -25,13 +24,17 @@
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
private final CompositeDisposable disposables = new CompositeDisposable();

public static CommentsFragment getInstance(final int serviceId, final String url,
public static CommentsFragment getInstance(final int serviceId, final String url,
final String name) {
final CommentsFragment instance = new CommentsFragment();
instance.setInitialData(serviceId, url, name);
return instance;
}

public CommentsFragment() {
super(UserAction.REQUESTED_COMMENTS);
}

/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -67,52 +70,13 @@ protected Single<CommentsInfo> loadResult(final boolean forceLoad) {
// Contract
//////////////////////////////////////////////////////////////////////////*/

@Override
public void showLoading() {
super.showLoading();
}

@Override
public void handleResult(@NonNull final CommentsInfo result) {
super.handleResult(result);

ViewUtils.slideUp(requireView(), 120, 150, 0.06f);

if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}

disposables.clear();
}

@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);

if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId), "Get next page of: " + url,
R.string.general_error);
}
}

/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/

@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}

hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
return true;
}

/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

import android.os.Bundle;

import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.kiosk.KioskList;
@@ -10,6 +11,7 @@
import org.schabi.newpipe.util.ServiceHelper;

public class DefaultKioskFragment extends KioskFragment {

@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -46,8 +48,8 @@ private void updateSelectedDefaultKiosk() {
currentInfo = null;
currentNextPage = null;
} catch (final ExtractionException e) {
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none",
"Loading default kiosk from selected service", 0);
showError(new ErrorInfo(e, UserAction.REQUESTED_KIOSK,
"Loading default kiosk for selected service"));
}
}
}
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@
import androidx.appcompat.app.ActionBar;

import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
@@ -82,6 +83,10 @@ public static KioskFragment getInstance(final int serviceId, final String kioskI
return instance;
}

public KioskFragment() {
super(UserAction.REQUESTED_KIOSK);
}

/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -102,9 +107,7 @@ public void setUserVisibleHint(final boolean isVisibleToUser) {
try {
setTitle(kioskTranslatedName);
} catch (final Exception e) {
onUnrecoverableError(e, UserAction.UI_ERROR,
"none",
"none", R.string.app_ui_crash);
showSnackBarError(new ErrorInfo(e, UserAction.UI_ERROR, "Setting kiosk title"));
}
}
}
@@ -169,22 +172,5 @@ public void handleResult(@NonNull final KioskInfo result) {

name = kioskTranslatedName;
setTitle(kioskTranslatedName);

if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_KIOSK,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
}

@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);

if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId),
"Get next page of: " + url, 0);
}
}
}
Original file line number Diff line number Diff line change
@@ -14,7 +14,6 @@

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.viewbinding.ViewBinding;

@@ -25,11 +24,12 @@
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.databinding.PlaylistHeaderBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@@ -40,8 +40,6 @@
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.KoreUtil;
@@ -87,6 +85,10 @@ public static PlaylistFragment getInstance(final int serviceId, final String url
return instance;
}

public PlaylistFragment() {
super(UserAction.REQUESTED_PLAYLIST);
}

/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -284,7 +286,7 @@ public void handleResult(@NonNull final PlaylistInfo result) {
NavigationHelper.openChannelFragment(getFM(), result.getServiceId(),
result.getUploaderUrl(), result.getUploaderName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
ErrorActivity.reportUiError(getActivity(), null, "Opening channel fragment", e);
}
});
}
@@ -315,8 +317,8 @@ public void handleResult(@NonNull final PlaylistInfo result) {
.localizeStreamCount(getContext(), result.getStreamCount()));

if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
result.getUrl(), result));
}

remotePlaylistManager.getPlaylist(result)
@@ -363,33 +365,6 @@ private PlayQueue getPlayQueue(final int index) {
);
}

@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);

if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(serviceId), "Get next page of: " + url, 0);
}
}

/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/

@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}

final int errorId = exception instanceof ExtractionException
? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(serviceId), url, errorId);
return true;
}

/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@@ -434,8 +409,9 @@ public void onNext(final List<PlaylistRemoteEntity> playlist) {
}

@Override
public void onError(final Throwable t) {
PlaylistFragment.this.onError(t);
public void onError(final Throwable throwable) {
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Get playlist bookmarks"));
}

@Override
@@ -460,12 +436,16 @@ private void onBookmarkClicked() {
if (currentInfo != null && playlistEntity == null) {
action = remotePlaylistManager.onBookmark(currentInfo)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
.subscribe(ignored -> { /* Do nothing */ }, throwable ->
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Adding playlist bookmark")));
} else if (playlistEntity != null) {
action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
.observeOn(AndroidSchedulers.mainThread())
.doFinally(() -> playlistEntity = null)
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
.subscribe(ignored -> { /* Do nothing */ }, throwable ->
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Deleting playlist bookmark")));
} else {
action = Disposable.empty();
}
Original file line number Diff line number Diff line change
@@ -47,7 +47,6 @@
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
@@ -258,11 +257,9 @@ public void onResume() {
try {
service = NewPipe.getService(serviceId);
} catch (final Exception e) {
ErrorActivity.reportError(getActivity(), e, requireActivity().getClass(),
ErrorActivity.reportUiError(getActivity(),
requireActivity().findViewById(android.R.id.content),
ErrorInfo.make(UserAction.UI_ERROR,
"",
"", R.string.general_error));
"Getting service for id " + serviceId, e);
}

if (!TextUtils.isEmpty(searchString)) {
@@ -413,7 +410,7 @@ public void reloadContent() {
searchEditText.setText("");
showKeyboardSearch();
}
animate(errorPanelRoot, false, 200);
hideErrorPanel();
}
}

@@ -540,7 +537,7 @@ private void initSearchListeners() {
if (DEBUG) {
Log.d(TAG, "onClick() called with: v = [" + v + "]");
}
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
if (isSuggestionsEnabled && !isErrorPanelVisible()) {
showSuggestionsPanel();
}
if (DeviceUtils.isTv(getContext())) {
@@ -553,8 +550,7 @@ private void initSearchListeners() {
Log.d(TAG, "onFocusChange() called with: "
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
}
if (isSuggestionsEnabled && hasFocus
&& errorPanelRoot.getVisibility() != View.VISIBLE) {
if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) {
showSuggestionsPanel();
}
});
@@ -704,9 +700,9 @@ private void showDeleteSuggestionDialog(final SuggestionItem item) {
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable,
UserAction.DELETE_FROM_HISTORY, "none",
"Deleting item failed", R.string.general_error));
throwable -> showSnackBarError(new ErrorInfo(throwable,
UserAction.DELETE_FROM_HISTORY,
"Deleting item failed")));
disposables.add(onDelete);
})
.show();
@@ -763,8 +759,8 @@ private void initSuggestionObserver() {
.suggestionsFor(serviceId, query)
.onErrorReturn(throwable -> {
if (!ExceptionUtils.isNetworkRelated(throwable)) {
showSnackBarError(throwable, UserAction.GET_SUGGESTIONS,
NewPipe.getNameOfService(serviceId), searchString, 0);
showSnackBarError(new ErrorInfo(throwable,
UserAction.GET_SUGGESTIONS, searchString, serviceId));
}
return new ArrayList<>();
})
@@ -800,7 +796,8 @@ private void initSuggestionObserver() {
if (listNotification.isOnNext()) {
handleSuggestions(listNotification.getValue());
} else if (listNotification.isOnError()) {
onSuggestionError(listNotification.getError());
showError(new ErrorInfo(listNotification.getError(),
UserAction.GET_SUGGESTIONS, searchString, serviceId));
}
});
}
@@ -832,8 +829,7 @@ private void search(final String theSearchString,
.subscribe(intent -> {
getFM().popBackStackImmediate();
activity.startActivity(intent);
}, throwable ->
showError(getString(R.string.unsupported_url), false)));
}, throwable -> showTextError(getString(R.string.unsupported_url))));
return;
}
} catch (final Exception ignored) {
@@ -849,10 +845,9 @@ private void search(final String theSearchString,
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
ignored -> {
},
error -> showSnackBarError(error, UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), theSearchString, 0)
ignored -> {},
throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
theSearchString, serviceId))
));
suggestionPublisher.onNext(theSearchString);
startLoading(false);
@@ -872,7 +867,7 @@ public void startLoading(final boolean forceLoad) {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
.subscribe(this::handleResult, this::onError);
.subscribe(this::handleResult, this::onItemError);

}

@@ -895,7 +890,7 @@ protected void loadMoreItems() {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
.subscribe(this::handleNextItems, this::onError);
.subscribe(this::handleNextItems, this::onItemError);
}

@Override
@@ -909,6 +904,15 @@ protected void onItemSelected(final InfoItem selectedItem) {
hideKeyboardSearch();
}

private void onItemError(final Throwable exception) {
if (exception instanceof SearchExtractor.NothingFoundException) {
infoListAdapter.clearStreamItemList();
showEmptyState();
} else {
showError(new ErrorInfo(exception, UserAction.SEARCHED, searchString, serviceId));
}
}

/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@@ -945,26 +949,11 @@ public void handleSuggestions(@NonNull final List<SuggestionItem> suggestions) {
searchBinding.suggestionsList.smoothScrollToPosition(0);
searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions));

if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) {
if (suggestionsPanelVisible && isErrorPanelVisible()) {
hideLoading();
}
}

public void onSuggestionError(final Throwable exception) {
if (DEBUG) {
Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
}
if (super.onError(exception)) {
return;
}

final int errorId = exception instanceof ParsingException
? R.string.parsing_error
: R.string.general_error;
onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS,
NewPipe.getNameOfService(serviceId), searchString, errorId);
}

/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@@ -975,13 +964,6 @@ public void hideLoading() {
showListFooter(false);
}

@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
hideSuggestionsPanel();
hideKeyboardSearch();
}

/*//////////////////////////////////////////////////////////////////////////
// Search Results
//////////////////////////////////////////////////////////////////////////*/
@@ -992,8 +974,8 @@ public void handleResult(@NonNull final SearchInfo result) {
if (!exceptions.isEmpty()
&& !(exceptions.size() == 1
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), searchString, 0);
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
searchString, serviceId));
}

searchSuggestion = result.getSearchSuggestion();
@@ -1061,33 +1043,20 @@ public void handleNextItems(final ListExtractor.InfoItemsPage<?> result) {
nextPage = result.getNextPage();

if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId),
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
+ "pageIds: " + nextPage.getIds() + ", "
+ "pageCookies: " + nextPage.getCookies(), 0);
+ "pageCookies: " + nextPage.getCookies(),
serviceId));
}
super.handleNextItems(result);
}

@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}

if (exception instanceof SearchExtractor.NothingFoundException) {
infoListAdapter.clearStreamItemList();
showEmptyState();
} else {
final int errorId = exception instanceof ParsingException
? R.string.parsing_error
: R.string.general_error;
onUnrecoverableError(exception, UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), searchString, errorId);
}

return true;
public void handleError() {
super.handleError();
hideSuggestionsPanel();
hideKeyboardSearch();
}

/*//////////////////////////////////////////////////////////////////////////
@@ -1113,9 +1082,8 @@ public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHo
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable,
UserAction.DELETE_FROM_HISTORY, "none",
"Deleting item failed", R.string.general_error));
throwable -> showSnackBarError(new ErrorInfo(throwable,
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
disposables.add(onDelete);
}
}
Original file line number Diff line number Diff line change
@@ -47,6 +47,10 @@ public static RelatedVideosFragment getInstance(final StreamInfo info) {
return instance;
}

public RelatedVideosFragment() {
super(UserAction.REQUESTED_STREAM);
}

/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -125,43 +129,9 @@ public void handleResult(@NonNull final RelatedStreamInfo result) {
}
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);

if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}

disposables.clear();
}

@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);

if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId),
"Get next page of: " + url,
R.string.general_error);
}
}

/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/

@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}

hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId), url, R.string.general_error);
return true;
}

/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
Original file line number Diff line number Diff line change
@@ -171,15 +171,15 @@ private void openCommentAuthor(final CommentsInfoItem item) {
if (TextUtils.isEmpty(item.getUploaderUrl())) {
return;
}
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
try {
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
NavigationHelper.openChannelFragment(
activity.getSupportFragmentManager(),
item.getServiceId(),
item.getUploaderUrl(),
item.getUploaderName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e);
ErrorActivity.reportUiError(activity, null, "Opening channel fragment", e);
}
}

Original file line number Diff line number Diff line change
@@ -202,19 +202,6 @@ public void hideLoading() {
}
}

@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);

if (itemsList != null) {
animate(itemsList, false, 200);
}
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200);
}
}

@Override
public void showEmptyState() {
super.showEmptyState();
@@ -249,9 +236,18 @@ protected void resetFragment() {
}

@Override
protected boolean onError(final Throwable exception) {
public void handleError() {
super.handleError();
resetFragment();
return super.onError(exception);

showListFooter(false);

if (itemsList != null) {
animate(itemsList, false, 200);
}
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200);
}
}

@Override
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
@@ -206,7 +207,8 @@ public void onNext(final List<PlaylistLocalItem> subscriptions) {

@Override
public void onError(final Throwable exception) {
BookmarkFragment.this.onError(exception);
showError(new ErrorInfo(exception,
UserAction.REQUESTED_BOOKMARK, "Loading playlists"));
}

@Override
@@ -237,17 +239,6 @@ public void handleResult(@NonNull final List<PlaylistLocalItem> result) {
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////

@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}

onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Bookmark", R.string.general_error);
return true;
}

@Override
protected void resetFragment() {
super.resetFragment();
@@ -295,8 +286,10 @@ private void showDeleteDialog(final String name, final Single<Integer> deleteRea
.setPositiveButton(R.string.delete, (dialog, i) ->
disposables.add(deleteReactor
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> { /*Do nothing on success*/ }, this::onError))
)
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Deleting playlist")))))
.setNegativeButton(R.string.cancel, null)
.show();
}
@@ -314,7 +307,10 @@ private void changeLocalPlaylistName(final long id, final String name) {
localPlaylistManager.renamePlaylist(id, name);
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> { /*Do nothing on success*/ }, this::onError);
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Changing playlist name")));
disposables.add(disposable);
}
}
72 changes: 18 additions & 54 deletions app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@ import icepick.State
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.FragmentFeedBinding
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.fragments.list.BaseListFragment
import org.schabi.newpipe.ktx.animate
@@ -48,7 +49,6 @@ import java.util.Calendar
class FeedFragment : BaseListFragment<FeedState, Unit>() {
private var _feedBinding: FragmentFeedBinding? = null
private val feedBinding get() = _feedBinding!!
private val errorBinding get() = _feedBinding!!.errorPanel

private lateinit var viewModel: FeedViewModel
@State
@@ -171,50 +171,24 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
// /////////////////////////////////////////////////////////////////////////

override fun showLoading() {
super.showLoading()
feedBinding.refreshRootView.animate(false, 0)
feedBinding.itemsList.animate(false, 0)

feedBinding.loadingProgressBar.animate(true, 200)
feedBinding.loadingProgressText.animate(true, 200)

feedBinding.emptyStateView.root.animate(false, 0)
errorBinding.root.animate(false, 0)
}

override fun hideLoading() {
super.hideLoading()
feedBinding.refreshRootView.animate(true, 200)
feedBinding.itemsList.animate(true, 300)

feedBinding.loadingProgressBar.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0)

feedBinding.emptyStateView.root.animate(false, 0)
errorBinding.root.animate(false, 0)
feedBinding.swiperefresh.isRefreshing = false
}

override fun showEmptyState() {
super.showEmptyState()
feedBinding.refreshRootView.animate(true, 200)
feedBinding.itemsList.animate(false, 0)

feedBinding.loadingProgressBar.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0)

feedBinding.emptyStateView.root.animate(true, 800)
errorBinding.root.animate(false, 0)
}

override fun showError(message: String, showRetryButton: Boolean) {
infoListAdapter.clearStreamItemList()
feedBinding.refreshRootView.animate(false, 120)
feedBinding.itemsList.animate(false, 120)

feedBinding.loadingProgressBar.animate(false, 120)
feedBinding.loadingProgressText.animate(false, 120)

errorBinding.errorMessageView.text = message
errorBinding.errorButtonRetry.animate(showRetryButton, if (showRetryButton) 600 else 0)
errorBinding.root.animate(true, 300)
}

override fun handleResult(result: FeedState) {
@@ -227,6 +201,14 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
updateRefreshViewState()
}

override fun handleError() {
super.handleError()
infoListAdapter.clearStreamItemList()
feedBinding.refreshRootView.animate(false, 200)
feedBinding.itemsList.animate(false, 200)
feedBinding.loadingProgressText.animate(false, 200)
}

private fun handleProgressState(progressState: FeedState.ProgressState) {
showLoading()

@@ -266,13 +248,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
)
}

if (loadedState.itemsErrors.isNotEmpty()) {
showSnackBarError(
loadedState.itemsErrors, UserAction.REQUESTED_FEED,
"none", "Loading feed", R.string.general_error
)
}

if (loadedState.items.isEmpty()) {
showEmptyState()
} else {
@@ -281,12 +256,13 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
}

private fun handleErrorState(errorState: FeedState.ErrorState): Boolean {
hideLoading()
errorState.error?.let {
onError(errorState.error)
return true
return if (errorState.error == null) {
hideLoading()
false
} else {
showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed"))
true
}
return false
}

private fun updateRelativeTimeViews() {
@@ -320,18 +296,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
listState = null
}

override fun onError(exception: Throwable): Boolean {
if (super.onError(exception)) return true

if (useAsFrontPage) {
showSnackBarError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0)
return true
}

onUnrecoverableError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0)
return true
}

companion object {
const val KEY_GROUP_ID = "ARG_GROUP_ID"
const val KEY_GROUP_NAME = "ARG_GROUP_NAME"
Loading