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:label="@string/general_error"
android:theme="@android:style/Theme.NoDisplay" />
<activity android:name=".report.ErrorActivity" />
<activity android:name=".error.ErrorActivity" />
<!-- giga get related -->
<activity
@ -106,7 +106,7 @@
</activity>
<activity
android:name=".ReCaptchaActivity"
android:name=".error.ReCaptchaActivity"
android:label="@string/recaptcha" />
<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.CoreConfiguration;
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.downloader.Downloader;
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.util.Localization;
import org.schabi.newpipe.util.ServiceHelper;
@ -224,14 +225,10 @@ public class App extends MultiDexApplication {
.setBuildConfigClass(BuildConfig.class)
.build();
ACRA.init(this, acraConfig);
} catch (final ACRAConfigurationException ace) {
ace.printStackTrace();
ErrorActivity.reportError(this,
ace,
null,
null,
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not initialize ACRA crash report", R.string.app_ui_crash));
} catch (final ACRAConfigurationException exception) {
exception.printStackTrace();
ErrorActivity.reportError(this, new ErrorInfo(exception,
UserAction.SOMETHING_ELSE, "Could not initialize ACRA crash report"));
}
}

View File

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

View File

@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request;
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.InstanceSpinnerLayoutBinding;
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
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.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.KioskTranslator;
@ -153,7 +153,7 @@ public class MainActivity extends AppCompatActivity {
try {
setupDrawer();
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e);
}
if (DeviceUtils.isTv(this)) {
@ -238,7 +238,7 @@ public class MainActivity extends AppCompatActivity {
try {
tabSelected(item);
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiErrorInSnackbar(this, "Selecting main page tab", e);
}
break;
case R.id.menu_options_about_group:
@ -340,7 +340,7 @@ public class MainActivity extends AppCompatActivity {
try {
showTabs();
} 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(
getString(R.string.drawer_header_description) + selectedServiceName);
} catch (final Exception e) {
ErrorActivity.reportUiError(this, e);
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up service toggle", e);
}
final SharedPreferences sharedPreferences
@ -679,19 +679,16 @@ public class MainActivity extends AppCompatActivity {
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (DEBUG) {
Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
}
final int id = item.getItemId();
switch (id) {
case android.R.id.home:
onHomeButtonPressed();
return true;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == android.R.id.home) {
onHomeButtonPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
/*//////////////////////////////////////////////////////////////////////////
@ -799,7 +796,7 @@ public class MainActivity extends AppCompatActivity {
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
}
} 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.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.StreamingService.LinkType;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
@ -49,7 +63,6 @@ import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
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.
*/
public class RouterActivity extends AppCompatActivity {
public static final String INTERNAL_ROUTE_KEY = "internalRoute";
/**
* Removes invisible separators (\p{Z}) and punctuation characters including
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
* more details.
*/
private static final String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
protected final CompositeDisposable disposables = new CompositeDisposable();
@State
protected int currentServiceId = -1;
@ -100,7 +106,6 @@ public class RouterActivity extends AppCompatActivity {
protected int selectedRadioPosition = -1;
protected int selectedPreviously = -1;
protected String currentUrl;
protected boolean internalRoute = false;
private StreamingService currentService;
private boolean selectionIsDownload = false;
@ -123,7 +128,7 @@ public class RouterActivity extends AppCompatActivity {
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
@ -145,37 +150,79 @@ public class RouterActivity extends AppCompatActivity {
private void handleUrl(final String url) {
disposables.add(Observable
.fromCallable(() -> {
if (currentServiceId == -1) {
currentService = NewPipe.getServiceByUrl(url);
currentServiceId = currentService.getServiceId();
currentLinkType = currentService.getLinkTypeByUrl(url);
currentUrl = url;
} else {
currentService = NewPipe.getService(currentServiceId);
}
try {
if (currentServiceId == -1) {
currentService = NewPipe.getServiceByUrl(url);
currentServiceId = currentService.getServiceId();
currentLinkType = currentService.getLinkTypeByUrl(url);
currentUrl = url;
} else {
currentService = NewPipe.getService(currentServiceId);
}
return currentLinkType != LinkType.NONE;
// return whether the url was found to be supported or not
return currentLinkType != LinkType.NONE;
} catch (final ExtractionException e) {
// this can be reached only when the url is completely unsupported
return false;
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
if (result) {
.subscribe(isUrlSupported -> {
if (isUrlSupported) {
onSuccess();
} else {
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) {
showUnsupportedUrlDialog(url);
if (errorInfo.getThrowable() instanceof ReCaptchaException) {
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
final Intent intent = new Intent(context, ReCaptchaActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
} else if (errorInfo.getThrowable() != null
&& ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) {
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) {
Toast.makeText(context, R.string.restricted_video_no_stream,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) {
Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof PaidContentException) {
Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof PrivateContentException) {
Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) {
Toast.makeText(context, R.string.soundcloud_go_plus_content,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) {
Toast.makeText(context, R.string.youtube_music_premium_content,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) {
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
} else {
ExtractorHelper.handleGeneralException(this, -1, url, throwable,
UserAction.SOMETHING_ELSE, null);
finish();
ErrorActivity.reportError(context, errorInfo);
}
if (context instanceof RouterActivity) {
((RouterActivity) context).finish();
}
}
@ -500,7 +547,8 @@ public class RouterActivity extends AppCompatActivity {
.subscribe(intent -> {
startActivity(intent);
finish();
}, throwable -> handleError(throwable, currentUrl))
}, throwable -> handleError(this, new ErrorInfo(throwable,
UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl)))
);
return;
}
@ -580,6 +628,7 @@ public class RouterActivity extends AppCompatActivity {
this.playerChoice = playerChoice;
}
@NonNull
@Override
public String toString() {
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
@ -646,9 +695,9 @@ public class RouterActivity extends AppCompatActivity {
if (fetcher != null) {
fetcher.dispose();
}
}, throwable -> ExtractorHelper.handleGeneralException(this,
choice.serviceId, choice.url, throwable, finalUserAction,
", opened with " + choice.playerChoice));
}, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
choice.url + " opened with " + choice.playerChoice,
choice.serviceId)));
}
}

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.StreamStateEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import kotlin.jvm.Throws
data class PlaylistStreamEntry(
@Embedded

View File

@ -37,6 +37,9 @@ import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity;
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.NewPipe;
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.SubtitlesStream;
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.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.FilenameUtils;
@ -61,7 +61,6 @@ import org.schabi.newpipe.util.ThemeHelper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@ -591,17 +590,6 @@ public class DownloadDialog extends DialogFragment
.show();
}
private void showErrorActivity(final Exception e) {
ErrorActivity.reportError(
context,
Collections.singletonList(e),
null,
null,
ErrorInfo
.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
);
}
private void prepareSelectedDownload() {
final StoredDirectoryHelper mainStorage;
final MediaFormat format;
@ -705,7 +693,8 @@ public class DownloadDialog extends DialogFragment
mainStorage.getTag());
}
} catch (final Exception e) {
showErrorActivity(e);
ErrorActivity.reportErrorInSnackbar(this,
new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"));
return;
}

View File

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

View File

@ -1,4 +1,4 @@
package org.schabi.newpipe.report;
package org.schabi.newpipe.error;
import android.app.Activity;
import android.app.AlertDialog;
@ -8,7 +8,6 @@ import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
@ -18,14 +17,11 @@ import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NavUtils;
import androidx.fragment.app.Fragment;
import com.google.android.material.snackbar.Snackbar;
import com.grack.nanojson.JsonWriter;
import org.acra.ReportField;
import org.acra.data.CrashReportData;
import org.schabi.newpipe.ActivityCommunicator;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
@ -34,14 +30,9 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.Vector;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -70,108 +61,77 @@ public class ErrorActivity extends AppCompatActivity {
public static final String TAG = ErrorActivity.class.toString();
// BUNDLE TAGS
public static final String ERROR_INFO = "error_info";
public static final String ERROR_LIST = "error_list";
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT
= "Exception in NewPipe " + BuildConfig.VERSION_NAME;
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
public static final String ERROR_GITHUB_ISSUE_URL
= "https://github.com/TeamNewPipe/NewPipe/issues";
private String[] errorList;
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private ErrorInfo errorInfo;
private Class returnActivity;
private String currentTimeStamp;
private ActivityErrorBinding activityErrorBinding;
public static void reportUiError(final AppCompatActivity activity, final Throwable el) {
reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UserAction.UI_ERROR,
"none", "", R.string.app_ui_crash));
public static void reportError(final Context context, final ErrorInfo errorInfo) {
final Intent intent = new Intent(context, ErrorActivity.class);
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,
final Class returnActivity, final View rootView,
final ErrorInfo errorInfo) {
public static void reportErrorInSnackbar(final Context context, final ErrorInfo errorInfo) {
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) {
if (rootView != null) {
Snackbar.make(rootView, R.string.error_snackbar_message, 3 * 1000)
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
.setActionTextColor(Color.YELLOW)
.setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v ->
startErrorActivity(returnActivity, context, errorInfo, el)).show();
reportError(context, errorInfo)).show();
} 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,
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;
}
////////////////////////////////////////////////////////////////////////
// Activity lifecycle
////////////////////////////////////////////////////////////////////////
@Override
protected void onCreate(final Bundle savedInstanceState) {
@ -193,38 +153,28 @@ public class ErrorActivity extends AppCompatActivity {
actionBar.setDisplayShowTitleEnabled(true);
}
final ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
returnActivity = ac.getReturnActivity();
errorInfo = intent.getParcelableExtra(ERROR_INFO);
errorList = intent.getStringArrayExtra(ERROR_LIST);
// important add guru meditation
addGuruMeditation();
currentTimeStamp = getCurrentTimeStamp();
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "EMAIL"));
activityErrorBinding.errorReportCopyButton.setOnClickListener(v -> {
ShareUtils.copyToClipboard(this, buildMarkdown());
});
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
ShareUtils.copyToClipboard(this, buildMarkdown()));
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "GITHUB"));
// normal bugreport
buildInfo(errorInfo);
if (errorInfo.getMessage() != 0) {
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage());
} else {
activityErrorBinding.errorMessageView.setVisibility(View.GONE);
activityErrorBinding.messageWhatHappenedView.setVisibility(View.GONE);
}
activityErrorBinding.errorView.setText(formErrorText(errorList));
activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId());
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
// print stack trace once again for debugging:
for (final String e : errorList) {
for (final String e : errorInfo.getStackTraces()) {
Log.e(TAG, e);
}
}
@ -239,15 +189,14 @@ public class ErrorActivity extends AppCompatActivity {
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId();
switch (id) {
case android.R.id.home:
goToReturnActivity();
break;
case R.id.menu_item_share_error:
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
break;
if (id == android.R.id.home) {
onBackPressed();
} else if (id == R.id.menu_item_share_error) {
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
} else {
return false;
}
return false;
return true;
}
private void openPrivacyPolicyDialog(final Context context, final String action) {
@ -264,7 +213,9 @@ public class ErrorActivity extends AppCompatActivity {
final Intent i = new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse("mailto:")) // only email apps should handle this
.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());
if (i.resolveActivity(getPackageManager()) != null) {
ShareUtils.openIntentInApp(context, i);
@ -310,17 +261,6 @@ public class ErrorActivity extends AppCompatActivity {
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) {
String text = "";
@ -355,7 +295,7 @@ public class ErrorActivity extends AppCompatActivity {
.value("version", BuildConfig.VERSION_NAME)
.value("os", getOsString())
.value("time", currentTimeStamp)
.array("exceptions", Arrays.asList(errorList))
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
.toString())
.end()
@ -393,27 +333,27 @@ public class ErrorActivity extends AppCompatActivity {
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorList.length > 1) {
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport
.append("<details><summary><b>Exceptions (")
.append(errorList.length)
.append(errorInfo.getStackTraces().length)
.append(")</b></summary><p>\n");
}
// add the logs
for (int i = 0; i < errorList.length; i++) {
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
htmlErrorReport.append("<details><summary><b>Crash log ");
if (errorList.length > 1) {
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append(i + 1);
}
htmlErrorReport.append("</b>")
.append("</summary><p>\n")
.append("\n```\n").append(errorList[i]).append("\n```\n")
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
.append("</details>\n");
}
// make sure to close everything
if (errorList.length > 1) {
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append("</p></details>\n");
}
htmlErrorReport.append("<hr>\n");
@ -460,17 +400,4 @@ public class ErrorActivity extends AppCompatActivity {
text += "\n" + getString(R.string.guru_meditation);
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.SharedPreferences;
@ -20,6 +20,9 @@ import androidx.preference.PreferenceManager;
import androidx.webkit.WebViewClientCompat;
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 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.
@ -6,9 +6,12 @@ package org.schabi.newpipe.report;
public enum UserAction {
USER_REPORT("user report"),
UI_ERROR("ui error"),
SUBSCRIPTION("subscription"),
SUBSCRIPTION_CHANGE("subscription change"),
SUBSCRIPTION_UPDATE("subscription update"),
SUBSCRIPTION_GET("get subscription"),
SUBSCRIPTION_IMPORT_EXPORT("subscription import or export"),
LOAD_IMAGE("load image"),
SOMETHING_ELSE("something"),
SOMETHING_ELSE("something else"),
SEARCHED("searched"),
GET_SUGGESTIONS("get suggestions"),
REQUESTED_STREAM("requested stream"),
@ -17,11 +20,15 @@ public enum UserAction {
REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"),
REQUESTED_FEED("requested feed"),
REQUESTED_BOOKMARK("bookmark"),
DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("Play stream"),
PLAY_STREAM("play stream"),
DOWNLOAD_OPEN_DIALOG("download open dialog"),
DOWNLOAD_POSTPROCESSING("download post-processing"),
DOWNLOAD_FAILED("download failed"),
PREFERENCES_MIGRATION("migration of preferences");
PREFERENCES_MIGRATION("migration of preferences"),
SHARE_TO_NEWPIPE("share to newpipe"),
CHECK_FOR_NEW_APP_VERSION("check for new app version");
private final String message;

View File

@ -1,48 +1,23 @@
package org.schabi.newpipe.fragments;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.jakewharton.rxbinding4.view.RxView;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorPanelHelper;
import org.schabi.newpipe.util.InfoCache;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
@ -56,11 +31,10 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
@Nullable
private ProgressBar loadingProgressBar;
private Disposable errorDisposable;
protected View errorPanelRoot;
private Button errorButtonRetry;
private TextView errorTextView;
private ErrorPanelHelper errorPanelHelper;
@Nullable
@State
protected ErrorInfo lastPanelError = null;
@Override
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());
}
@Override
public void onResume() {
super.onResume();
if (lastPanelError != null) {
showError(lastPanelError);
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (errorDisposable != null) {
errorDisposable.dispose();
}
errorPanelHelper.dispose();
}
/*//////////////////////////////////////////////////////////////////////////
@ -89,22 +69,9 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
emptyStateView = rootView.findViewById(R.id.empty_state_view);
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
errorPanelRoot = rootView.findViewById(R.id.error_panel);
errorButtonRetry = rootView.findViewById(R.id.error_button_retry);
errorTextView = rootView.findViewById(R.id.error_message_view);
}
@Override
protected void initListeners() {
super.initListeners();
errorDisposable = RxView.clicks(errorButtonRetry)
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(o -> onRetryButtonClicked());
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
}
protected void onRetryButtonClicked() {
@ -143,7 +110,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (loadingProgressBar != null) {
animate(loadingProgressBar, true, 400);
}
animate(errorPanelRoot, false, 150);
hideErrorPanel();
}
@Override
@ -154,10 +121,9 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (loadingProgressBar != null) {
animate(loadingProgressBar, false, 0);
}
animate(errorPanelRoot, false, 150);
hideErrorPanel();
}
@Override
public void showEmptyState() {
isLoading.set(false);
if (emptyStateView != null) {
@ -166,26 +132,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (loadingProgressBar != null) {
animate(loadingProgressBar, false, 0);
}
animate(errorPanelRoot, false, 150);
}
@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);
hideErrorPanel();
}
@Override
@ -196,138 +143,69 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
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
//////////////////////////////////////////////////////////////////////////*/
/**
* Default implementation handles some general exceptions.
*
* @param exception The exception that should be handled
* @return If the exception was handled
*/
protected boolean onError(final Throwable exception) {
if (DEBUG) {
Log.d(TAG, "onError() called with: exception = [" + exception + "]");
}
isLoading.set(false);
public final void showError(final ErrorInfo errorInfo) {
handleError();
if (isDetached() || isRemoving()) {
if (DEBUG) {
Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
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) {
Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
Log.w(TAG, "showTextError() is detached or removing = [" + errorString + "]");
}
return true;
return;
}
if (exception instanceof ReCaptchaException) {
onReCaptchaException((ReCaptchaException) exception);
return true;
} else if (ExceptionUtils.isNetworkRelated(exception)) {
showError(getString(R.string.network_error), true);
return true;
} else if (exception instanceof AgeRestrictedContentException) {
showError(getString(R.string.restricted_video_no_stream), false);
return true;
} else if (exception instanceof GeographicRestrictionException) {
showError(getString(R.string.georestricted_content), false);
return true;
} else if (exception instanceof PaidContentException) {
showError(getString(R.string.paid_content), false);
return true;
} else if (exception instanceof PrivateContentException) {
showError(getString(R.string.private_content), false);
return true;
} else if (exception instanceof SoundCloudGoPlusContentException) {
showError(getString(R.string.soundcloud_go_plus_content), false);
return true;
} else if (exception instanceof YoutubeMusicPremiumContentException) {
showError(getString(R.string.youtube_music_premium_content), false);
return true;
} else if (exception instanceof ContentNotAvailableException) {
showError(getString(R.string.content_not_available), false);
return true;
} else if (exception instanceof ContentNotSupportedException) {
showError(getString(R.string.content_not_supported), false);
return true;
}
return false;
errorPanelHelper.showTextError(errorString);
}
public void onReCaptchaException(final ReCaptchaException exception) {
if (DEBUG) {
Log.d(TAG, "onReCaptchaException() called");
}
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
final Intent intent = new Intent(activity, ReCaptchaActivity.class);
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, exception.getUrl());
startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST);
showError(getString(R.string.recaptcha_request_toast), false);
public final void hideErrorPanel() {
errorPanelHelper.hide();
lastPanelError = null;
}
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);
public final boolean isErrorPanelVisible() {
return errorPanelHelper.isVisible();
}
/**
* 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).
*
* @param exception List of the exceptions to show
* @param userAction The user action that caused the exception
* @param serviceName The service where the exception happened
* @param request The page that was requested
* @param errorId The ID of the error
* @param errorInfo The error information
*/
public void showSnackBarError(final List<Throwable> exception, final UserAction userAction,
final String serviceName, final String request,
@StringRes final int errorId) {
public void showSnackBarError(final ErrorInfo errorInfo) {
if (DEBUG) {
Log.d(TAG, "showSnackBarError() called with: "
+ "exception = [" + exception + "], userAction = [" + userAction + "], "
+ "request = [" + request + "], errorId = [" + errorId + "]");
Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]");
}
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null;
if (rootView == null && getView() != null) {
rootView = getView();
}
if (rootView == null) {
return;
}
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
ErrorInfo.make(userAction, serviceName, request, errorId));
ErrorActivity.reportErrorInSnackbar(this, errorInfo);
}
}

View File

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

View File

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

View File

@ -37,12 +37,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
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.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding;
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.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
@ -71,6 +70,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.BackPressable;
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.videos.RelatedVideosFragment;
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.PlayQueueItem;
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.DeviceUtils;
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 RELATED_TAB_TAG = "NEXT VIDEO";
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
// tabs
private boolean showComments;
@ -526,7 +524,7 @@ public final class VideoDetailFragment
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
subChannelUrl, subChannelName);
} 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);
if (!isEmpty(info.getThumbnailUrl())) {
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
@Override
public void onLoadingFailed(final String imageUri, final View view,
final FailReason failReason) {
showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE,
infoServiceName, imageUri, R.string.could_not_load_thumbnails);
showSnackBarError(new ErrorInfo(failReason.getCause(), UserAction.LOAD_IMAGE,
imageUri, info));
}
};
@ -906,10 +903,8 @@ public final class VideoDetailFragment
openVideoPlayer();
}
}
}, throwable -> {
isLoading.set(false);
onError(throwable);
});
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
url == null ? "no url" : url, serviceId)));
}
/*//////////////////////////////////////////////////////////////////////////
@ -932,18 +927,22 @@ public final class VideoDetailFragment
}
if (showRelatedStreams && binding.relatedStreamsLayout == null) {
//temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG);
// temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(new EmptyFragment(false), RELATED_TAB_TAG);
tabIcons.add(R.drawable.ic_art_track_white_24dp);
tabContentDescriptions.add(R.string.related_streams_tab_description);
}
if (showDescription) {
// 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);
tabContentDescriptions.add(R.string.description_tab_description);
}
if (pageAdapter.getCount() == 0) {
pageAdapter.addFragment(new EmptyFragment(true), EMPTY_TAB_TAG);
}
pageAdapter.notifyDataSetUpdate();
if (pageAdapter.getCount() >= 2) {
@ -1327,8 +1326,8 @@ public final class VideoDetailFragment
}
@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
public void handleError() {
super.handleError();
setErrorImage(R.drawable.not_available_monkey);
if (binding.relatedStreamsLayout != null) { // hide related streams for tablets
@ -1341,8 +1340,8 @@ public final class VideoDetailFragment
}
private void hideAgeRestrictedContent() {
showError(getString(R.string.restricted_video,
getString(R.string.show_age_restricted_content_title)), false);
showTextError(getString(R.string.restricted_video,
getString(R.string.show_age_restricted_content_title)));
}
private void setupBroadcastReceiver() {
@ -1548,11 +1547,8 @@ public final class VideoDetailFragment
}
if (!info.getErrors().isEmpty()) {
showSnackBarError(info.getErrors(),
UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(info.getServiceId()),
info.getUrl(),
0);
showSnackBarError(new ErrorInfo(info.getErrors(),
UserAction.REQUESTED_STREAM, info.getUrl(), info));
}
binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM
@ -1592,6 +1588,10 @@ public final class VideoDetailFragment
}
public void openDownloadDialog() {
if (currentInfo == null) {
return;
}
try {
final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
downloadDialog.setVideoStreams(sortedVideoStreams);
@ -1601,18 +1601,9 @@ public final class VideoDetailFragment
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (final Exception e) {
final ErrorInfo info = ErrorInfo.make(UserAction.UI_ERROR,
ServiceList.all()
.get(currentInfo
.getServiceId())
.getServiceInfo()
.getName(), "",
R.string.could_not_setup_download_menu);
ErrorActivity.reportError(activity,
e,
activity.getClass(),
activity.findViewById(android.R.id.content), info);
ErrorActivity.reportErrorInSnackbar(activity,
new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, "Showing download dialog",
currentInfo));
}
}
@ -1620,24 +1611,6 @@ public final class VideoDetailFragment
// Stream Results
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
final int errorId = exception instanceof YoutubeStreamExtractor.DeobfuscateException
? R.string.youtube_signature_deobfuscation_error
: exception instanceof ExtractionException
? R.string.parsing_error
: R.string.general_error;
onUnrecoverableError(exception, UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId), url, errorId);
return true;
}
private void updateProgressInfo(@NonNull final StreamInfo info) {
if (positionSubscriber != null) {
positionSubscriber.dispose();

View File

@ -14,7 +14,6 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -22,6 +21,7 @@ import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
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.InfoListAdapter;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@ -47,6 +46,7 @@ import java.util.List;
import java.util.Queue;
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>
implements ListViewContract<I, N>, StateSaver.WriteRead,
@ -292,7 +292,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
selectedItem.getUrl(),
selectedItem.getName());
} 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.getName());
} 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
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
animateHideRecyclerViewAllowingScrolling(itemsList);
}
@Override
public void hideLoading() {
super.hideLoading();
animate(itemsList, true, 300);
}
@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);
animate(itemsList, false, 200);
}
@Override
public void showEmptyState() {
super.showEmptyState();
showListFooter(false);
animateHideRecyclerViewAllowingScrolling(itemsList);
}
@Override
@ -439,6 +441,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
isLoading.set(false);
}
@Override
public void handleError() {
super.handleError();
showListFooter(false);
animateHideRecyclerViewAllowingScrolling(itemsList);
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String key) {

View File

@ -7,12 +7,17 @@ import android.view.View;
import androidx.annotation.NonNull;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.views.NewPipeRecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import icepick.State;
@ -30,10 +35,15 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
@State
protected String url;
private final UserAction errorUserAction;
protected I currentInfo;
protected Page currentNextPage;
protected Disposable currentWorker;
protected BaseListInfoFragment(final UserAction errorUserAction) {
this.errorUserAction = errorUserAction;
}
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
@ -133,7 +143,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
currentInfo = result;
currentNextPage = result.getNextPage();
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) -> {
isLoading.set(false);
handleNextItems(InfoItemsPage);
}, (@NonNull Throwable throwable) -> {
isLoading.set(false);
onError(throwable);
});
}, (@NonNull Throwable throwable) ->
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
errorUserAction, "Loading more items: " + url, serviceId)));
}
private void forbidDownwardFocusScroll() {
@ -182,10 +193,16 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
currentNextPage = result.getNextPage();
infoListAdapter.addInfoItemList(result.getItems());
showListFooter(hasMoreItems());
if (!result.getErrors().isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction,
"Get next items of: " + url, serviceId));
}
}
@Override
@ -213,6 +230,18 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
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.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.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.viewbinding.ViewBinding;
@ -27,20 +26,19 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
import org.schabi.newpipe.databinding.FragmentChannelBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
@ -91,6 +89,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
return instance;
}
public ChannelFragment() {
super(UserAction.REQUESTED_CHANNEL);
}
@Override
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
@ -217,9 +219,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> {
animate(headerBinding.channelSubscribeButton, false, 100);
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(currentInfo.getServiceId()),
"Get subscription status", 0);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
"Get subscription status", currentInfo));
};
final Observable<List<SubscriptionEntity>> observable = subscriptionManager
@ -269,11 +270,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
};
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
onUnrecoverableError(throwable,
UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(info.getServiceId()),
"Updating Subscription for " + info.getUrl(),
R.string.subscription_update_failed);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE,
"Updating subscription for " + info.getUrl(), info));
disposables.add(subscriptionManager.updateChannelInfo(info)
.subscribeOn(Schedulers.io())
@ -290,11 +288,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
};
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
onUnrecoverableError(throwable,
UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(currentInfo.getServiceId()),
"Subscription Change",
R.string.subscription_change_failed);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_CHANGE,
"Changing subscription for " + currentInfo.getUrl(), currentInfo));
/* Emit clicks from main thread unto io thread */
return RxView.clicks(subscribeButton)
@ -408,7 +403,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
currentInfo.getParentChannelUrl(),
currentInfo.getParentChannelName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
}
} else if (DEBUG) {
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);
final List<Throwable> errors = new ArrayList<>(result.getErrors());
if (!errors.isEmpty()) {
// handling ContentNotSupportedException not to show the error but an appropriate string
// so that crashes won't be sent uselessly and the user will understand what happened
errors.removeIf(throwable -> {
if (throwable instanceof ContentNotSupportedException) {
showContentNotSupported();
}
return throwable instanceof ContentNotSupportedException;
});
if (!errors.isEmpty()) {
showSnackBarError(errors, UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
for (final Throwable throwable : result.getErrors()) {
if (throwable instanceof ContentNotSupportedException) {
showContentNotSupported();
}
}
if (disposables != null) {
disposables.clear();
}
disposables.clear();
if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose();
}
@ -539,38 +520,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
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
//////////////////////////////////////////////////////////////////////////*/

View File

@ -11,12 +11,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import io.reactivex.rxjava3.core.Single;
@ -25,13 +24,17 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
private final CompositeDisposable disposables = new CompositeDisposable();
public static CommentsFragment getInstance(final int serviceId, final String url,
public static CommentsFragment getInstance(final int serviceId, final String url,
final String name) {
final CommentsFragment instance = new CommentsFragment();
instance.setInitialData(serviceId, url, name);
return instance;
}
public CommentsFragment() {
super(UserAction.REQUESTED_COMMENTS);
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@ -67,52 +70,13 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
}
@Override
public void handleResult(@NonNull final CommentsInfo result) {
super.handleResult(result);
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
disposables.clear();
}
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId), "Get next page of: " + url,
R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/

View File

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

View File

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

View File

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

View File

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

View File

@ -16,12 +16,11 @@ import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.RelatedStreamsHeaderBinding;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.RelatedStreamInfo;
import java.io.Serializable;
@ -47,6 +46,10 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
return instance;
}
public RelatedVideosFragment() {
super(UserAction.REQUESTED_STREAM);
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@ -125,43 +128,9 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
}
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
disposables.clear();
}
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId),
"Get next page of: " + url,
R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId), url, R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@ -190,11 +159,9 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
super.onRestoreInstanceState(savedState);
if (savedState != null) {
final Serializable serializable = savedState.getSerializable(INFO_KEY);
if (serializable instanceof RelatedStreamInfo) {
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
}
final Serializable serializable = savedState.getSerializable(INFO_KEY);
if (serializable instanceof RelatedStreamInfo) {
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
}
}

