1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-12-23 16:40:32 +00:00

Merge pull request #5148 from Stypox/error-panel

Show improved error panel instead of annoying snackbar or crashing
This commit is contained in:
Tobi 2021-03-14 17:41:27 +01:00 committed by GitHub
commit 404a6c12a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 1148 additions and 1511 deletions

View File

@ -0,0 +1,46 @@
package org.schabi.newpipe.error;
import android.os.Parcel;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* Instrumented tests for {@link ErrorInfo}.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ErrorInfoTest {
@Test
public void errorInfoTestParcelable() {
final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"),
UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId());
// Obtain a Parcel object and write the parcelable object to it:
final Parcel parcel = Parcel.obtain();
info.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
final ErrorInfo infoFromParcel = (ErrorInfo) ErrorInfo.CREATOR.createFromParcel(parcel);
assertTrue(Arrays.toString(infoFromParcel.getStackTraces())
.contains(ErrorInfoTest.class.getSimpleName()));
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
assertEquals(ServiceList.YouTube.getServiceInfo().getName(),
infoFromParcel.getServiceName());
assertEquals("request", infoFromParcel.getRequest());
assertEquals(R.string.parsing_error, infoFromParcel.getMessageStringId());
parcel.recycle();
}
}

View File

@ -1,38 +0,0 @@
package org.schabi.newpipe.report;
import android.os.Parcel;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.schabi.newpipe.R;
import static org.junit.Assert.assertEquals;
/**
* Instrumented tests for {@link ErrorInfo}.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ErrorInfoTest {
@Test
public void errorInfoTestParcelable() {
final ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request",
R.string.general_error);
// Obtain a Parcel object and write the parcelable object to it:
final Parcel parcel = Parcel.obtain();
info.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
final ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel);
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
assertEquals("youtube", infoFromParcel.getServiceName());
assertEquals("request", infoFromParcel.getRequest());
assertEquals(R.string.general_error, infoFromParcel.getMessage());
parcel.recycle();
}
}

View File

@ -85,7 +85,7 @@
android:name=".ExitActivity" android:name=".ExitActivity"
android:label="@string/general_error" android:label="@string/general_error"
android:theme="@android:style/Theme.NoDisplay" /> android:theme="@android:style/Theme.NoDisplay" />
<activity android:name=".report.ErrorActivity" /> <activity android:name=".error.ErrorActivity" />
<!-- giga get related --> <!-- giga get related -->
<activity <activity
@ -106,7 +106,7 @@
</activity> </activity>
<activity <activity
android:name=".ReCaptchaActivity" android:name=".error.ReCaptchaActivity"
android:label="@string/recaptcha" /> android:label="@string/recaptcha" />
<provider <provider

View File

@ -1,47 +0,0 @@
package org.schabi.newpipe;
/*
* Created by Christian Schabesberger on 24.12.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* ActivityCommunicator.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* 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.
**/
public class ActivityCommunicator {
private static ActivityCommunicator activityCommunicator;
private volatile Class returnActivity;
public static ActivityCommunicator getCommunicator() {
if (activityCommunicator == null) {
activityCommunicator = new ActivityCommunicator();
}
return activityCommunicator;
}
public Class getReturnActivity() {
return returnActivity;
}
public void setReturnActivity(final Class returnActivity) {
this.returnActivity = returnActivity;
}
}

View File

@ -20,12 +20,13 @@ import org.acra.ACRA;
import org.acra.config.ACRAConfigurationException; import org.acra.config.ACRAConfigurationException;
import org.acra.config.CoreConfiguration; import org.acra.config.CoreConfiguration;
import org.acra.config.CoreConfigurationBuilder; import org.acra.config.CoreConfigurationBuilder;
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.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
@ -224,14 +225,10 @@ public class App extends MultiDexApplication {
.setBuildConfigClass(BuildConfig.class) .setBuildConfigClass(BuildConfig.class)
.build(); .build();
ACRA.init(this, acraConfig); ACRA.init(this, acraConfig);
} catch (final ACRAConfigurationException ace) { } catch (final ACRAConfigurationException exception) {
ace.printStackTrace(); exception.printStackTrace();
ErrorActivity.reportError(this, ErrorActivity.reportError(this, new ErrorInfo(exception,
ace, UserAction.SOMETHING_ELSE, "Could not initialize ACRA crash report"));
null,
null,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not initialize ACRA crash report", R.string.app_ui_crash));
} }
} }

View File

@ -10,19 +10,22 @@ import android.content.pm.Signature;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException; import com.grack.nanojson.JsonParserException;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Maybe; import org.schabi.newpipe.error.ErrorActivity;
import io.reactivex.rxjava3.disposables.Disposable; import org.schabi.newpipe.error.ErrorInfo;
import io.reactivex.rxjava3.schedulers.Schedulers; import org.schabi.newpipe.error.UserAction;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.security.MessageDigest; import java.security.MessageDigest;
@ -31,9 +34,11 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import org.schabi.newpipe.report.UserAction; import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class CheckForNewAppVersion { public final class CheckForNewAppVersion {
private CheckForNewAppVersion() { } private CheckForNewAppVersion() { }
@ -58,9 +63,8 @@ public final class CheckForNewAppVersion {
packageInfo = application.getPackageManager().getPackageInfo( packageInfo = application.getPackageManager().getPackageInfo(
application.getPackageName(), PackageManager.GET_SIGNATURES); application.getPackageName(), PackageManager.GET_SIGNATURES);
} catch (final PackageManager.NameNotFoundException e) { } catch (final PackageManager.NameNotFoundException e) {
ErrorActivity.reportError(application, e, null, null, ErrorActivity.reportError(application, new ErrorInfo(e,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
"Could not find package info", R.string.app_ui_crash));
return ""; return "";
} }
@ -72,9 +76,8 @@ public final class CheckForNewAppVersion {
final CertificateFactory cf = CertificateFactory.getInstance("X509"); final CertificateFactory cf = CertificateFactory.getInstance("X509");
c = (X509Certificate) cf.generateCertificate(input); c = (X509Certificate) cf.generateCertificate(input);
} catch (final CertificateException e) { } catch (final CertificateException e) {
ErrorActivity.reportError(application, e, null, null, ErrorActivity.reportError(application, new ErrorInfo(e,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error"));
"Certificate error", R.string.app_ui_crash));
return ""; return "";
} }
@ -83,9 +86,8 @@ public final class CheckForNewAppVersion {
final byte[] publicKey = md.digest(c.getEncoded()); final byte[] publicKey = md.digest(c.getEncoded());
return byte2HexFormatted(publicKey); return byte2HexFormatted(publicKey);
} catch (NoSuchAlgorithmException | CertificateEncodingException e) { } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
ErrorActivity.reportError(application, e, null, null, ErrorActivity.reportError(application, new ErrorInfo(e,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key"));
"Could not retrieve SHA1 key", R.string.app_ui_crash));
return ""; return "";
} }
} }

View File

@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request; import org.schabi.newpipe.extractor.downloader.Request;
import org.schabi.newpipe.extractor.downloader.Response; import org.schabi.newpipe.extractor.downloader.Response;

View File

@ -60,6 +60,7 @@ import org.schabi.newpipe.databinding.DrawerHeaderBinding;
import org.schabi.newpipe.databinding.DrawerLayoutBinding; import org.schabi.newpipe.databinding.DrawerLayoutBinding;
import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding; import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
import org.schabi.newpipe.databinding.ToolbarLayoutBinding; import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@ -72,7 +73,6 @@ import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.KioskTranslator;
@ -153,7 +153,7 @@ public class MainActivity extends AppCompatActivity {
try { try {
setupDrawer(); setupDrawer();
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError(this, e); ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e);
} }
if (DeviceUtils.isTv(this)) { if (DeviceUtils.isTv(this)) {
@ -238,7 +238,7 @@ public class MainActivity extends AppCompatActivity {
try { try {
tabSelected(item); tabSelected(item);
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError(this, e); ErrorActivity.reportUiErrorInSnackbar(this, "Selecting main page tab", e);
} }
break; break;
case R.id.menu_options_about_group: case R.id.menu_options_about_group:
@ -340,7 +340,7 @@ public class MainActivity extends AppCompatActivity {
try { try {
showTabs(); showTabs();
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError(this, e); ErrorActivity.reportUiErrorInSnackbar(this, "Showing main page tabs", e);
} }
} }
} }
@ -487,7 +487,7 @@ public class MainActivity extends AppCompatActivity {
drawerHeaderBinding.drawerHeaderActionButton.setContentDescription( drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
getString(R.string.drawer_header_description) + selectedServiceName); getString(R.string.drawer_header_description) + selectedServiceName);
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError(this, e); ErrorActivity.reportUiErrorInSnackbar(this, "Setting up service toggle", e);
} }
final SharedPreferences sharedPreferences final SharedPreferences sharedPreferences
@ -679,19 +679,16 @@ public class MainActivity extends AppCompatActivity {
} }
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
} }
final int id = item.getItemId();
switch (id) { if (item.getItemId() == android.R.id.home) {
case android.R.id.home:
onHomeButtonPressed(); onHomeButtonPressed();
return true; return true;
default:
return super.onOptionsItemSelected(item);
} }
return super.onOptionsItemSelected(item);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -799,7 +796,7 @@ public class MainActivity extends AppCompatActivity {
NavigationHelper.gotoMainFragment(getSupportFragmentManager()); NavigationHelper.gotoMainFragment(getSupportFragmentManager());
} }
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError(this, e); ErrorActivity.reportUiErrorInSnackbar(this, "Handling intent", e);
} }
} }

View File

@ -33,15 +33,29 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding; import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding; import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.download.DownloadDialog; 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.Info;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.StreamingService.LinkType; import org.schabi.newpipe.extractor.StreamingService.LinkType;
import org.schabi.newpipe.extractor.channel.ChannelInfo; 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.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.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
@ -49,7 +63,6 @@ import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -84,13 +97,6 @@ import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
* Get the url from the intent and open it in the chosen preferred player. * Get the url from the intent and open it in the chosen preferred player.
*/ */
public class RouterActivity extends AppCompatActivity { 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(); protected final CompositeDisposable disposables = new CompositeDisposable();
@State @State
protected int currentServiceId = -1; protected int currentServiceId = -1;
@ -100,7 +106,6 @@ public class RouterActivity extends AppCompatActivity {
protected int selectedRadioPosition = -1; protected int selectedRadioPosition = -1;
protected int selectedPreviously = -1; protected int selectedPreviously = -1;
protected String currentUrl; protected String currentUrl;
protected boolean internalRoute = false;
private StreamingService currentService; private StreamingService currentService;
private boolean selectionIsDownload = false; private boolean selectionIsDownload = false;
@ -123,7 +128,7 @@ public class RouterActivity extends AppCompatActivity {
} }
@Override @Override
protected void onSaveInstanceState(final Bundle outState) { protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState); Icepick.saveInstanceState(this, outState);
} }
@ -145,6 +150,7 @@ public class RouterActivity extends AppCompatActivity {
private void handleUrl(final String url) { private void handleUrl(final String url) {
disposables.add(Observable disposables.add(Observable
.fromCallable(() -> { .fromCallable(() -> {
try {
if (currentServiceId == -1) { if (currentServiceId == -1) {
currentService = NewPipe.getServiceByUrl(url); currentService = NewPipe.getServiceByUrl(url);
currentServiceId = currentService.getServiceId(); currentServiceId = currentService.getServiceId();
@ -154,28 +160,69 @@ public class RouterActivity extends AppCompatActivity {
currentService = NewPipe.getService(currentServiceId); currentService = NewPipe.getService(currentServiceId);
} }
// return whether the url was found to be supported or not
return currentLinkType != LinkType.NONE; return currentLinkType != LinkType.NONE;
} catch (final ExtractionException e) {
// this can be reached only when the url is completely unsupported
return false;
}
}) })
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> { .subscribe(isUrlSupported -> {
if (result) { if (isUrlSupported) {
onSuccess(); onSuccess();
} else { } else {
showUnsupportedUrlDialog(url); showUnsupportedUrlDialog(url);
} }
}, throwable -> handleError(throwable, url))); }, throwable -> handleError(this, new ErrorInfo(throwable,
UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url))));
} }
private void handleError(final Throwable throwable, final String url) { /**
throwable.printStackTrace(); * @param context the context. It will be {@code finish()}ed at the end of the handling if it is
* an instance of {@link RouterActivity}.
* @param errorInfo the error information
*/
private static void handleError(final Context context, final ErrorInfo errorInfo) {
if (errorInfo.getThrowable() != null) {
errorInfo.getThrowable().printStackTrace();
}
if (throwable instanceof ExtractionException) { if (errorInfo.getThrowable() instanceof ReCaptchaException) {
showUnsupportedUrlDialog(url); 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 { } else {
ExtractorHelper.handleGeneralException(this, -1, url, throwable, ErrorActivity.reportError(context, errorInfo);
UserAction.SOMETHING_ELSE, null); }
finish();
if (context instanceof RouterActivity) {
((RouterActivity) context).finish();
} }
} }
@ -500,7 +547,8 @@ public class RouterActivity extends AppCompatActivity {
.subscribe(intent -> { .subscribe(intent -> {
startActivity(intent); startActivity(intent);
finish(); finish();
}, throwable -> handleError(throwable, currentUrl)) }, throwable -> handleError(this, new ErrorInfo(throwable,
UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl)))
); );
return; return;
} }
@ -580,6 +628,7 @@ public class RouterActivity extends AppCompatActivity {
this.playerChoice = playerChoice; this.playerChoice = playerChoice;
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
@ -646,9 +695,9 @@ public class RouterActivity extends AppCompatActivity {
if (fetcher != null) { if (fetcher != null) {
fetcher.dispose(); fetcher.dispose();
} }
}, throwable -> ExtractorHelper.handleGeneralException(this, }, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
choice.serviceId, choice.url, throwable, finalUserAction, choice.url + " opened with " + choice.playerChoice,
", opened with " + choice.playerChoice)); choice.serviceId)));
} }
} }

