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 714c9fb397
73 changed files with 1148 additions and 1511 deletions

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;
}
}