View File

@ -14,11 +14,11 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.CommentTextOnTouchListener;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
@ -171,15 +171,15 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
if (TextUtils.isEmpty(item.getUploaderUrl())) {
return;
}
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
try {
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
NavigationHelper.openChannelFragment(
activity.getSupportFragmentManager(),
item.getServiceId(),
item.getUploaderUrl(),
item.getUploaderName());
} catch (final Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e);
ErrorActivity.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()
}
/**
* 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 {
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 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
@ -184,7 +185,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
public void showLoading() {
super.showLoading();
if (itemsList != null) {
animate(itemsList, false, 200);
animateHideRecyclerViewAllowingScrolling(itemsList);
}
if (headerRootBinding != null) {
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
public void showEmptyState() {
super.showEmptyState();
@ -249,9 +237,18 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
}
@Override
protected boolean onError(final Throwable exception) {
public void handleError() {
super.handleError();
resetFragment();
return super.onError(exception);
showListFooter(false);
if (itemsList != null) {
animateHideRecyclerViewAllowingScrolling(itemsList);
}
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200);
}
}
@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.PlaylistMetadataEntry;
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.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@ -206,7 +207,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override
public void onError(final Throwable exception) {
BookmarkFragment.this.onError(exception);
showError(new ErrorInfo(exception,
UserAction.REQUESTED_BOOKMARK, "Loading playlists"));
}
@Override
@ -237,17 +239,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected boolean onError(final Throwable exception) {
if (super.onError(exception)) {
return true;
}
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Bookmark", R.string.general_error);
return true;
}
@Override
protected void resetFragment() {
super.resetFragment();
@ -295,8 +286,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
.setPositiveButton(R.string.delete, (dialog, i) ->
disposables.add(deleteReactor
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> { /*Do nothing on success*/ }, this::onError))
)
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Deleting playlist")))))
.setNegativeButton(R.string.cancel, null)
.show();
}
@ -314,7 +307,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
localPlaylistManager.renamePlaylist(id, name);
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> { /*Do nothing on success*/ }, this::onError);
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Changing playlist name")));
disposables.add(disposable);
}
}