View File

@ -7,7 +7,6 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import kotlin.jvm.Throws
data class PlaylistStreamEntry( data class PlaylistStreamEntry(
@Embedded @Embedded

View File

@ -37,6 +37,9 @@ import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity; import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.databinding.DownloadDialogBinding; import org.schabi.newpipe.databinding.DownloadDialogBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.extractor.localization.Localization;
@ -45,9 +48,6 @@ import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.FilenameUtils; import org.schabi.newpipe.util.FilenameUtils;
@ -61,7 +61,6 @@ import org.schabi.newpipe.util.ThemeHelper;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -591,17 +590,6 @@ public class DownloadDialog extends DialogFragment
.show(); .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() { private void prepareSelectedDownload() {
final StoredDirectoryHelper mainStorage; final StoredDirectoryHelper mainStorage;
final MediaFormat format; final MediaFormat format;
@ -705,7 +693,8 @@ public class DownloadDialog extends DialogFragment
mainStorage.getTag()); mainStorage.getTag());
} }
} catch (final Exception e) { } catch (final Exception e) {
showErrorActivity(e); ErrorActivity.reportErrorInSnackbar(this,
new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"));
return; return;
} }

View File

@ -1,9 +1,10 @@
package org.schabi.newpipe.report; package org.schabi.newpipe.error;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.acra.ReportField;
import org.acra.data.CrashReportData; import org.acra.data.CrashReportData;
import org.acra.sender.ReportSender; import org.acra.sender.ReportSender;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -32,8 +33,12 @@ public class AcraReportSender implements ReportSender {
@Override @Override
public void send(@NonNull final Context context, @NonNull final CrashReportData report) { public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
ErrorActivity.reportError(context, report, ErrorActivity.reportError(context, new ErrorInfo(
ErrorInfo.make(UserAction.UI_ERROR, "none", new String[]{report.getString(ReportField.STACK_TRACE)},
"App crash, UI failure", R.string.app_ui_crash)); UserAction.UI_ERROR,
ErrorInfo.SERVICE_NONE,
"ACRA report",
R.string.app_ui_crash,
null));
} }
} }

View File

@ -1,4 +1,4 @@
package org.schabi.newpipe.report; package org.schabi.newpipe.error;
import android.content.Context; import android.content.Context;

View File

@ -1,4 +1,4 @@
package org.schabi.newpipe.report; package org.schabi.newpipe.error;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
@ -8,7 +8,6 @@ import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -18,14 +17,11 @@ import android.view.View;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NavUtils; import androidx.fragment.app.Fragment;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import com.grack.nanojson.JsonWriter; 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.BuildConfig;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -34,14 +30,9 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.io.PrintWriter; import java.time.LocalDateTime;
import java.io.StringWriter; import java.time.format.DateTimeFormatter;
import java.text.SimpleDateFormat;
import java.util.Arrays; 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; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -70,108 +61,77 @@ public class ErrorActivity extends AppCompatActivity {
public static final String TAG = ErrorActivity.class.toString(); public static final String TAG = ErrorActivity.class.toString();
// BUNDLE TAGS // BUNDLE TAGS
public static final String ERROR_INFO = "error_info"; 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_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
= "Exception in NewPipe " + BuildConfig.VERSION_NAME;
public static final String ERROR_GITHUB_ISSUE_URL public static final String ERROR_GITHUB_ISSUE_URL
= "https://github.com/TeamNewPipe/NewPipe/issues"; = "https://github.com/TeamNewPipe/NewPipe/issues";
private String[] errorList; public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private ErrorInfo errorInfo; private ErrorInfo errorInfo;
private Class returnActivity;
private String currentTimeStamp; private String currentTimeStamp;
private ActivityErrorBinding activityErrorBinding; private ActivityErrorBinding activityErrorBinding;
public static void reportUiError(final AppCompatActivity activity, final Throwable el) { public static void reportError(final Context context, final ErrorInfo errorInfo) {
reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UserAction.UI_ERROR, final Intent intent = new Intent(context, ErrorActivity.class);
"none", "", R.string.app_ui_crash)); intent.putExtra(ERROR_INFO, errorInfo);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
} }
public static void reportError(final Context context, final List<Throwable> el, public static void reportErrorInSnackbar(final Context context, final ErrorInfo errorInfo) {
final Class returnActivity, final View rootView, final View rootView = context instanceof Activity
? ((Activity) context).findViewById(android.R.id.content) : null;
reportErrorInSnackbar(context, rootView, errorInfo);
}
public static void reportErrorInSnackbar(final Fragment fragment, final ErrorInfo errorInfo) {
View rootView = fragment.getView();
if (rootView == null && fragment.getActivity() != null) {
rootView = fragment.getActivity().findViewById(android.R.id.content);
}
reportErrorInSnackbar(fragment.requireContext(), rootView, errorInfo);
}
public static void reportUiErrorInSnackbar(final Context context,
final String request,
final Throwable throwable) {
reportErrorInSnackbar(context, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
}
public static void reportUiErrorInSnackbar(final Fragment fragment,
final String request,
final Throwable throwable) {
reportErrorInSnackbar(fragment, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
}
////////////////////////////////////////////////////////////////////////
// Utils
////////////////////////////////////////////////////////////////////////
private static void reportErrorInSnackbar(final Context context,
@Nullable final View rootView,
final ErrorInfo errorInfo) { final ErrorInfo errorInfo) {
if (rootView != null) { 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) .setActionTextColor(Color.YELLOW)
.setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v -> .setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v ->
startErrorActivity(returnActivity, context, errorInfo, el)).show(); reportError(context, errorInfo)).show();
} else { } else {
startErrorActivity(returnActivity, context, errorInfo, el); reportError(context, errorInfo);
} }
} }
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, // Activity lifecycle
final ErrorInfo errorInfo) { ////////////////////////////////////////////////////////////////////////
List<Throwable> el = null;
if (e != null) {
el = new Vector<>();
el.add(e);
}
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);
}
// 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)};
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 @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
@ -193,38 +153,28 @@ public class ErrorActivity extends AppCompatActivity {
actionBar.setDisplayShowTitleEnabled(true); actionBar.setDisplayShowTitleEnabled(true);
} }
final ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
returnActivity = ac.getReturnActivity();
errorInfo = intent.getParcelableExtra(ERROR_INFO); errorInfo = intent.getParcelableExtra(ERROR_INFO);
errorList = intent.getStringArrayExtra(ERROR_LIST);
// important add guru meditation // important add guru meditation
addGuruMeditation(); addGuruMeditation();
currentTimeStamp = getCurrentTimeStamp(); currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());
activityErrorBinding.errorReportEmailButton.setOnClickListener(v -> activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "EMAIL")); openPrivacyPolicyDialog(this, "EMAIL"));
activityErrorBinding.errorReportCopyButton.setOnClickListener(v -> { activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
ShareUtils.copyToClipboard(this, buildMarkdown()); ShareUtils.copyToClipboard(this, buildMarkdown()));
});
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v -> activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "GITHUB")); openPrivacyPolicyDialog(this, "GITHUB"));
// normal bugreport // normal bugreport
buildInfo(errorInfo); buildInfo(errorInfo);
if (errorInfo.getMessage() != 0) { activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId());
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage()); activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
} else {
activityErrorBinding.errorMessageView.setVisibility(View.GONE);
activityErrorBinding.messageWhatHappenedView.setVisibility(View.GONE);
}
activityErrorBinding.errorView.setText(formErrorText(errorList));
// print stack trace once again for debugging: // print stack trace once again for debugging:
for (final String e : errorList) { for (final String e : errorInfo.getStackTraces()) {
Log.e(TAG, e); Log.e(TAG, e);
} }
} }
@ -239,16 +189,15 @@ public class ErrorActivity extends AppCompatActivity {
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId(); final int id = item.getItemId();
switch (id) { if (id == android.R.id.home) {
case android.R.id.home: onBackPressed();
goToReturnActivity(); } else if (id == R.id.menu_item_share_error) {
break;
case R.id.menu_item_share_error:
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson()); ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
break; } else {
}
return false; return false;
} }
return true;
}
private void openPrivacyPolicyDialog(final Context context, final String action) { private void openPrivacyPolicyDialog(final Context context, final String action) {
new AlertDialog.Builder(context) new AlertDialog.Builder(context)
@ -264,7 +213,9 @@ public class ErrorActivity extends AppCompatActivity {
final Intent i = new Intent(Intent.ACTION_SENDTO) final Intent i = new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse("mailto:")) // only email apps should handle this .setData(Uri.parse("mailto:")) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS}) .putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS})
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
+ getString(R.string.app_name) + " "
+ BuildConfig.VERSION_NAME)
.putExtra(Intent.EXTRA_TEXT, buildJson()); .putExtra(Intent.EXTRA_TEXT, buildJson());
if (i.resolveActivity(getPackageManager()) != null) { if (i.resolveActivity(getPackageManager()) != null) {
ShareUtils.openIntentInApp(context, i); ShareUtils.openIntentInApp(context, i);
@ -310,17 +261,6 @@ public class ErrorActivity extends AppCompatActivity {
return checkedReturnActivity; return checkedReturnActivity;
} }
private void goToReturnActivity() {
final Class<? extends Activity> checkedReturnActivity = getReturnActivity(returnActivity);
if (checkedReturnActivity == null) {
super.onBackPressed();
} else {
final Intent intent = new Intent(this, checkedReturnActivity);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
NavUtils.navigateUpTo(this, intent);
}
}
private void buildInfo(final ErrorInfo info) { private void buildInfo(final ErrorInfo info) {
String text = ""; String text = "";
@ -355,7 +295,7 @@ public class ErrorActivity extends AppCompatActivity {
.value("version", BuildConfig.VERSION_NAME) .value("version", BuildConfig.VERSION_NAME)
.value("os", getOsString()) .value("os", getOsString())
.value("time", currentTimeStamp) .value("time", currentTimeStamp)
.array("exceptions", Arrays.asList(errorList)) .array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
.value("user_comment", activityErrorBinding.errorCommentBox.getText() .value("user_comment", activityErrorBinding.errorCommentBox.getText()
.toString()) .toString())
.end() .end()
@ -393,27 +333,27 @@ public class ErrorActivity extends AppCompatActivity {
// Collapse all logs to a single paragraph when there are more than one // Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean. // to keep the GitHub issue clean.
if (errorList.length > 1) { if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport htmlErrorReport
.append("<details><summary><b>Exceptions (") .append("<details><summary><b>Exceptions (")
.append(errorList.length) .append(errorInfo.getStackTraces().length)
.append(")</b></summary><p>\n"); .append(")</b></summary><p>\n");
} }
// add the logs // 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 "); htmlErrorReport.append("<details><summary><b>Crash log ");
if (errorList.length > 1) { if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append(i + 1); htmlErrorReport.append(i + 1);
} }
htmlErrorReport.append("</b>") htmlErrorReport.append("</b>")
.append("</summary><p>\n") .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"); .append("</details>\n");
} }
// make sure to close everything // make sure to close everything
if (errorList.length > 1) { if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append("</p></details>\n"); htmlErrorReport.append("</p></details>\n");
} }
htmlErrorReport.append("<hr>\n"); htmlErrorReport.append("<hr>\n");
@ -460,17 +400,4 @@ public class ErrorActivity extends AppCompatActivity {
text += "\n" + getString(R.string.guru_meditation); text += "\n" + getString(R.string.guru_meditation);
activityErrorBinding.errorSorryView.setText(text); activityErrorBinding.errorSorryView.setText(text);
} }
@Override
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());
}
} }

View File

@ -0,0 +1,113 @@
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 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 {
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
) : 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
}
}
}
}

View File

@ -0,0 +1,132 @@
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, 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
}
}

View File

@ -1,4 +1,4 @@
package org.schabi.newpipe; package org.schabi.newpipe.error;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -20,6 +20,9 @@ import androidx.preference.PreferenceManager;
import androidx.webkit.WebViewClientCompat; import androidx.webkit.WebViewClientCompat;
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding; import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;

View File