View File

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

View File

@ -14,7 +14,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.viewbinding.ViewBinding;
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.databinding.PlaylistControlBinding;
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.StreamType;
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.playqueue.PlayQueue;
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.settings.SettingsActivity;
import org.schabi.newpipe.settings.HistorySettingsFragment;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@ -49,6 +47,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@ -163,48 +162,11 @@ public class StatisticsPlaylistFragment
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_history_clear:
new AlertDialog.Builder(activity)
.setTitle(R.string.delete_view_history_alert)
.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);
if (item.getItemId() == R.id.action_history_clear) {
HistorySettingsFragment
.openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables);
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
@ -228,7 +190,7 @@ public class StatisticsPlaylistFragment
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
itemsListState = Objects.requireNonNull(itemsList.getLayoutManager()).onSaveInstanceState();
}
@Override
@ -287,7 +249,8 @@ public class StatisticsPlaylistFragment
@Override
public void onError(final Throwable exception) {
StatisticsPlaylistFragment.this.onError(exception);
showError(
new ErrorInfo(exception, UserAction.SOMETHING_ELSE, "History Statistics"));
}
@Override
@ -313,7 +276,7 @@ public class StatisticsPlaylistFragment
}
itemListAdapter.addItems(processResult(result));
if (itemsListState != null) {
if (itemsListState != null && itemsList.getLayoutManager() != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
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
//////////////////////////////////////////////////////////////////////////*/
@ -439,9 +391,8 @@ public class StatisticsPlaylistFragment
Toast.LENGTH_SHORT).show();
}
},
throwable -> showSnackBarError(throwable,
UserAction.DELETE_FROM_HISTORY, "none",
"Deleting item failed", R.string.general_error));
throwable -> showSnackBarError(new ErrorInfo(throwable,
UserAction.DELETE_FROM_HISTORY, "Deleting item")));
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.databinding.LocalPlaylistHeaderBinding;
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.StreamType;
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.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
@ -110,7 +111,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
debouncedSaveSignal = PublishSubject.create();
disposables = new CompositeDisposable();
@ -334,7 +335,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void onError(final Throwable exception) {
LocalPlaylistFragment.this.onError(exception);
showError(new ErrorInfo(exception, UserAction.REQUESTED_BOOKMARK,
"Loading local playlist"));
}
@Override
@ -344,25 +346,23 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_remove_watched:
if (!isRemovingWatched) {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.remove_watched_popup_warning)
.setTitle(R.string.remove_watched_popup_title)
.setPositiveButton(R.string.yes,
(DialogInterface d, int id) -> removeWatchedStreams(false))
.setNeutralButton(
R.string.remove_watched_popup_yes_and_partially_watched_videos,
(DialogInterface d, int id) -> removeWatchedStreams(true))
.setNegativeButton(R.string.cancel,
(DialogInterface d, int id) -> d.cancel())
.create()
.show();
}
break;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == R.id.menu_item_remove_watched) {
if (!isRemovingWatched) {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.remove_watched_popup_warning)
.setTitle(R.string.remove_watched_popup_title)
.setPositiveButton(R.string.yes,
(DialogInterface d, int id) -> removeWatchedStreams(false))
.setNeutralButton(
R.string.remove_watched_popup_yes_and_partially_watched_videos,
(DialogInterface d, int id) -> removeWatchedStreams(true))
.setNegativeButton(R.string.cancel,
(DialogInterface d, int id) -> d.cancel())
.create()
.show();
}
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
@ -455,7 +455,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
hideLoading();
isRemovingWatched = false;
}, this::onError));
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Removing watched videos, partially watched=" + removePartiallyWatched))));
}
@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
//////////////////////////////////////////////////////////////////////////*/
@ -562,7 +552,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final Disposable disposable = playlistManager.renamePlaylist(playlistId, title)
.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);
}
@ -583,7 +575,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final Disposable disposable = playlistManager
.changePlaylistThumbnail(playlistId, thumbnailUrl)
.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);
}
@ -632,7 +626,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return debouncedSaveSignal
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> saveImmediate(), this::onError);
.subscribe(ignored -> saveImmediate(), throwable ->
showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
"Debounced saver")));
}
private void saveImmediate() {
@ -669,7 +665,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
isModified.set(false);
}
},
this::onError
throwable -> showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
);
disposables.add(disposable);
}
@ -683,7 +680,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return new ItemTouchHelper.SimpleCallback(directions,
ItemTouchHelper.ACTION_STATE_IDLE) {
@Override
public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView,
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
final int viewSize,
final int viewSizeOutOfBounds,
final int totalSize,
@ -696,9 +693,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
@Override
public boolean onMove(final RecyclerView recyclerView,
final RecyclerView.ViewHolder source,
final RecyclerView.ViewHolder target) {
public boolean onMove(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder source,
@NonNull final RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()
|| itemListAdapter == null) {
return false;
@ -724,7 +721,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
@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.FeedItemCarouselBinding
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.fragments.BaseStateFragment
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_VALUE
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.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture
@ -288,8 +289,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
binding.itemsList.adapter = groupAdapter
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(this::handleResult) })
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, { it?.let(this::handleFeedGroups) })
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) }
}
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
@ -381,7 +382,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
}
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)
}
// /////////////////////////////////////////////////////////////////////////
// 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
// /////////////////////////////////////////////////////////////////////////