@ -1,4 +1,4 @@
package org.schabi.newpipe.report; package org.schabi.newpipe.error;
/** /**
* The user actions that can cause an error. * The user actions that can cause an error.
@ -6,9 +6,12 @@ package org.schabi.newpipe.report;
public enum UserAction { public enum UserAction {
USER_REPORT("user report"), USER_REPORT("user report"),
UI_ERROR("ui error"), 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"), LOAD_IMAGE("load image"),
SOMETHING_ELSE("something"), SOMETHING_ELSE("something else"),
SEARCHED("searched"), SEARCHED("searched"),
GET_SUGGESTIONS("get suggestions"), GET_SUGGESTIONS("get suggestions"),
REQUESTED_STREAM("requested stream"), REQUESTED_STREAM("requested stream"),
@ -17,11 +20,15 @@ public enum UserAction {
REQUESTED_KIOSK("requested kiosk"), REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"), REQUESTED_COMMENTS("requested comments"),
REQUESTED_FEED("requested feed"), REQUESTED_FEED("requested feed"),
REQUESTED_BOOKMARK("bookmark"),
DELETE_FROM_HISTORY("delete from history"), 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_POSTPROCESSING("download post-processing"),
DOWNLOAD_FAILED("download failed"), 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; private final String message;

View File

@ -1,48 +1,23 @@
package org.schabi.newpipe.fragments; package org.schabi.newpipe.fragments;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.jakewharton.rxbinding4.view.RxView;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.error.ErrorPanelHelper;
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.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.InfoCache; 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 java.util.concurrent.atomic.AtomicBoolean;
import icepick.State; import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animate;
@ -56,11 +31,10 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
@Nullable @Nullable
private ProgressBar loadingProgressBar; private ProgressBar loadingProgressBar;
private Disposable errorDisposable; private ErrorPanelHelper errorPanelHelper;
@Nullable
protected View errorPanelRoot; @State
private Button errorButtonRetry; protected ErrorInfo lastPanelError = null;
private TextView errorTextView;
@Override @Override
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) { public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
@ -74,12 +48,18 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
wasLoading.set(isLoading.get()); wasLoading.set(isLoading.get());
} }
@Override
public void onResume() {
super.onResume();
if (lastPanelError != null) {
showError(lastPanelError);
}
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (errorDisposable != null) { errorPanelHelper.dispose();
errorDisposable.dispose();
}
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -89,22 +69,9 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
@Override @Override
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
emptyStateView = rootView.findViewById(R.id.empty_state_view); emptyStateView = rootView.findViewById(R.id.empty_state_view);
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar); loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
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());
} }
protected void onRetryButtonClicked() { protected void onRetryButtonClicked() {
@ -143,7 +110,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (loadingProgressBar != null) { if (loadingProgressBar != null) {
animate(loadingProgressBar, true, 400); animate(loadingProgressBar, true, 400);
} }
animate(errorPanelRoot, false, 150); hideErrorPanel();
} }
@Override @Override
@ -154,10 +121,9 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (loadingProgressBar != null) { if (loadingProgressBar != null) {
animate(loadingProgressBar, false, 0); animate(loadingProgressBar, false, 0);
} }
animate(errorPanelRoot, false, 150); hideErrorPanel();
} }
@Override
public void showEmptyState() { public void showEmptyState() {
isLoading.set(false); isLoading.set(false);
if (emptyStateView != null) { if (emptyStateView != null) {
@ -166,26 +132,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (loadingProgressBar != null) { if (loadingProgressBar != null) {
animate(loadingProgressBar, false, 0); animate(loadingProgressBar, false, 0);
} }
animate(errorPanelRoot, false, 150); hideErrorPanel();
}
@Override
public void showError(final String message, final boolean showRetryButton) {
if (DEBUG) {
Log.d(TAG, "showError() called with: "
+ "message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
}
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 @Override
@ -196,138 +143,69 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
hideLoading(); hideLoading();
} }
@Override
public void handleError() {
isLoading.set(false);
InfoCache.getInstance().clearCache();
if (emptyStateView != null) {
animate(emptyStateView, false, 150);
}
if (loadingProgressBar != null) {
animate(loadingProgressBar, false, 0);
}
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Error handling // Error handling
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
/** public final void showError(final ErrorInfo errorInfo) {
* Default implementation handles some general exceptions. handleError();
*
* @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);
if (isDetached() || isRemoving()) { if (isDetached() || isRemoving()) {
if (DEBUG) { if (DEBUG) {
Log.w(TAG, "onError() is detached or removing = [" + exception + "]"); Log.w(TAG, "showError() is detached or removing = [" + errorInfo + "]");
} }
return true; return;
} }
if (ExceptionUtils.isInterruptedCaused(exception)) { errorPanelHelper.showError(errorInfo);
lastPanelError = errorInfo;
}
public final void showTextError(@NonNull final String errorString) {
handleError();
if (isDetached() || isRemoving()) {
if (DEBUG) { if (DEBUG) {
Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]"); Log.w(TAG, "showTextError() is detached or removing = [" + errorString + "]");
} }
return true; return;
} }
if (exception instanceof ReCaptchaException) { errorPanelHelper.showTextError(errorString);
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 false; public final void hideErrorPanel() {
errorPanelHelper.hide();
lastPanelError = null;
} }
public void onReCaptchaException(final ReCaptchaException exception) { public final boolean isErrorPanelVisible() {
if (DEBUG) { return errorPanelHelper.isVisible();
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);
showError(getString(R.string.recaptcha_request_toast), false);
}
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);
}
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 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);
} }
/** /**
* Show a SnackBar and only call * Show a SnackBar and only call
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorInfo)} * {@link ErrorActivity#reportErrorInSnackbar(androidx.fragment.app.Fragment, ErrorInfo)}
* IF we a find a valid view (otherwise the error screen appears). * IF we a find a valid view (otherwise the error screen appears).
* *
* @param exception List of the exceptions to show * @param errorInfo The error information
* @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
*/ */
public void showSnackBarError(final List<Throwable> exception, final UserAction userAction, public void showSnackBarError(final ErrorInfo errorInfo) {
final String serviceName, final String request,
@StringRes final int errorId) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "showSnackBarError() called with: " Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]");
+ "exception = [" + exception + "], userAction = [" + userAction + "], "
+ "request = [" + request + "], errorId = [" + errorId + "]");
} }
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null; ErrorActivity.reportErrorInSnackbar(this, errorInfo);
if (rootView == null && getView() != null) {
rootView = getView();
}
if (rootView == null) {
return;
}
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
ErrorInfo.make(userAction, serviceName, request, errorId));
} }
} }

View File

@ -11,9 +11,18 @@ import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
public class EmptyFragment extends BaseFragment { public class EmptyFragment extends BaseFragment {
final boolean showMessage;
public EmptyFragment(final boolean showMessage) {
this.showMessage = showMessage;
}
@Override @Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
final Bundle savedInstanceState) { final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_empty, container, false); final View view = inflater.inflate(R.layout.fragment_empty, container, false);
view.findViewById(R.id.empty_state_view).setVisibility(
showMessage ? View.VISIBLE : View.GONE);
return view;
} }
} }

View File

@ -14,7 +14,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround; import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
@ -25,10 +24,8 @@ import com.google.android.material.tabs.TabLayout;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentMainBinding; import org.schabi.newpipe.databinding.FragmentMainBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.tabs.Tab; import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager; import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
@ -128,7 +125,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @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); super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: " Log.d(TAG, "onCreateOptionsMenu() called with: "
@ -144,13 +142,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) { if (item.getItemId() == R.id.action_search) {
case R.id.action_search:
try { try {
NavigationHelper.openSearchFragment(getFM(), NavigationHelper.openSearchFragment(getFM(),
ServiceHelper.getSelectedServiceId(activity), ""); ServiceHelper.getSelectedServiceId(activity), "");
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); ErrorActivity.reportUiErrorInSnackbar(this, "Opening search fragment", e);
} }
return true; return true;
} }
@ -241,8 +238,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
} }
if (throwable != null) { if (throwable != null) {
ErrorActivity.reportError(context, throwable, null, null, ErrorInfo ErrorActivity.reportUiErrorInSnackbar(context, "Getting fragment item", throwable);
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
return new BlankFragment(); return new BlankFragment();
} }
@ -254,7 +250,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
} }
@Override @Override
public int getItemPosition(final Object object) { public int getItemPosition(@NonNull final Object object) {
// Causes adapter to reload all Fragments when // Causes adapter to reload all Fragments when
// notifyDataSetChanged is called // notifyDataSetChanged is called
return POSITION_NONE; return POSITION_NONE;

View File

@ -7,7 +7,7 @@ public interface ViewContract<I> {
void showEmptyState(); void showEmptyState();
void showError(String message, boolean showRetryButton);
void handleResult(I result); void handleResult(I result);
void handleError();
} }

View File

@ -37,12 +37,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
@ -56,14 +54,15 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene
import org.schabi.newpipe.App; import org.schabi.newpipe.App;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding; import org.schabi.newpipe.databinding.FragmentVideoDetailBinding;
import org.schabi.newpipe.download.DownloadDialog; 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.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; 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.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
@ -71,6 +70,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.EmptyFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment; import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.ktx.AnimationType;
@ -86,9 +86,6 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -151,6 +148,7 @@ public final class VideoDetailFragment
private static final String COMMENTS_TAB_TAG = "COMMENTS"; private static final String COMMENTS_TAB_TAG = "COMMENTS";
private static final String RELATED_TAB_TAG = "NEXT VIDEO"; private static final String RELATED_TAB_TAG = "NEXT VIDEO";
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB"; private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
// tabs // tabs
private boolean showComments; private boolean showComments;
@ -526,7 +524,7 @@ public final class VideoDetailFragment
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(), NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
subChannelUrl, subChannelName); subChannelUrl, subChannelName);
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
} }
} }
@ -684,13 +682,12 @@ public final class VideoDetailFragment
binding.detailThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); binding.detailThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!isEmpty(info.getThumbnailUrl())) { if (!isEmpty(info.getThumbnailUrl())) {
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
@Override @Override
public void onLoadingFailed(final String imageUri, final View view, public void onLoadingFailed(final String imageUri, final View view,
final FailReason failReason) { final FailReason failReason) {
showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE, showSnackBarError(new ErrorInfo(failReason.getCause(), UserAction.LOAD_IMAGE,
infoServiceName, imageUri, R.string.could_not_load_thumbnails); imageUri, info));
} }
}; };
@ -906,10 +903,8 @@ public final class VideoDetailFragment
openVideoPlayer(); openVideoPlayer();
} }
} }
}, throwable -> { }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
isLoading.set(false); url == null ? "no url" : url, serviceId)));
onError(throwable);
});
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -932,18 +927,22 @@ public final class VideoDetailFragment
} }
if (showRelatedStreams && binding.relatedStreamsLayout == null) { if (showRelatedStreams && binding.relatedStreamsLayout == null) {
//temp empty fragment. will be updated in handleResult // temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG); pageAdapter.addFragment(new EmptyFragment(false), RELATED_TAB_TAG);
tabIcons.add(R.drawable.ic_art_track_white_24dp); tabIcons.add(R.drawable.ic_art_track_white_24dp);
tabContentDescriptions.add(R.string.related_streams_tab_description); tabContentDescriptions.add(R.string.related_streams_tab_description);
} }
if (showDescription) { if (showDescription) {
// temp empty fragment. will be updated in handleResult // temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(new Fragment(), DESCRIPTION_TAB_TAG); pageAdapter.addFragment(new EmptyFragment(false), DESCRIPTION_TAB_TAG);
tabIcons.add(R.drawable.ic_description_white_24dp); tabIcons.add(R.drawable.ic_description_white_24dp);
tabContentDescriptions.add(R.string.description_tab_description); tabContentDescriptions.add(R.string.description_tab_description);
} }
if (pageAdapter.getCount() == 0) {
pageAdapter.addFragment(new EmptyFragment(true), EMPTY_TAB_TAG);
}
pageAdapter.notifyDataSetUpdate(); pageAdapter.notifyDataSetUpdate();
if (pageAdapter.getCount() >= 2) { if (pageAdapter.getCount() >= 2) {
@ -1327,8 +1326,8 @@ public final class VideoDetailFragment
} }
@Override @Override
public void showError(final String message, final boolean showRetryButton) { public void handleError() {
super.showError(message, showRetryButton); super.handleError();
setErrorImage(R.drawable.not_available_monkey); setErrorImage(R.drawable.not_available_monkey);
if (binding.relatedStreamsLayout != null) { // hide related streams for tablets if (binding.relatedStreamsLayout != null) { // hide related streams for tablets
@ -1341,8 +1340,8 @@ public final class VideoDetailFragment
} }
private void hideAgeRestrictedContent() { private void hideAgeRestrictedContent() {
showError(getString(R.string.restricted_video, showTextError(getString(R.string.restricted_video,
getString(R.string.show_age_restricted_content_title)), false); getString(R.string.show_age_restricted_content_title)));
} }
private void setupBroadcastReceiver() { private void setupBroadcastReceiver() {
@ -1548,11 +1547,8 @@ public final class VideoDetailFragment
} }
if (!info.getErrors().isEmpty()) { if (!info.getErrors().isEmpty()) {
showSnackBarError(info.getErrors(), showSnackBarError(new ErrorInfo(info.getErrors(),
UserAction.REQUESTED_STREAM, UserAction.REQUESTED_STREAM, info.getUrl(), info));
NewPipe.getNameOfService(info.getServiceId()),
info.getUrl(),
0);
} }
binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM
@ -1592,6 +1588,10 @@ public final class VideoDetailFragment
} }
public void openDownloadDialog() { public void openDownloadDialog() {
if (currentInfo == null) {
return;
}
try { try {
final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
downloadDialog.setVideoStreams(sortedVideoStreams); downloadDialog.setVideoStreams(sortedVideoStreams);
@ -1601,18 +1601,9 @@ public final class VideoDetailFragment
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (final Exception e) { } catch (final Exception e) {
final ErrorInfo info = ErrorInfo.make(UserAction.UI_ERROR, ErrorActivity.reportErrorInSnackbar(activity,
ServiceList.all() new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, "Showing download dialog",
.get(currentInfo currentInfo));
.getServiceId())
.getServiceInfo()
.getName(), "",
R.string.could_not_setup_download_menu);
ErrorActivity.reportError(activity,
e,
activity.getClass(),
activity.findViewById(android.R.id.content), info);
} }
} }
@ -1620,24 +1611,6 @@ public final class VideoDetailFragment
// Stream Results // 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) { private void updateProgressInfo(@NonNull final StreamInfo info) {
if (positionSubscriber != null) { if (positionSubscriber != null) {
positionSubscriber.dispose(); positionSubscriber.dispose();

View File

@ -14,7 +14,6 @@ import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -22,6 +21,7 @@ import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PignateFooterBinding; import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
@ -33,7 +33,6 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
@ -47,6 +46,7 @@ import java.util.List;
import java.util.Queue; import java.util.Queue;
import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N>, StateSaver.WriteRead, implements ListViewContract<I, N>, StateSaver.WriteRead,
@ -292,7 +292,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
selectedItem.getUrl(), selectedItem.getUrl(),
selectedItem.getName()); selectedItem.getName());
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); ErrorActivity.reportUiErrorInSnackbar(
BaseListFragment.this, "Opening channel fragment", e);
} }
} }
}); });
@ -307,7 +308,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
selectedItem.getUrl(), selectedItem.getUrl(),
selectedItem.getName()); selectedItem.getName());
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); ErrorActivity.reportUiErrorInSnackbar(BaseListFragment.this,
"Opening playlist fragment", e);
} }
} }
}); });
@ -406,23 +408,23 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
// Contract // Contract
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
animateHideRecyclerViewAllowingScrolling(itemsList);
}
@Override @Override
public void hideLoading() { public void hideLoading() {
super.hideLoading(); super.hideLoading();
animate(itemsList, true, 300); 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 @Override
public void showEmptyState() { public void showEmptyState() {
super.showEmptyState(); super.showEmptyState();
showListFooter(false); showListFooter(false);
animateHideRecyclerViewAllowingScrolling(itemsList);
} }
@Override @Override
@ -439,6 +441,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
isLoading.set(false); isLoading.set(false);
} }
@Override
public void handleError() {
super.handleError();
showListFooter(false);
animateHideRecyclerViewAllowingScrolling(itemsList);
}
@Override @Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String key) { final String key) {

View File

@ -7,12 +7,17 @@ import android.view.View;
import androidx.annotation.NonNull; 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.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.views.NewPipeRecyclerView; import org.schabi.newpipe.views.NewPipeRecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue; import java.util.Queue;
import icepick.State; import icepick.State;
@ -30,10 +35,15 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
@State @State
protected String url; protected String url;
private final UserAction errorUserAction;
protected I currentInfo; protected I currentInfo;
protected Page currentNextPage; protected Page currentNextPage;
protected Disposable currentWorker; protected Disposable currentWorker;
protected BaseListInfoFragment(final UserAction errorUserAction) {
this.errorUserAction = errorUserAction;
}
@Override @Override
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
@ -133,7 +143,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
currentInfo = result; currentInfo = result;
currentNextPage = result.getNextPage(); currentNextPage = result.getNextPage();
handleResult(result); handleResult(result);
}, this::onError); }, throwable ->
showError(new ErrorInfo(throwable, errorUserAction,
"Start loading: " + url, serviceId)));
} }
/** /**
@ -161,10 +173,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
.subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> { .subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
isLoading.set(false); isLoading.set(false);
handleNextItems(InfoItemsPage); handleNextItems(InfoItemsPage);
}, (@NonNull Throwable throwable) -> { }, (@NonNull Throwable throwable) ->
isLoading.set(false); dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
onError(throwable); errorUserAction, "Loading more items: " + url, serviceId)));
});
} }
private void forbidDownwardFocusScroll() { private void forbidDownwardFocusScroll() {
@ -182,10 +193,16 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
@Override @Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) { public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result); super.handleNextItems(result);
currentNextPage = result.getNextPage(); currentNextPage = result.getNextPage();
infoListAdapter.addInfoItemList(result.getItems()); infoListAdapter.addInfoItemList(result.getItems());
showListFooter(hasMoreItems()); showListFooter(hasMoreItems());
if (!result.getErrors().isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction,
"Get next items of: " + url, serviceId));
}
} }
@Override @Override
@ -213,6 +230,18 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
showEmptyState(); 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 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
this.url = u; this.url = u;
this.name = !TextUtils.isEmpty(title) ? title : ""; 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);
}
}
} }

View File

@ -16,7 +16,6 @@ import android.widget.Button;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.viewbinding.ViewBinding; import androidx.viewbinding.ViewBinding;
@ -27,20 +26,19 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.databinding.ChannelHeaderBinding; import org.schabi.newpipe.databinding.ChannelHeaderBinding;
import org.schabi.newpipe.databinding.FragmentChannelBinding; import org.schabi.newpipe.databinding.FragmentChannelBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding; 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.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; 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.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
@ -91,6 +89,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
return instance; return instance;
} }
public ChannelFragment() {
super(UserAction.REQUESTED_CHANNEL);
}
@Override @Override
public void setUserVisibleHint(final boolean isVisibleToUser) { public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser); super.setUserVisibleHint(isVisibleToUser);
@ -217,9 +219,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
private void monitorSubscription(final ChannelInfo info) { private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> { final Consumer<Throwable> onError = (Throwable throwable) -> {
animate(headerBinding.channelSubscribeButton, false, 100); animate(headerBinding.channelSubscribeButton, false, 100);
showSnackBarError(throwable, UserAction.SUBSCRIPTION, showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
NewPipe.getNameOfService(currentInfo.getServiceId()), "Get subscription status", currentInfo));
"Get subscription status", 0);
}; };
final Observable<List<SubscriptionEntity>> observable = subscriptionManager final Observable<List<SubscriptionEntity>> observable = subscriptionManager
@ -269,11 +270,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
}; };
final Consumer<Throwable> onError = (@NonNull Throwable throwable) -> final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
onUnrecoverableError(throwable, showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE,
UserAction.SUBSCRIPTION, "Updating subscription for " + info.getUrl(), info));
NewPipe.getNameOfService(info.getServiceId()),
"Updating Subscription for " + info.getUrl(),
R.string.subscription_update_failed);
disposables.add(subscriptionManager.updateChannelInfo(info) disposables.add(subscriptionManager.updateChannelInfo(info)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -290,11 +288,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
}; };
final Consumer<Throwable> onError = (@NonNull Throwable throwable) -> final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
onUnrecoverableError(throwable, showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_CHANGE,
UserAction.SUBSCRIPTION, "Changing subscription for " + currentInfo.getUrl(), currentInfo));
NewPipe.getNameOfService(currentInfo.getServiceId()),
"Subscription Change",
R.string.subscription_change_failed);
/* Emit clicks from main thread unto io thread */ /* Emit clicks from main thread unto io thread */
return RxView.clicks(subscribeButton) return RxView.clicks(subscribeButton)
@ -408,7 +403,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
currentInfo.getParentChannelUrl(), currentInfo.getParentChannelUrl(),
currentInfo.getParentChannelName()); currentInfo.getParentChannelName());
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
} }
} else if (DEBUG) { } else if (DEBUG) {
Log.i(TAG, "Can't open parent channel because we got no channel URL"); Log.i(TAG, "Can't open parent channel because we got no channel URL");
@ -469,27 +464,13 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
playlistControlBinding.getRoot().setVisibility(View.VISIBLE); playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
final List<Throwable> errors = new ArrayList<>(result.getErrors()); for (final Throwable throwable : 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) { if (throwable instanceof ContentNotSupportedException) {
showContentNotSupported(); showContentNotSupported();
} }
return throwable instanceof ContentNotSupportedException;
});
if (!errors.isEmpty()) {
showSnackBarError(errors, UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
} }
if (disposables != null) {
disposables.clear(); disposables.clear();
}
if (subscribeButtonMonitor != null) { if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose(); subscribeButtonMonitor.dispose();
} }
@ -539,38 +520,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
currentInfo.getNextPage(), streamItems, 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 // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View File

@ -11,12 +11,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
@ -32,6 +31,10 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
return instance; return instance;
} }
public CommentsFragment() {
super(UserAction.REQUESTED_COMMENTS);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// LifeCycle // LifeCycle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -67,52 +70,13 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
// Contract // Contract
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
}
@Override @Override
public void handleResult(@NonNull final CommentsInfo result) { public void handleResult(@NonNull final CommentsInfo result) {
super.handleResult(result); super.handleResult(result);
ViewUtils.slideUp(requireView(), 120, 150, 0.06f); 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(); 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 // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View File

@ -2,14 +2,16 @@ package org.schabi.newpipe.fragments.list.kiosk;
import android.os.Bundle; import android.os.Bundle;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.kiosk.KioskList; import org.schabi.newpipe.extractor.kiosk.KioskList;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
public class DefaultKioskFragment extends KioskFragment { public class DefaultKioskFragment extends KioskFragment {
@Override @Override
public void onCreate(final Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -46,8 +48,8 @@ public class DefaultKioskFragment extends KioskFragment {
currentInfo = null; currentInfo = null;
currentNextPage = null; currentNextPage = null;
} catch (final ExtractionException e) { } catch (final ExtractionException e) {
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none", showError(new ErrorInfo(e, UserAction.REQUESTED_KIOSK,
"Loading default kiosk from selected service", 0); "Loading default kiosk for selected service"));
} }
} }
} }

View File