View File

@ -22,13 +22,13 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.BaseFragment;
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.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
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.FilePickerActivityHelper;
import org.schabi.newpipe.util.ServiceHelper;
@ -84,10 +84,12 @@ public class SubscriptionsImportFragment extends BaseFragment {
setupServiceVariables();
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
ErrorActivity.reportError(activity, Collections.emptyList(), null, null,
ErrorInfo.make(UserAction.SOMETHING_ELSE,
ErrorActivity.reportErrorInSnackbar(activity,
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
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();
}
}

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.ThemeHelper
import java.io.Serializable
import kotlin.collections.contains
class FeedGroupDialog : DialogFragment(), BackPressable {
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.util.ThemeHelper
import java.util.Collections
import kotlin.collections.ArrayList
import kotlin.collections.List
import kotlin.collections.map
import kotlin.collections.sortedBy
class FeedGroupReorderDialog : DialogFragment() {
private var _binding: DialogFeedGroupReorderBinding? = null

View File

@ -35,15 +35,14 @@ import androidx.core.app.ServiceCompat;
import org.reactivestreams.Publisher;
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.ktx.ExceptionUtils;
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.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@ -152,13 +151,10 @@ public abstract class BaseImportExportService extends Service {
postErrorResult(null, null);
}
protected void stopAndReportError(@Nullable final Throwable error, final String request) {
protected void stopAndReportError(final Throwable throwable, final String request) {
stopService();
final ErrorInfo errorInfo = ErrorInfo
.make(UserAction.SUBSCRIPTION, "unknown", request, R.string.general_error);
ErrorActivity.reportError(this, error != null ? Collections.singletonList(error)
: Collections.emptyList(), null, null, errorInfo);
ErrorActivity.reportError(this, new ErrorInfo(
throwable, UserAction.SUBSCRIPTION_IMPORT_EXPORT, request));
}
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.NewPipeDatabase;
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.localization.ContentCountry;
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.ZipHelper;
@ -198,7 +196,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
} catch (final Exception e) {
onError(e);
ErrorActivity.reportUiErrorInSnackbar(this, "Exporting database", e);
}
}
@ -243,20 +241,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
System.exit(0);
}
} 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;
import android.content.Context;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
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.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.InfoCache;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@ -46,120 +48,103 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
public boolean onPreferenceTreeClick(final Preference preference) {
if (preference.getKey().equals(cacheWipeKey)) {
InfoCache.getInstance().clearCache();
Toast.makeText(preference.getContext(), R.string.metadata_cache_wipe_complete_notice,
Toast.LENGTH_SHORT).show();
Toast.makeText(requireContext(),
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)) {
new AlertDialog.Builder(getActivity())
.setTitle(R.string.delete_view_history_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> {
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)));
private static Disposable getDeletePlaybackStatesDisposable(
@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")));
}
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)));
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")));
}
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()
.show();
}
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")));
}
if (preference.getKey().equals(playbackStatesClearKey)) {
new AlertDialog.Builder(getActivity())
.setTitle(R.string.delete_playback_states_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> {
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")));
}
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)));
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)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> {
disposables.add(getDeletePlaybackStatesDisposable(context, recordManager));
disposables.add(getWholeStreamHistoryDisposable(context, recordManager));
disposables.add(getRemoveOrphanedRecordsDisposable(context, recordManager));
}))
.create()
.show();
}
disposables.add(onDeletePlaybackStates);
}))
.create()
.show();
}
public static void openDeletePlaybackStatesDialog(@NonNull final Context context,
final HistoryRecordManager recordManager,
final CompositeDisposable disposables) {
new AlertDialog.Builder(context)
.setTitle(R.string.delete_playback_states_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) ->
disposables.add(getDeletePlaybackStatesDisposable(context, recordManager))))
.create()
.show();
}
if (preference.getKey().equals(searchHistoryClearKey)) {
new AlertDialog.Builder(getActivity())
.setTitle(R.string.delete_search_history_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> {
final Disposable onDelete = recordManager.deleteCompleteSearchHistory()
.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()
.show();
}
return super.onPreferenceTreeClick(preference);
public static void openDeleteSearchHistoryDialog(@NonNull final Context context,
final HistoryRecordManager recordManager,
final CompositeDisposable disposables) {
new AlertDialog.Builder(context)
.setTitle(R.string.delete_search_history_alert)
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) ->
disposables.add(getDeleteSearchHistoryDisposable(context, recordManager))))
.create()
.show();
}
}