@ -12,6 +12,8 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
@ -20,7 +22,6 @@ import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
@ -28,8 +29,6 @@ import org.schabi.newpipe.util.Localization;
import icepick.State; import icepick.State;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
/** /**
* Created by Christian Schabesberger on 23.09.17. * Created by Christian Schabesberger on 23.09.17.
* <p> * <p>
@ -82,6 +81,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
return instance; return instance;
} }
public KioskFragment() {
super(UserAction.REQUESTED_KIOSK);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// LifeCycle // LifeCycle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -102,9 +105,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
try { try {
setTitle(kioskTranslatedName); setTitle(kioskTranslatedName);
} catch (final Exception e) { } catch (final Exception e) {
onUnrecoverableError(e, UserAction.UI_ERROR, showSnackBarError(new ErrorInfo(e, UserAction.UI_ERROR, "Setting kiosk title"));
"none",
"none", R.string.app_ui_crash);
} }
} }
} }
@ -157,34 +158,11 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
// Contract // Contract
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
animate(itemsList, false, 100);
}
@Override @Override
public void handleResult(@NonNull final KioskInfo result) { public void handleResult(@NonNull final KioskInfo result) {
super.handleResult(result); super.handleResult(result);
name = kioskTranslatedName; name = kioskTranslatedName;
setTitle(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);
}
} }
} }

View File

@ -14,7 +14,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.viewbinding.ViewBinding; import androidx.viewbinding.ViewBinding;
@ -25,11 +24,12 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.databinding.PlaylistControlBinding; import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.databinding.PlaylistHeaderBinding; 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.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList; 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.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@ -40,8 +40,6 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.KoreUtil;
@ -62,6 +60,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
@ -87,6 +86,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
return instance; return instance;
} }
public PlaylistFragment() {
super(UserAction.REQUESTED_PLAYLIST);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// LifeCycle // LifeCycle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -262,7 +265,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
public void showLoading() { public void showLoading() {
super.showLoading(); super.showLoading();
animate(headerBinding.getRoot(), false, 200); animate(headerBinding.getRoot(), false, 200);
animate(itemsList, false, 100); animateHideRecyclerViewAllowingScrolling(itemsList);
IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView); IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView);
animate(headerBinding.uploaderLayout, false, 200); animate(headerBinding.uploaderLayout, false, 200);
@ -284,7 +287,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
NavigationHelper.openChannelFragment(getFM(), result.getServiceId(), NavigationHelper.openChannelFragment(getFM(), result.getServiceId(),
result.getUploaderUrl(), result.getUploaderName()); result.getUploaderUrl(), result.getUploaderName());
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
} }
}); });
} }
@ -315,8 +318,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
.localizeStreamCount(getContext(), result.getStreamCount())); .localizeStreamCount(getContext(), result.getStreamCount()));
if (!result.getErrors().isEmpty()) { if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); result.getUrl(), result));
} }
remotePlaylistManager.getPlaylist(result) remotePlaylistManager.getPlaylist(result)
@ -363,33 +366,6 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
); );
} }
@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 // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -434,8 +410,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
} }
@Override @Override
public void onError(final Throwable t) { public void onError(final Throwable throwable) {
PlaylistFragment.this.onError(t); showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Get playlist bookmarks"));
} }
@Override @Override
@ -460,12 +437,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
if (currentInfo != null && playlistEntity == null) { if (currentInfo != null && playlistEntity == null) {
action = remotePlaylistManager.onBookmark(currentInfo) action = remotePlaylistManager.onBookmark(currentInfo)
.observeOn(AndroidSchedulers.mainThread()) .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) { } else if (playlistEntity != null) {
action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doFinally(() -> playlistEntity = null) .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 { } else {
action = Disposable.empty(); action = Disposable.empty();
} }

View File

@ -35,16 +35,18 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.databinding.FragmentSearchBinding; import org.schabi.newpipe.databinding.FragmentSearchBinding;
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.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.MetaInfo; import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService; 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.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory; import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
@ -54,9 +56,6 @@ import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -162,11 +161,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
private EditText searchEditText; private EditText searchEditText;
private View searchClear; private View searchClear;
private TextView correctSuggestion;
private TextView metaInfoTextView;
private View metaInfoSeparator;
private View suggestionsPanel;
private boolean suggestionsPanelVisible = false; private boolean suggestionsPanelVisible = false;
/*////////////////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////////////////*/
@ -258,20 +252,23 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
try { try {
service = NewPipe.getService(serviceId); service = NewPipe.getService(serviceId);
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportError(getActivity(), e, requireActivity().getClass(), ErrorActivity.reportUiErrorInSnackbar(this,
requireActivity().findViewById(android.R.id.content), "Getting service for id " + serviceId, e);
ErrorInfo.make(UserAction.UI_ERROR, }
"",
"", R.string.general_error)); if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
initSuggestionObserver();
} }
if (!TextUtils.isEmpty(searchString)) { if (!TextUtils.isEmpty(searchString)) {
if (wasLoading.getAndSet(false)) { if (wasLoading.getAndSet(false)) {
search(searchString, contentFilter, sortFilter); search(searchString, contentFilter, sortFilter);
return;
} else if (infoListAdapter.getItemsList().isEmpty()) { } else if (infoListAdapter.getItemsList().isEmpty()) {
if (savedState == null) { if (savedState == null) {
search(searchString, contentFilter, sortFilter); search(searchString, contentFilter, sortFilter);
} else if (!isLoading.get() && !wasSearchFocused) { return;
} else if (!isLoading.get() && !wasSearchFocused && lastPanelError == null) {
infoListAdapter.clearStreamItemList(); infoListAdapter.clearStreamItemList();
showEmptyState(); showEmptyState();
} }
@ -281,11 +278,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
handleSearchSuggestion(); handleSearchSuggestion();
disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo), disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
metaInfoTextView, metaInfoSeparator)); searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator));
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
initSuggestionObserver();
}
if (TextUtils.isEmpty(searchString) || wasSearchFocused) { if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
showKeyboardSearch(); showKeyboardSearch();
@ -367,10 +360,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container); searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text); searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear); searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
correctSuggestion = rootView.findViewById(R.id.correct_suggestion);
metaInfoTextView = rootView.findViewById(R.id.search_meta_info_text_view);
metaInfoSeparator = rootView.findViewById(R.id.search_meta_info_separator);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -413,7 +402,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
searchEditText.setText(""); searchEditText.setText("");
showKeyboardSearch(); showKeyboardSearch();
} }
animate(errorPanelRoot, false, 200); hideErrorPanel();
} }
} }
@ -540,7 +529,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onClick() called with: v = [" + v + "]"); Log.d(TAG, "onClick() called with: v = [" + v + "]");
} }
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { if (isSuggestionsEnabled && !isErrorPanelVisible()) {
showSuggestionsPanel(); showSuggestionsPanel();
} }
if (DeviceUtils.isTv(getContext())) { if (DeviceUtils.isTv(getContext())) {
@ -553,8 +542,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "onFocusChange() called with: " Log.d(TAG, "onFocusChange() called with: "
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]"); + "v = [" + v + "], hasFocus = [" + hasFocus + "]");
} }
if (isSuggestionsEnabled && hasFocus if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) {
&& errorPanelRoot.getVisibility() != View.VISIBLE) {
showSuggestionsPanel(); showSuggestionsPanel();
} }
}); });
@ -704,9 +692,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.subscribe( .subscribe(
howManyDeleted -> suggestionPublisher howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()), .onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable, throwable -> showSnackBarError(new ErrorInfo(throwable,
UserAction.DELETE_FROM_HISTORY, "none", UserAction.DELETE_FROM_HISTORY,
"Deleting item failed", R.string.general_error)); "Deleting item failed")));
disposables.add(onDelete); disposables.add(onDelete);
}) })
.show(); .show();
@ -733,14 +721,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
suggestionDisposable.dispose(); suggestionDisposable.dispose();
} }
final Observable<String> observable = suggestionPublisher suggestionDisposable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWithItem(searchString != null .startWithItem(searchString != null
? searchString ? searchString
: "") : "")
.filter(ss -> isSuggestionsEnabled); .filter(ss -> isSuggestionsEnabled)
suggestionDisposable = observable
.switchMap(query -> { .switchMap(query -> {
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
.getRelatedSearches(query, 3, 25); .getRelatedSearches(query, 3, 25);
@ -763,8 +749,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.suggestionsFor(serviceId, query) .suggestionsFor(serviceId, query)
.onErrorReturn(throwable -> { .onErrorReturn(throwable -> {
if (!ExceptionUtils.isNetworkRelated(throwable)) { if (!ExceptionUtils.isNetworkRelated(throwable)) {
showSnackBarError(throwable, UserAction.GET_SUGGESTIONS, showSnackBarError(new ErrorInfo(throwable,
NewPipe.getNameOfService(serviceId), searchString, 0); UserAction.GET_SUGGESTIONS, searchString, serviceId));
} }
return new ArrayList<>(); return new ArrayList<>();
}) })
@ -800,7 +786,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (listNotification.isOnNext()) { if (listNotification.isOnNext()) {
handleSuggestions(listNotification.getValue()); handleSuggestions(listNotification.getValue());
} else if (listNotification.isOnError()) { } else if (listNotification.isOnError()) {
onSuggestionError(listNotification.getError()); showError(new ErrorInfo(listNotification.getError(),
UserAction.GET_SUGGESTIONS, searchString, serviceId));
} }
}); });
} }
@ -832,8 +819,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.subscribe(intent -> { .subscribe(intent -> {
getFM().popBackStackImmediate(); getFM().popBackStackImmediate();
activity.startActivity(intent); activity.startActivity(intent);
}, throwable -> }, throwable -> showTextError(getString(R.string.unsupported_url))));
showError(getString(R.string.unsupported_url), false)));
return; return;
} }
} catch (final Exception ignored) { } catch (final Exception ignored) {
@ -844,15 +830,16 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
this.searchString = theSearchString; this.searchString = theSearchString;
infoListAdapter.clearStreamItemList(); infoListAdapter.clearStreamItemList();
hideSuggestionsPanel(); hideSuggestionsPanel();
showMetaInfoInTextView(null, searchBinding.searchMetaInfoTextView,
searchBinding.searchMetaInfoSeparator);
hideKeyboardSearch(); hideKeyboardSearch();
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString) disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
ignored -> { ignored -> { },
}, throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
error -> showSnackBarError(error, UserAction.SEARCHED, theSearchString, serviceId))
NewPipe.getNameOfService(serviceId), theSearchString, 0)
)); ));
suggestionPublisher.onNext(theSearchString); suggestionPublisher.onNext(theSearchString);
startLoading(false); startLoading(false);
@ -872,7 +859,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnEvent((searchResult, throwable) -> isLoading.set(false)) .doOnEvent((searchResult, throwable) -> isLoading.set(false))
.subscribe(this::handleResult, this::onError); .subscribe(this::handleResult, this::onItemError);
} }
@ -895,7 +882,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false)) .doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
.subscribe(this::handleNextItems, this::onError); .subscribe(this::handleNextItems, this::onItemError);
} }
@Override @Override
@ -909,6 +896,15 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
hideKeyboardSearch(); 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 // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -945,26 +941,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
searchBinding.suggestionsList.smoothScrollToPosition(0); searchBinding.suggestionsList.smoothScrollToPosition(0);
searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions)); searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions));
if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) { if (suggestionsPanelVisible && isErrorPanelVisible()) {
hideLoading(); 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 // Contract
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -975,13 +956,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
showListFooter(false); showListFooter(false);
} }
@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
hideSuggestionsPanel();
hideKeyboardSearch();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Search Results // Search Results
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -992,8 +966,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (!exceptions.isEmpty() if (!exceptions.isEmpty()
&& !(exceptions.size() == 1 && !(exceptions.size() == 1
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) { && exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED, showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), searchString, 0); searchString, serviceId));
} }
searchSuggestion = result.getSearchSuggestion(); searchSuggestion = result.getSearchSuggestion();
@ -1002,8 +976,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// List<MetaInfo> cannot be bundled without creating some containers // List<MetaInfo> cannot be bundled without creating some containers
metaInfo = new MetaInfo[result.getMetaInfo().size()]; metaInfo = new MetaInfo[result.getMetaInfo().size()];
metaInfo = result.getMetaInfo().toArray(metaInfo); metaInfo = result.getMetaInfo().toArray(metaInfo);
disposables.add(showMetaInfoInTextView(result.getMetaInfo(), metaInfoTextView, disposables.add(showMetaInfoInTextView(result.getMetaInfo(),
metaInfoSeparator)); searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator));
handleSearchSuggestion(); handleSearchSuggestion();
@ -1061,33 +1035,20 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
nextPage = result.getNextPage(); nextPage = result.getNextPage();
if (!result.getErrors().isEmpty()) { if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED, showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId),
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", " "\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
+ "pageIds: " + nextPage.getIds() + ", " + "pageIds: " + nextPage.getIds() + ", "
+ "pageCookies: " + nextPage.getCookies(), 0); + "pageCookies: " + nextPage.getCookies(),
serviceId));
} }
super.handleNextItems(result); super.handleNextItems(result);
} }
@Override @Override
protected boolean onError(final Throwable exception) { public void handleError() {
if (super.onError(exception)) { super.handleError();
return true; hideSuggestionsPanel();
} hideKeyboardSearch();
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;
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -1113,9 +1074,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.subscribe( .subscribe(
howManyDeleted -> suggestionPublisher howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()), .onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable, throwable -> showSnackBarError(new ErrorInfo(throwable,
UserAction.DELETE_FROM_HISTORY, "none", UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
"Deleting item failed", R.string.general_error));
disposables.add(onDelete); disposables.add(onDelete);
} }
} }

View File

@ -16,12 +16,11 @@ import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.RelatedStreamsHeaderBinding; import org.schabi.newpipe.databinding.RelatedStreamsHeaderBinding;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.RelatedStreamInfo; import org.schabi.newpipe.util.RelatedStreamInfo;
import java.io.Serializable; import java.io.Serializable;
@ -47,6 +46,10 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
return instance; return instance;
} }
public RelatedVideosFragment() {
super(UserAction.REQUESTED_STREAM);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// LifeCycle // LifeCycle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -125,43 +128,9 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
} }
ViewUtils.slideUp(requireView(), 120, 96, 0.06f); 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(); 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 // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -190,13 +159,11 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
@Override @Override
protected void onRestoreInstanceState(@NonNull final Bundle savedState) { protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
super.onRestoreInstanceState(savedState); super.onRestoreInstanceState(savedState);
if (savedState != null) {
final Serializable serializable = savedState.getSerializable(INFO_KEY); final Serializable serializable = savedState.getSerializable(INFO_KEY);
if (serializable instanceof RelatedStreamInfo) { if (serializable instanceof RelatedStreamInfo) {
this.relatedStreamInfo = (RelatedStreamInfo) serializable; this.relatedStreamInfo = (RelatedStreamInfo) serializable;
} }
} }
}
@Override @Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,

View File

@ -14,11 +14,11 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.CommentTextOnTouchListener; import org.schabi.newpipe.util.CommentTextOnTouchListener;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.ImageDisplayConstants;
@ -171,15 +171,15 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
if (TextUtils.isEmpty(item.getUploaderUrl())) { if (TextUtils.isEmpty(item.getUploaderUrl())) {
return; return;
} }
try {
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext(); final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
try {
NavigationHelper.openChannelFragment( NavigationHelper.openChannelFragment(
activity.getSupportFragmentManager(), activity.getSupportFragmentManager(),
item.getServiceId(), item.getServiceId(),
item.getUploaderUrl(), item.getUploaderUrl(),
item.getUploaderName()); item.getUploaderName());
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e); ErrorActivity.reportUiErrorInSnackbar(activity, "Opening channel fragment", e);
} }
} }

View File

@ -319,6 +319,16 @@ fun View.slideUp(duration: Long, delay: Long, @FloatRange(from = 0.0, to = 1.0)
.start() .start()
} }
/**
* Instead of hiding normally using [animate], which would make
* the recycler view unable to capture touches after being hidden, this just animates the alpha
* value setting it to `0.0` after `200` milliseconds.
*/
fun View.animateHideRecyclerViewAllowingScrolling() {
// not hiding normally because the view needs to still capture touches and allow scroll
animate().alpha(0.0f).setDuration(200).start()
}
enum class AnimationType { enum class AnimationType {
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
} }

View File

@ -24,6 +24,7 @@ import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.list.ListViewContract; import org.schabi.newpipe.fragments.list.ListViewContract;
import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
/** /**
* This fragment is design to be used with persistent data such as * This fragment is design to be used with persistent data such as
@ -184,7 +185,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
public void showLoading() { public void showLoading() {
super.showLoading(); super.showLoading();
if (itemsList != null) { if (itemsList != null) {
animate(itemsList, false, 200); animateHideRecyclerViewAllowingScrolling(itemsList);
} }
if (headerRootBinding != null) { if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200); animate(headerRootBinding.getRoot(), false, 200);
@ -202,19 +203,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
} }
} }
@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 @Override
public void showEmptyState() { public void showEmptyState() {
super.showEmptyState(); super.showEmptyState();
@ -249,9 +237,18 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
} }
@Override @Override
protected boolean onError(final Throwable exception) { public void handleError() {
super.handleError();
resetFragment(); resetFragment();
return super.onError(exception);
showListFooter(false);
if (itemsList != null) {
animateHideRecyclerViewAllowingScrolling(itemsList);
}
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200);
}
} }
@Override @Override

View File

@ -23,10 +23,11 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistLocalItem; import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
@ -206,7 +207,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override @Override
public void onError(final Throwable exception) { public void onError(final Throwable exception) {
BookmarkFragment.this.onError(exception); showError(new ErrorInfo(exception,
UserAction.REQUESTED_BOOKMARK, "Loading playlists"));
} }
@Override @Override
@ -237,17 +239,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
// Fragment Error Handling // 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 @Override
protected void resetFragment() { protected void resetFragment() {
super.resetFragment(); super.resetFragment();
@ -295,8 +286,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
.setPositiveButton(R.string.delete, (dialog, i) -> .setPositiveButton(R.string.delete, (dialog, i) ->
disposables.add(deleteReactor disposables.add(deleteReactor
.observeOn(AndroidSchedulers.mainThread()) .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) .setNegativeButton(R.string.cancel, null)
.show(); .show();
} }
@ -314,7 +307,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
localPlaylistManager.renamePlaylist(id, name); localPlaylistManager.renamePlaylist(id, name);
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name) final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
.observeOn(AndroidSchedulers.mainThread()) .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); disposables.add(disposable);
} }
} }

View File

@ -38,17 +38,18 @@ import icepick.State
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.FragmentFeedBinding 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.fragments.list.BaseListFragment
import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
import org.schabi.newpipe.local.feed.service.FeedLoadService import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.report.UserAction
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import java.util.Calendar import java.util.Calendar
class FeedFragment : BaseListFragment<FeedState, Unit>() { class FeedFragment : BaseListFragment<FeedState, Unit>() {
private var _feedBinding: FragmentFeedBinding? = null private var _feedBinding: FragmentFeedBinding? = null
private val feedBinding get() = _feedBinding!! private val feedBinding get() = _feedBinding!!
private val errorBinding get() = _feedBinding!!.errorPanel
private lateinit var viewModel: FeedViewModel private lateinit var viewModel: FeedViewModel
@State @State
@ -106,7 +107,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun initListeners() { override fun initListeners() {
super.initListeners() super.initListeners()
feedBinding.refreshRootView.setOnClickListener { reloadContent() } feedBinding.refreshRootView.setOnClickListener { reloadContent() }
feedBinding.swiperefresh.setOnRefreshListener { reloadContent() } feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
} }
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
@ -171,50 +172,26 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
override fun showLoading() { override fun showLoading() {
super.showLoading()
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
feedBinding.refreshRootView.animate(false, 0) feedBinding.refreshRootView.animate(false, 0)
feedBinding.itemsList.animate(false, 0)
feedBinding.loadingProgressBar.animate(true, 200)
feedBinding.loadingProgressText.animate(true, 200) feedBinding.loadingProgressText.animate(true, 200)
feedBinding.swipeRefreshLayout.isRefreshing = true
feedBinding.emptyStateView.root.animate(false, 0)
errorBinding.root.animate(false, 0)
} }
override fun hideLoading() { override fun hideLoading() {
super.hideLoading()
feedBinding.refreshRootView.animate(true, 200) feedBinding.refreshRootView.animate(true, 200)
feedBinding.itemsList.animate(true, 300)
feedBinding.loadingProgressBar.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0) feedBinding.loadingProgressText.animate(false, 0)
feedBinding.swipeRefreshLayout.isRefreshing = false
feedBinding.emptyStateView.root.animate(false, 0)
errorBinding.root.animate(false, 0)
feedBinding.swiperefresh.isRefreshing = false
} }
override fun showEmptyState() { override fun showEmptyState() {
super.showEmptyState()
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
feedBinding.refreshRootView.animate(true, 200) feedBinding.refreshRootView.animate(true, 200)
feedBinding.itemsList.animate(false, 0)
feedBinding.loadingProgressBar.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0) feedBinding.loadingProgressText.animate(false, 0)
feedBinding.swipeRefreshLayout.isRefreshing = false
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) { override fun handleResult(result: FeedState) {
@ -227,6 +204,15 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
updateRefreshViewState() updateRefreshViewState()
} }
override fun handleError() {
super.handleError()
infoListAdapter.clearStreamItemList()
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
feedBinding.refreshRootView.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0)
feedBinding.swipeRefreshLayout.isRefreshing = false
}
private fun handleProgressState(progressState: FeedState.ProgressState) { private fun handleProgressState(progressState: FeedState.ProgressState) {
showLoading() showLoading()
@ -266,13 +252,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()) { if (loadedState.items.isEmpty()) {
showEmptyState() showEmptyState()
} else { } else {
@ -281,12 +260,13 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
} }
private fun handleErrorState(errorState: FeedState.ErrorState): Boolean { private fun handleErrorState(errorState: FeedState.ErrorState): Boolean {
return if (errorState.error == null) {
hideLoading() hideLoading()
errorState.error?.let { false
onError(errorState.error) } else {
return true showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed"))
true
} }
return false
} }
private fun updateRelativeTimeViews() { private fun updateRelativeTimeViews() {
@ -320,18 +300,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
listState = null 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 { companion object {
const val KEY_GROUP_ID = "ARG_GROUP_ID" const val KEY_GROUP_ID = "ARG_GROUP_ID"
const val KEY_GROUP_NAME = "ARG_GROUP_NAME" const val KEY_GROUP_NAME = "ARG_GROUP_NAME"

View File

@ -14,7 +14,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.viewbinding.ViewBinding; import androidx.viewbinding.ViewBinding;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
@ -27,6 +26,8 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.PlaylistControlBinding; import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding; import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoItemDialog;
@ -34,10 +35,7 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.settings.HistorySettingsFragment;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
@ -49,6 +47,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects;
import icepick.State; import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@ -163,47 +162,10 @@ public class StatisticsPlaylistFragment
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) { if (item.getItemId() == R.id.action_history_clear) {
case R.id.action_history_clear: HistorySettingsFragment
new AlertDialog.Builder(activity) .openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables);
.setTitle(R.string.delete_view_history_alert) } else {
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> {
final Disposable onDelete = recordManager.deleteWholeStreamHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getContext(),
R.string.watch_history_deleted,
Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(getContext(),
throwable,
SettingsActivity.class, null,
ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY,
"none",
"Delete view history",
R.string.general_error)));
final Disposable onClearOrphans = recordManager.removeOrphanedRecords()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> {
},
throwable -> ErrorActivity.reportError(getContext(),
throwable,
SettingsActivity.class, null,
ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY,
"none",
"Delete search history",
R.string.general_error)));
disposables.add(onClearOrphans);
disposables.add(onDelete);
}))
.create()
.show();
break;
default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
return true; return true;
@ -228,7 +190,7 @@ public class StatisticsPlaylistFragment
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); itemsListState = Objects.requireNonNull(itemsList.getLayoutManager()).onSaveInstanceState();
} }
@Override @Override
@ -287,7 +249,8 @@ public class StatisticsPlaylistFragment
@Override @Override
public void onError(final Throwable exception) { public void onError(final Throwable exception) {
StatisticsPlaylistFragment.this.onError(exception); showError(
new ErrorInfo(exception, UserAction.SOMETHING_ELSE, "History Statistics"));
} }
@Override @Override
@ -313,7 +276,7 @@ public class StatisticsPlaylistFragment
} }
itemListAdapter.addItems(processResult(result)); itemListAdapter.addItems(processResult(result));
if (itemsListState != null) { if (itemsListState != null && itemsList.getLayoutManager() != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null; itemsListState = null;
} }
@ -341,17 +304,6 @@ public class StatisticsPlaylistFragment
} }
} }
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "History Statistics", R.string.general_error);
return true;
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Utils // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -439,9 +391,8 @@ public class StatisticsPlaylistFragment
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
}, },
throwable -> showSnackBarError(throwable, throwable -> showSnackBarError(new ErrorInfo(throwable,
UserAction.DELETE_FROM_HISTORY, "none", UserAction.DELETE_FROM_HISTORY, "Deleting item")));
"Deleting item failed", R.string.general_error));
disposables.add(onDelete); disposables.add(onDelete);
} }

View File

@ -34,6 +34,8 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding; import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding; import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoItemDialog;
@ -42,7 +44,6 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
@ -110,7 +111,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override @Override
public void onCreate(final Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
debouncedSaveSignal = PublishSubject.create(); debouncedSaveSignal = PublishSubject.create();
disposables = new CompositeDisposable(); disposables = new CompositeDisposable();
@ -334,7 +335,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override @Override
public void onError(final Throwable exception) { public void onError(final Throwable exception) {
LocalPlaylistFragment.this.onError(exception); showError(new ErrorInfo(exception, UserAction.REQUESTED_BOOKMARK,
"Loading local playlist"));
} }
@Override @Override
@ -344,8 +346,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) { if (item.getItemId() == R.id.menu_item_remove_watched) {
case R.id.menu_item_remove_watched:
if (!isRemovingWatched) { if (!isRemovingWatched) {
new AlertDialog.Builder(requireContext()) new AlertDialog.Builder(requireContext())
.setMessage(R.string.remove_watched_popup_warning) .setMessage(R.string.remove_watched_popup_warning)
@ -360,8 +361,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.create() .create()
.show(); .show();
} }
break; } else {
default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
return true; return true;
@ -455,7 +455,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
hideLoading(); hideLoading();
isRemovingWatched = false; isRemovingWatched = false;
}, this::onError)); }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Removing watched videos, partially watched=" + removePartiallyWatched))));
} }
@Override @Override
@ -511,17 +512,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
} }
} }
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Local Playlist", R.string.general_error);
return true;
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Playlist Metadata/Streams Manipulation // Playlist Metadata/Streams Manipulation
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -562,7 +552,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final Disposable disposable = playlistManager.renamePlaylist(playlistId, title) final Disposable disposable = playlistManager.renamePlaylist(playlistId, title)
.observeOn(AndroidSchedulers.mainThread()) .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,
"Renaming playlist")));
disposables.add(disposable); disposables.add(disposable);
} }
@ -583,7 +575,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final Disposable disposable = playlistManager final Disposable disposable = playlistManager
.changePlaylistThumbnail(playlistId, thumbnailUrl) .changePlaylistThumbnail(playlistId, thumbnailUrl)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(ignore -> successToast.show(), this::onError); .subscribe(ignore -> successToast.show(), throwable ->
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Changing playlist thumbnail")));
disposables.add(disposable); disposables.add(disposable);
} }
@ -632,7 +626,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return debouncedSaveSignal return debouncedSaveSignal
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> saveImmediate(), this::onError); .subscribe(ignored -> saveImmediate(), throwable ->
showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
"Debounced saver")));
} }
private void saveImmediate() { private void saveImmediate() {
@ -669,7 +665,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
isModified.set(false); isModified.set(false);
} }
}, },
this::onError throwable -> showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
); );
disposables.add(disposable); disposables.add(disposable);
} }
@ -683,7 +680,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return new ItemTouchHelper.SimpleCallback(directions, return new ItemTouchHelper.SimpleCallback(directions,
ItemTouchHelper.ACTION_STATE_IDLE) { ItemTouchHelper.ACTION_STATE_IDLE) {
@Override @Override
public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView, public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
final int viewSize, final int viewSize,
final int viewSizeOutOfBounds, final int viewSizeOutOfBounds,
final int totalSize, final int totalSize,
@ -696,9 +693,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
} }
@Override @Override
public boolean onMove(final RecyclerView recyclerView, public boolean onMove(@NonNull final RecyclerView recyclerView,
final RecyclerView.ViewHolder source, @NonNull final RecyclerView.ViewHolder source,
final RecyclerView.ViewHolder target) { @NonNull final RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType() if (source.getItemViewType() != target.getItemViewType()
|| itemListAdapter == null) { || itemListAdapter == null) {
return false; return false;
@ -724,7 +721,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
} }
@Override @Override
public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) { } public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
final int swipeDir) { }
}; };
} }

View File

@ -34,6 +34,8 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogTitleBinding import org.schabi.newpipe.databinding.DialogTitleBinding
import org.schabi.newpipe.databinding.FeedItemCarouselBinding import org.schabi.newpipe.databinding.FeedItemCarouselBinding
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.channel.ChannelInfoItem import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.fragments.BaseStateFragment import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animate
@ -56,7 +58,6 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
import org.schabi.newpipe.report.UserAction
import org.schabi.newpipe.util.FilePickerActivityHelper import org.schabi.newpipe.util.FilePickerActivityHelper
import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.OnClickGesture
@ -288,8 +289,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
binding.itemsList.adapter = groupAdapter binding.itemsList.adapter = groupAdapter
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java) viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(this::handleResult) }) viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, { it?.let(this::handleFeedGroups) }) viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) }
} }
private fun showLongTapDialog(selectedItem: ChannelInfoItem) { private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
@ -381,7 +382,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
} }
} }
is SubscriptionState.ErrorState -> { is SubscriptionState.ErrorState -> {
result.error?.let { onError(result.error) } result.error?.let {
showError(ErrorInfo(result.error, UserAction.SOMETHING_ELSE, "Subscriptions"))
}
} }
} }
} }
@ -412,17 +415,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
binding.itemsList.animate(true, 200) binding.itemsList.animate(true, 200)
} }
// /////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
// /////////////////////////////////////////////////////////////////////////
override fun onError(exception: Throwable): Boolean {
if (super.onError(exception)) return true
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error)
return true
}
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
// Grid Mode // Grid Mode
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////

View File

@ -22,13 +22,13 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
@ -84,10 +84,12 @@ public class SubscriptionsImportFragment extends BaseFragment {
setupServiceVariables(); setupServiceVariables();
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) { if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
ErrorActivity.reportError(activity, Collections.emptyList(), null, null, ErrorActivity.reportErrorInSnackbar(activity,
ErrorInfo.make(UserAction.SOMETHING_ELSE, new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
NewPipe.getNameOfService(currentServiceId), NewPipe.getNameOfService(currentServiceId),
"Service don't support importing", R.string.general_error)); "Service does not support importing subscriptions",
R.string.general_error,
null));
activity.finish(); activity.finish();
} }
} }

View File

@ -42,7 +42,6 @@ import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
import org.schabi.newpipe.util.DeviceUtils import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import java.io.Serializable import java.io.Serializable
import kotlin.collections.contains
class FeedGroupDialog : DialogFragment(), BackPressable { class FeedGroupDialog : DialogFragment(), BackPressable {
private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null

View File

@ -24,6 +24,10 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import java.util.Collections import java.util.Collections
import kotlin.collections.ArrayList
import kotlin.collections.List
import kotlin.collections.map
import kotlin.collections.sortedBy
class FeedGroupReorderDialog : DialogFragment() { class FeedGroupReorderDialog : DialogFragment() {
private var _binding: DialogFeedGroupReorderBinding? = null private var _binding: DialogFeedGroupReorderBinding? = null

View File

@ -35,15 +35,14 @@ import androidx.core.app.ServiceCompat;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.util.Collections;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -152,13 +151,10 @@ public abstract class BaseImportExportService extends Service {
postErrorResult(null, null); postErrorResult(null, null);
} }
protected void stopAndReportError(@Nullable final Throwable error, final String request) { protected void stopAndReportError(final Throwable throwable, final String request) {
stopService(); stopService();
ErrorActivity.reportError(this, new ErrorInfo(
final ErrorInfo errorInfo = ErrorInfo throwable, UserAction.SUBSCRIPTION_IMPORT_EXPORT, request));
.make(UserAction.SUBSCRIPTION, "unknown", request, R.string.general_error);
ErrorActivity.reportError(this, error != null ? Collections.singletonList(error)
: Collections.emptyList(), null, null, errorInfo);
} }
protected void postErrorResult(final String title, final String text) { protected void postErrorResult(final String title, final String text) {

View File

@ -1,23 +0,0 @@
package org.schabi.newpipe.report
import android.os.Parcelable
import androidx.annotation.StringRes
import kotlinx.android.parcel.Parcelize
@Parcelize
class ErrorInfo(
val userAction: UserAction?,
val serviceName: String,
val request: String,
@field:StringRes @param:StringRes val message: Int
) : Parcelable {
companion object {
@JvmStatic
fun make(
userAction: UserAction?,
serviceName: String,
request: String,
@StringRes message: Int
) = ErrorInfo(userAction, serviceName, request, message)
}
}

View File

@ -21,13 +21,11 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.ZipHelper; import org.schabi.newpipe.util.ZipHelper;
@ -198,7 +196,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
} catch (final Exception e) { } catch (final Exception e) {
onError(e); ErrorActivity.reportUiErrorInSnackbar(this, "Exporting database", e);
} }
} }
@ -243,20 +241,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
System.exit(0); System.exit(0);
} }
} catch (final Exception e) { } catch (final Exception e) {
onError(e); ErrorActivity.reportUiErrorInSnackbar(this, "Importing database", e);
} }
} }
/*//////////////////////////////////////////////////////////////////////////
// Error
//////////////////////////////////////////////////////////////////////////*/
protected void onError(final Throwable e) {
final Activity activity = getActivity();
ErrorActivity.reportError(activity, e,
activity.getClass(),
null,
ErrorInfo.make(UserAction.UI_ERROR,
"none", "", R.string.app_ui_crash));
}
} }