View File

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

View File

@ -1,6 +1,5 @@
package org.schabi.newpipe.settings;
import android.app.Activity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -16,11 +15,9 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.extractor.NewPipe;
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.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
@ -83,7 +80,7 @@ public class SelectKioskFragment extends DialogFragment {
try {
selectKioskAdapter = new SelectKioskAdapter();
} catch (final Exception e) {
onError(e);
ErrorActivity.reportUiErrorInSnackbar(this, "Selecting kiosk", e);
}
recyclerView.setAdapter(selectKioskAdapter);
@ -109,16 +106,6 @@ public class SelectKioskFragment extends DialogFragment {
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
//////////////////////////////////////////////////////////////////////////*/

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.PlaylistMetadataEntry;
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.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.Vector;
@ -115,8 +115,8 @@ public class SelectPlaylistFragment extends DialogFragment {
protected void onError(final Throwable e) {
final Activity activity = requireActivity();
ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorInfo
.make(UserAction.UI_ERROR, "none", "load_playlists", R.string.app_ui_crash));
ErrorActivity.reportErrorInSnackbar(activity, new ErrorInfo(e,
UserAction.UI_ERROR, "Loading playlists"));
}
/*//////////////////////////////////////////////////////////////////////////

View File

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

View File

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

View File

@ -12,6 +12,9 @@ import com.grack.nanojson.JsonSink;
import org.schabi.newpipe.R;
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.StreamingService;
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.playlist.LocalPlaylistFragment;
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.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
@ -483,9 +483,8 @@ public abstract class Tab {
final StreamingService service = NewPipe.getService(kioskServiceId);
kioskId = service.getKioskList().getDefaultKioskId();
} catch (final ExtractionException e) {
ErrorActivity.reportError(context, e, null, null,
ErrorInfo.make(UserAction.REQUESTED_KIOSK, "none",
"Loading default kiosk from selected service", 0));
ErrorActivity.reportErrorInSnackbar(context, new ErrorInfo(e,
UserAction.REQUESTED_KIOSK, "Loading default kiosk for selected service"));
}
return kioskId;
}

View File

@ -20,12 +20,9 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
@ -33,7 +30,6 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem;
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.channel.ChannelInfo;
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.FeedInfo;
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
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.StreamInfoItem;
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.List;
@ -280,65 +261,6 @@ public final class ExtractorHelper {
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,
* 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 View metaInfoSeparator) {
final Context context = metaInfoTextView.getContext();
final boolean showMetaInfo = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_meta_info_key), true);
if (!showMetaInfo || metaInfos == null || metaInfos.isEmpty()) {
if (metaInfos == null || metaInfos.isEmpty()
|| !PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
context.getString(R.string.show_meta_info_key), true)) {
metaInfoTextView.setVisibility(View.GONE);
metaInfoSeparator.setVisibility(View.GONE);
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.R;
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.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
@ -583,16 +583,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
try {
service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName();
} catch (Exception e) {
service = "-";
service = ErrorInfo.SERVICE_NONE;
}
ErrorActivity.reportError(
mContext,
mission.errObject,
null,
null,
ErrorInfo.make(action, service, request.toString(), reason)
);
ErrorActivity.reportError(mContext,
new ErrorInfo(ErrorInfo.Companion.throwableToStringList(mission.errObject), action,
service, request.toString(), reason, null));
}
public void clearFinishedDownloads(boolean delete) {

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
@ -17,11 +17,24 @@
android:textStyle="bold"
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
android:id="@+id/error_button_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:text="@string/retry"
android:textAlignment="center"
android:textAllCaps="true"

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -14,16 +13,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="90dp"
tools:visibility="visible" />
android:layout_marginTop="90dp" />
</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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