View File

@ -1,17 +1,19 @@
package org.schabi.newpipe.settings; package org.schabi.newpipe.settings;
import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference; import androidx.preference.Preference;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.InfoCache;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@ -46,120 +48,103 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
public boolean onPreferenceTreeClick(final Preference preference) { public boolean onPreferenceTreeClick(final Preference preference) {
if (preference.getKey().equals(cacheWipeKey)) { if (preference.getKey().equals(cacheWipeKey)) {
InfoCache.getInstance().clearCache(); InfoCache.getInstance().clearCache();
Toast.makeText(preference.getContext(), R.string.metadata_cache_wipe_complete_notice, Toast.makeText(requireContext(),
Toast.LENGTH_SHORT).show(); R.string.metadata_cache_wipe_complete_notice, Toast.LENGTH_SHORT).show();
} else if (preference.getKey().equals(viewsHistoryClearKey)) {
openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables);
} else if (preference.getKey().equals(playbackStatesClearKey)) {
openDeletePlaybackStatesDialog(requireContext(), recordManager, disposables);
} else if (preference.getKey().equals(searchHistoryClearKey)) {
openDeleteSearchHistoryDialog(requireContext(), recordManager, disposables);
} else {
return super.onPreferenceTreeClick(preference);
}
return true;
} }
if (preference.getKey().equals(viewsHistoryClearKey)) { private static Disposable getDeletePlaybackStatesDisposable(
new AlertDialog.Builder(getActivity()) @NonNull final Context context, final HistoryRecordManager recordManager) {
return recordManager.deleteCompleteStreamStateHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(context,
R.string.watch_history_states_deleted, Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(context,
new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY,
"Delete playback states")));
}
private static Disposable getWholeStreamHistoryDisposable(
@NonNull final Context context, final HistoryRecordManager recordManager) {
return recordManager.deleteWholeStreamHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(context,
R.string.watch_history_deleted, Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(context,
new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY,
"Delete from history")));
}
private static Disposable getRemoveOrphanedRecordsDisposable(
@NonNull final Context context, final HistoryRecordManager recordManager) {
return recordManager.removeOrphanedRecords()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> { },
throwable -> ErrorActivity.reportError(context,
new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY,
"Clear orphaned records")));
}
private static Disposable getDeleteSearchHistoryDisposable(
@NonNull final Context context, final HistoryRecordManager recordManager) {
return recordManager.deleteCompleteSearchHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(context,
R.string.search_history_deleted, Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(context,
new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY,
"Delete search history")));
}
public static void openDeleteWatchHistoryDialog(@NonNull final Context context,
final HistoryRecordManager recordManager,
final CompositeDisposable disposables) {
new AlertDialog.Builder(context)
.setTitle(R.string.delete_view_history_alert) .setTitle(R.string.delete_view_history_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> { .setPositiveButton(R.string.delete, ((dialog, which) -> {
final Disposable onDeletePlaybackStates disposables.add(getDeletePlaybackStatesDisposable(context, recordManager));
= recordManager.deleteCompleteStreamStateHistory() disposables.add(getWholeStreamHistoryDisposable(context, recordManager));
.observeOn(AndroidSchedulers.mainThread()) disposables.add(getRemoveOrphanedRecordsDisposable(context, recordManager));
.subscribe(
howManyDeleted -> Toast.makeText(getActivity(),
R.string.watch_history_states_deleted,
Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(getContext(),
throwable,
SettingsActivity.class, null,
ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY,
"none",
"Delete playback states",
R.string.general_error)));
final Disposable onDelete = recordManager.deleteWholeStreamHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getActivity(),
R.string.watch_history_deleted,
Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(getContext(),
throwable,
SettingsActivity.class, null,
ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY,
"none",
"Delete view history",
R.string.general_error)));
final Disposable onClearOrphans = recordManager.removeOrphanedRecords()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> {
},
throwable -> ErrorActivity.reportError(getContext(),
throwable,
SettingsActivity.class, null,
ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY,
"none",
"Delete search history",
R.string.general_error)));
disposables.add(onDeletePlaybackStates);
disposables.add(onClearOrphans);
disposables.add(onDelete);
})) }))
.create() .create()
.show(); .show();
} }
if (preference.getKey().equals(playbackStatesClearKey)) { public static void openDeletePlaybackStatesDialog(@NonNull final Context context,
new AlertDialog.Builder(getActivity()) final HistoryRecordManager recordManager,
final CompositeDisposable disposables) {
new AlertDialog.Builder(context)
.setTitle(R.string.delete_playback_states_alert) .setTitle(R.string.delete_playback_states_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> { .setPositiveButton(R.string.delete, ((dialog, which) ->
disposables.add(getDeletePlaybackStatesDisposable(context, recordManager))))
final Disposable onDeletePlaybackStates
= recordManager.deleteCompleteStreamStateHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getActivity(),
R.string.watch_history_states_deleted,
Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(getContext(),
throwable,
SettingsActivity.class, null,
ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY,
"none",
"Delete playback states",
R.string.general_error)));
disposables.add(onDeletePlaybackStates);
}))
.create() .create()
.show(); .show();
} }
if (preference.getKey().equals(searchHistoryClearKey)) { public static void openDeleteSearchHistoryDialog(@NonNull final Context context,
new AlertDialog.Builder(getActivity()) final HistoryRecordManager recordManager,
final CompositeDisposable disposables) {
new AlertDialog.Builder(context)
.setTitle(R.string.delete_search_history_alert) .setTitle(R.string.delete_search_history_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> { .setPositiveButton(R.string.delete, ((dialog, which) ->
final Disposable onDelete = recordManager.deleteCompleteSearchHistory() disposables.add(getDeleteSearchHistoryDisposable(context, recordManager))))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getActivity(),
R.string.search_history_deleted,
Toast.LENGTH_SHORT).show(),
throwable -> ErrorActivity.reportError(getContext(),
throwable,
SettingsActivity.class, null,
ErrorInfo.make(
UserAction.DELETE_FROM_HISTORY,
"none",
"Delete search history",
R.string.general_error)));
disposables.add(onDelete);
}))
.create() .create()
.show(); .show();
} }
return super.onPreferenceTreeClick(preference);
}
} }

View File

@ -1,6 +1,5 @@
package org.schabi.newpipe.settings; package org.schabi.newpipe.settings;
import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -20,10 +19,8 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.util.List; import java.util.List;
@ -108,7 +105,7 @@ public class SelectChannelFragment extends DialogFragment {
emptyView.setVisibility(View.GONE); emptyView.setVisibility(View.GONE);
final SubscriptionManager subscriptionManager = new SubscriptionManager(getContext()); final SubscriptionManager subscriptionManager = new SubscriptionManager(requireContext());
subscriptionManager.subscriptions().toObservable() subscriptionManager.subscriptions().toObservable()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -122,7 +119,7 @@ public class SelectChannelFragment extends DialogFragment {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
public void onCancel(final DialogInterface dialogInterface) { public void onCancel(@NonNull final DialogInterface dialogInterface) {
super.onCancel(dialogInterface); super.onCancel(dialogInterface);
if (onCancelListener != null) { if (onCancelListener != null) {
onCancelListener.onCancel(); onCancelListener.onCancel();
@ -156,16 +153,17 @@ public class SelectChannelFragment extends DialogFragment {
private Observer<List<SubscriptionEntity>> getSubscriptionObserver() { private Observer<List<SubscriptionEntity>> getSubscriptionObserver() {
return new Observer<List<SubscriptionEntity>>() { return new Observer<List<SubscriptionEntity>>() {
@Override @Override
public void onSubscribe(final Disposable d) { } public void onSubscribe(@NonNull final Disposable disposable) { }
@Override @Override
public void onNext(final List<SubscriptionEntity> newSubscriptions) { public void onNext(@NonNull final List<SubscriptionEntity> newSubscriptions) {
displayChannels(newSubscriptions); displayChannels(newSubscriptions);
} }
@Override @Override
public void onError(final Throwable exception) { public void onError(@NonNull final Throwable exception) {
SelectChannelFragment.this.onError(exception); ErrorActivity.reportUiErrorInSnackbar(SelectChannelFragment.this,
"Loading subscription", exception);
} }
@Override @Override
@ -173,16 +171,6 @@ public class SelectChannelFragment extends DialogFragment {
}; };
} }
/*//////////////////////////////////////////////////////////////////////////
// Error
//////////////////////////////////////////////////////////////////////////*/
protected void onError(final Throwable e) {
final Activity activity = getActivity();
ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorInfo
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Interfaces // Interfaces
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -197,6 +185,7 @@ public class SelectChannelFragment extends DialogFragment {
private class SelectChannelAdapter private class SelectChannelAdapter
extends RecyclerView.Adapter<SelectChannelAdapter.SelectChannelItemHolder> { extends RecyclerView.Adapter<SelectChannelAdapter.SelectChannelItemHolder> {
@NonNull
@Override @Override
public SelectChannelItemHolder onCreateViewHolder(final ViewGroup parent, public SelectChannelItemHolder onCreateViewHolder(final ViewGroup parent,
final int viewType) { final int viewType) {

View File

@ -1,6 +1,5 @@
package org.schabi.newpipe.settings; package org.schabi.newpipe.settings;
import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -16,11 +15,9 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@ -83,7 +80,7 @@ public class SelectKioskFragment extends DialogFragment {
try { try {
selectKioskAdapter = new SelectKioskAdapter(); selectKioskAdapter = new SelectKioskAdapter();
} catch (final Exception e) { } catch (final Exception e) {
onError(e); ErrorActivity.reportUiErrorInSnackbar(this, "Selecting kiosk", e);
} }
recyclerView.setAdapter(selectKioskAdapter); recyclerView.setAdapter(selectKioskAdapter);
@ -109,16 +106,6 @@ public class SelectKioskFragment extends DialogFragment {
dismiss(); dismiss();
} }
/*//////////////////////////////////////////////////////////////////////////
// Error
//////////////////////////////////////////////////////////////////////////*/
protected void onError(final Throwable e) {
final Activity activity = getActivity();
ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorInfo
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Interfaces // Interfaces
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View File

@ -24,11 +24,11 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistLocalItem; import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import java.util.List; import java.util.List;
import java.util.Vector; import java.util.Vector;
@ -115,8 +115,8 @@ public class SelectPlaylistFragment extends DialogFragment {
protected void onError(final Throwable e) { protected void onError(final Throwable e) {
final Activity activity = requireActivity(); final Activity activity = requireActivity();
ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorInfo ErrorActivity.reportErrorInSnackbar(activity, new ErrorInfo(e,
.make(UserAction.UI_ERROR, "none", "load_playlists", R.string.app_ui_crash)); UserAction.UI_ERROR, "Loading playlists"));
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////

View File

@ -7,9 +7,9 @@ import android.util.Log;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.error.UserAction;
import static org.schabi.newpipe.MainActivity.DEBUG; import static org.schabi.newpipe.MainActivity.DEBUG;
@ -95,15 +95,13 @@ public final class SettingMigrations {
} catch (final Exception e) { } catch (final Exception e) {
// save the version with the last successful migration and report the error // save the version with the last successful migration and report the error
sp.edit().putInt(lastPrefVersionKey, currentVersion).apply(); sp.edit().putInt(lastPrefVersionKey, currentVersion).apply();
final ErrorInfo errorInfo = ErrorInfo.make( ErrorActivity.reportError(context, new ErrorInfo(
e,
UserAction.PREFERENCES_MIGRATION, UserAction.PREFERENCES_MIGRATION,
"none",
"Migrating preferences from version " + lastPrefVersion + " to " "Migrating preferences from version " + lastPrefVersion + " to "
+ VERSION + ". " + VERSION + ". "
+ "Error at " + currentVersion + " => " + ++currentVersion, + "Error at " + currentVersion + " => " + ++currentVersion
0 ));
);
ErrorActivity.reportError(context, e, SettingMigrations.class, null, errorInfo);
return; return;
} }
} }

View File

@ -27,10 +27,10 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SelectChannelFragment; import org.schabi.newpipe.settings.SelectChannelFragment;
import org.schabi.newpipe.settings.SelectKioskFragment; import org.schabi.newpipe.settings.SelectKioskFragment;
import org.schabi.newpipe.settings.SelectPlaylistFragment; import org.schabi.newpipe.settings.SelectPlaylistFragment;
@ -183,10 +183,9 @@ public class ChooseTabsFragment extends Fragment {
final Tab.Type type = typeFrom(tabId); final Tab.Type type = typeFrom(tabId);
if (type == null) { if (type == null) {
ErrorActivity.reportError(requireContext(), ErrorActivity.reportErrorInSnackbar(this,
new IllegalStateException("Tab id not found: " + tabId), null, null, new ErrorInfo(new IllegalStateException("Tab id not found: " + tabId),
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", UserAction.SOMETHING_ELSE, "Choosing tabs on settings"));
"Choosing tabs on settings", 0));
return; return;
} }

View File

@ -12,6 +12,9 @@ import com.grack.nanojson.JsonSink;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem.LocalItemType; import org.schabi.newpipe.database.LocalItem.LocalItemType;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@ -25,9 +28,6 @@ import org.schabi.newpipe.local.feed.FeedFragment;
import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@ -483,9 +483,8 @@ public abstract class Tab {
final StreamingService service = NewPipe.getService(kioskServiceId); final StreamingService service = NewPipe.getService(kioskServiceId);
kioskId = service.getKioskList().getDefaultKioskId(); kioskId = service.getKioskList().getDefaultKioskId();
} catch (final ExtractionException e) { } catch (final ExtractionException e) {
ErrorActivity.reportError(context, e, null, null, ErrorActivity.reportErrorInSnackbar(context, new ErrorInfo(e,
ErrorInfo.make(UserAction.REQUESTED_KIOSK, "none", UserAction.REQUESTED_KIOSK, "Loading default kiosk for selected service"));
"Loading default kiosk from selected service", 0));
} }
return kioskId; return kioskId;
} }

View File

@ -20,12 +20,9 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat; import androidx.core.text.HtmlCompat;
@ -33,7 +30,6 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
@ -44,29 +40,14 @@ import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo;
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.ParsingException;
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.feed.FeedExtractor; import org.schabi.newpipe.extractor.feed.FeedExtractor;
import org.schabi.newpipe.extractor.feed.FeedInfo; import org.schabi.newpipe.extractor.feed.FeedInfo;
import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -280,65 +261,6 @@ public final class ExtractorHelper {
return null != loadFromCache(serviceId, url, infoType).blockingGet(); return null != loadFromCache(serviceId, url, infoType).blockingGet();
} }
/**
* A simple and general error handler that show a Toast for known exceptions,
* and for others, opens the report error activity with the (optional) error message.
*
* @param context Android app context
* @param serviceId the service the exception happened in
* @param url the URL where the exception happened
* @param exception the exception to be handled
* @param userAction the action of the user that caused the exception
* @param optionalErrorMessage the optional error message
*/
public static void handleGeneralException(final Context context, final int serviceId,
final String url, final Throwable exception,
final UserAction userAction,
final String optionalErrorMessage) {
final Handler handler = new Handler(context.getMainLooper());
handler.post(() -> {
if (exception 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 (ExceptionUtils.isNetworkRelated(exception)) {
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
} else if (exception instanceof AgeRestrictedContentException) {
Toast.makeText(context, R.string.restricted_video_no_stream,
Toast.LENGTH_LONG).show();
} else if (exception instanceof GeographicRestrictionException) {
Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show();
} else if (exception instanceof PaidContentException) {
Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show();
} else if (exception instanceof PrivateContentException) {
Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show();
} else if (exception instanceof SoundCloudGoPlusContentException) {
Toast.makeText(context, R.string.soundcloud_go_plus_content,
Toast.LENGTH_LONG).show();
} else if (exception instanceof YoutubeMusicPremiumContentException) {
Toast.makeText(context, R.string.youtube_music_premium_content,
Toast.LENGTH_LONG).show();
} else if (exception instanceof ContentNotAvailableException) {
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
} else if (exception instanceof ContentNotSupportedException) {
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
} else {
final int errorId = exception instanceof YoutubeStreamExtractor.DeobfuscateException
? R.string.youtube_signature_deobfuscation_error
: exception instanceof ParsingException
? R.string.parsing_error : R.string.general_error;
ErrorActivity.reportError(handler, context, exception, MainActivity.class, null,
ErrorInfo.make(userAction, serviceId == -1 ? "none"
: NewPipe.getNameOfService(serviceId),
url + (optionalErrorMessage == null ? ""
: optionalErrorMessage), errorId));
}
});
}
/** /**
* Formats the text contained in the meta info list as HTML and puts it into the text view, * Formats the text contained in the meta info list as HTML and puts it into the text view,
* while also making the separator visible. If the list is null or empty, or the user chose not * while also making the separator visible. If the list is null or empty, or the user chose not
@ -352,10 +274,9 @@ public final class ExtractorHelper {
final TextView metaInfoTextView, final TextView metaInfoTextView,
final View metaInfoSeparator) { final View metaInfoSeparator) {
final Context context = metaInfoTextView.getContext(); final Context context = metaInfoTextView.getContext();
final boolean showMetaInfo = PreferenceManager.getDefaultSharedPreferences(context) if (metaInfos == null || metaInfos.isEmpty()
.getBoolean(context.getString(R.string.show_meta_info_key), true); || !PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
context.getString(R.string.show_meta_info_key), true)) {
if (!showMetaInfo || metaInfos == null || metaInfos.isEmpty()) {
metaInfoTextView.setVisibility(View.GONE); metaInfoTextView.setVisibility(View.GONE);
metaInfoSeparator.setVisibility(View.GONE); metaInfoSeparator.setVisibility(View.GONE);
return Disposable.empty(); return Disposable.empty();

View File

@ -41,9 +41,9 @@ import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ShareUtils;
@ -583,16 +583,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
try { try {
service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName(); service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName();
} catch (Exception e) { } catch (Exception e) {
service = "-"; service = ErrorInfo.SERVICE_NONE;
} }
ErrorActivity.reportError( ErrorActivity.reportError(mContext,
mContext, new ErrorInfo(ErrorInfo.Companion.throwableToStringList(mission.errObject), action,
mission.errObject, service, request.toString(), reason, null));
null,
null,
ErrorInfo.make(action, service, request.toString(), reason)
);
} }
public void clearFinishedDownloads(boolean delete) { public void clearFinishedDownloads(boolean delete) {

View File

@ -219,7 +219,7 @@
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/detail_title_root_layout" android:layout_below="@id/detail_title_root_layout"

View File

@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".report.ErrorActivity"> tools:context=".error.ErrorActivity">
<include <include
layout="@layout/toolbar_layout" layout="@layout/toolbar_layout"

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical" android:orientation="vertical"
@ -17,11 +17,24 @@
android:textStyle="bold" android:textStyle="bold"
tools:text="Network error" /> tools:text="Network error" />
<Button
android:id="@+id/error_button_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/error_snackbar_action"
android:textAlignment="center"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textSize="16sp"
android:theme="@style/ServiceColoredButton" />
<Button <Button
android:id="@+id/error_button_retry" android:id="@+id/error_button_retry"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:text="@string/retry" android:text="@string/retry"
android:textAlignment="center" android:textAlignment="center"
android:textAllCaps="true" android:textAllCaps="true"

View File

@ -8,7 +8,7 @@
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"

View File

@ -16,7 +16,7 @@
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"

View File

@ -63,7 +63,7 @@
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"

View File

@ -52,11 +52,11 @@
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerHorizontal="true"
android:layout_marginTop="50dp" android:layout_marginTop="16dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -14,16 +13,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="90dp" android:layout_marginTop="90dp" />
tools:visibility="visible" />
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<View
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_alignParentTop="true"
android:background="?attr/toolbar_shadow"
android:visibility="gone" />
</RelativeLayout> </RelativeLayout>

View File

@ -71,7 +71,7 @@
</RelativeLayout> </RelativeLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swiperefresh" android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_below="@+id/refresh_root_view"> android:layout_below="@+id/refresh_root_view">
@ -122,7 +122,7 @@
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"

View File

@ -53,7 +53,7 @@
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"

View File

@ -52,7 +52,7 @@
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"

View File

@ -52,7 +52,7 @@
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"

View File

@ -104,11 +104,10 @@
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerInParent="true"
android:layout_marginTop="50dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />

View File

@ -15,7 +15,7 @@
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/items_list" android:layout_below="@id/items_list"

View File

@ -206,7 +206,7 @@
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include
android:id="@+id/error_panel" android:id="@+id/error_panel"
layout="@layout/error_retry" layout="@layout/error_panel"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/detail_title_root_layout" android:layout_below="@id/detail_title_root_layout"

View File

@ -372,6 +372,7 @@
<string name="title_activity_recaptcha">reCAPTCHA challenge</string> <string name="title_activity_recaptcha">reCAPTCHA challenge</string>
<string name="subtitle_activity_recaptcha">Press \"Done\" when solved</string> <string name="subtitle_activity_recaptcha">Press \"Done\" when solved</string>
<string name="recaptcha_request_toast">reCAPTCHA challenge requested</string> <string name="recaptcha_request_toast">reCAPTCHA challenge requested</string>
<string name="recaptcha_solve">Solve</string>
<string name="recaptcha_done_button">Done</string> <string name="recaptcha_done_button">Done</string>
<!-- Downloads --> <!-- Downloads -->
<string name="settings_category_downloads_title">Download</string> <string name="settings_category_downloads_title">Download</string>

View File

@ -1,4 +1,4 @@
package org.schabi.newpipe.report; package org.schabi.newpipe.error;
import android.app.Activity; import android.app.Activity;

View File

@ -1,8 +1,7 @@
package org.schabi.newpipe package org.schabi.newpipe.error
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.schabi.newpipe.ReCaptchaActivity.YT_URL
class ReCaptchaActivityTest { class ReCaptchaActivityTest {
private fun assertSanitized(expected: String, actual: String?) { private fun assertSanitized(expected: String, actual: String?) {
@ -10,9 +9,9 @@ class ReCaptchaActivityTest {
} }
@Test fun `null, empty or blank url is sanitized correctly`() { @Test fun `null, empty or blank url is sanitized correctly`() {
assertSanitized(YT_URL, null) assertSanitized(ReCaptchaActivity.YT_URL, null)
assertSanitized(YT_URL, "") assertSanitized(ReCaptchaActivity.YT_URL, "")
assertSanitized(YT_URL, " \n \t ") assertSanitized(ReCaptchaActivity.YT_URL, " \n \t ")
} }
@Test fun `YouTube url containing pbj=1 is sanitized correctly`() { @Test fun `YouTube url containing pbj=1 is sanitized correctly`() {