mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 15:23:00 +00:00 
			
		
		
		
	Merge pull request #5148 from Stypox/error-panel
Show improved error panel instead of annoying snackbar or crashing
This commit is contained in:
		| @@ -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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -85,7 +85,7 @@ | |||||||
|             android:name=".ExitActivity" |             android:name=".ExitActivity" | ||||||
|             android:label="@string/general_error" |             android:label="@string/general_error" | ||||||
|             android:theme="@android:style/Theme.NoDisplay" /> |             android:theme="@android:style/Theme.NoDisplay" /> | ||||||
|         <activity android:name=".report.ErrorActivity" /> |         <activity android:name=".error.ErrorActivity" /> | ||||||
|  |  | ||||||
|         <!-- giga get related --> |         <!-- giga get related --> | ||||||
|         <activity |         <activity | ||||||
| @@ -106,7 +106,7 @@ | |||||||
|         </activity> |         </activity> | ||||||
|  |  | ||||||
|         <activity |         <activity | ||||||
|             android:name=".ReCaptchaActivity" |             android:name=".error.ReCaptchaActivity" | ||||||
|             android:label="@string/recaptcha" /> |             android:label="@string/recaptcha" /> | ||||||
|  |  | ||||||
|         <provider |         <provider | ||||||
|   | |||||||
| @@ -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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -20,12 +20,13 @@ import org.acra.ACRA; | |||||||
| import org.acra.config.ACRAConfigurationException; | import org.acra.config.ACRAConfigurationException; | ||||||
| import org.acra.config.CoreConfiguration; | import org.acra.config.CoreConfiguration; | ||||||
| import org.acra.config.CoreConfigurationBuilder; | import org.acra.config.CoreConfigurationBuilder; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.ReCaptchaActivity; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.downloader.Downloader; | import org.schabi.newpipe.extractor.downloader.Downloader; | ||||||
| import org.schabi.newpipe.ktx.ExceptionUtils; | import org.schabi.newpipe.ktx.ExceptionUtils; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.settings.SettingsActivity; | import org.schabi.newpipe.settings.SettingsActivity; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
| import org.schabi.newpipe.util.ServiceHelper; | import org.schabi.newpipe.util.ServiceHelper; | ||||||
| @@ -224,14 +225,10 @@ public class App extends MultiDexApplication { | |||||||
|                     .setBuildConfigClass(BuildConfig.class) |                     .setBuildConfigClass(BuildConfig.class) | ||||||
|                     .build(); |                     .build(); | ||||||
|             ACRA.init(this, acraConfig); |             ACRA.init(this, acraConfig); | ||||||
|         } catch (final ACRAConfigurationException ace) { |         } catch (final ACRAConfigurationException exception) { | ||||||
|             ace.printStackTrace(); |             exception.printStackTrace(); | ||||||
|             ErrorActivity.reportError(this, |             ErrorActivity.reportError(this, new ErrorInfo(exception, | ||||||
|                     ace, |                     UserAction.SOMETHING_ELSE, "Could not initialize ACRA crash report")); | ||||||
|                     null, |  | ||||||
|                     null, |  | ||||||
|                     ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", |  | ||||||
|                             "Could not initialize ACRA crash report", R.string.app_ui_crash)); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,19 +10,22 @@ import android.content.pm.Signature; | |||||||
| import android.net.ConnectivityManager; | import android.net.ConnectivityManager; | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.core.app.NotificationCompat; | import androidx.core.app.NotificationCompat; | ||||||
| import androidx.core.app.NotificationManagerCompat; | import androidx.core.app.NotificationManagerCompat; | ||||||
| import androidx.core.content.ContextCompat; | import androidx.core.content.ContextCompat; | ||||||
| import androidx.preference.PreferenceManager; | import androidx.preference.PreferenceManager; | ||||||
|  |  | ||||||
| import com.grack.nanojson.JsonObject; | import com.grack.nanojson.JsonObject; | ||||||
| import com.grack.nanojson.JsonParser; | import com.grack.nanojson.JsonParser; | ||||||
| import com.grack.nanojson.JsonParserException; | import com.grack.nanojson.JsonParserException; | ||||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; |  | ||||||
| import io.reactivex.rxjava3.core.Maybe; | import org.schabi.newpipe.error.ErrorActivity; | ||||||
| import io.reactivex.rxjava3.disposables.Disposable; | import org.schabi.newpipe.error.ErrorInfo; | ||||||
| import io.reactivex.rxjava3.schedulers.Schedulers; | import org.schabi.newpipe.error.UserAction; | ||||||
|  |  | ||||||
| import java.io.ByteArrayInputStream; | import java.io.ByteArrayInputStream; | ||||||
| import java.io.InputStream; | import java.io.InputStream; | ||||||
| import java.security.MessageDigest; | import java.security.MessageDigest; | ||||||
| @@ -31,9 +34,11 @@ import java.security.cert.CertificateEncodingException; | |||||||
| import java.security.cert.CertificateException; | import java.security.cert.CertificateException; | ||||||
| import java.security.cert.CertificateFactory; | import java.security.cert.CertificateFactory; | ||||||
| import java.security.cert.X509Certificate; | import java.security.cert.X509Certificate; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | ||||||
| import org.schabi.newpipe.report.UserAction; | import io.reactivex.rxjava3.core.Maybe; | ||||||
|  | import io.reactivex.rxjava3.disposables.Disposable; | ||||||
|  | import io.reactivex.rxjava3.schedulers.Schedulers; | ||||||
|  |  | ||||||
| public final class CheckForNewAppVersion { | public final class CheckForNewAppVersion { | ||||||
|     private CheckForNewAppVersion() { } |     private CheckForNewAppVersion() { } | ||||||
| @@ -58,9 +63,8 @@ public final class CheckForNewAppVersion { | |||||||
|             packageInfo = application.getPackageManager().getPackageInfo( |             packageInfo = application.getPackageManager().getPackageInfo( | ||||||
|                     application.getPackageName(), PackageManager.GET_SIGNATURES); |                     application.getPackageName(), PackageManager.GET_SIGNATURES); | ||||||
|         } catch (final PackageManager.NameNotFoundException e) { |         } catch (final PackageManager.NameNotFoundException e) { | ||||||
|             ErrorActivity.reportError(application, e, null, null, |             ErrorActivity.reportError(application, new ErrorInfo(e, | ||||||
|                     ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", |                     UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info")); | ||||||
|                             "Could not find package info", R.string.app_ui_crash)); |  | ||||||
|             return ""; |             return ""; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -72,9 +76,8 @@ public final class CheckForNewAppVersion { | |||||||
|             final CertificateFactory cf = CertificateFactory.getInstance("X509"); |             final CertificateFactory cf = CertificateFactory.getInstance("X509"); | ||||||
|             c = (X509Certificate) cf.generateCertificate(input); |             c = (X509Certificate) cf.generateCertificate(input); | ||||||
|         } catch (final CertificateException e) { |         } catch (final CertificateException e) { | ||||||
|             ErrorActivity.reportError(application, e, null, null, |             ErrorActivity.reportError(application, new ErrorInfo(e, | ||||||
|                     ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", |                     UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error")); | ||||||
|                             "Certificate error", R.string.app_ui_crash)); |  | ||||||
|             return ""; |             return ""; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -83,9 +86,8 @@ public final class CheckForNewAppVersion { | |||||||
|             final byte[] publicKey = md.digest(c.getEncoded()); |             final byte[] publicKey = md.digest(c.getEncoded()); | ||||||
|             return byte2HexFormatted(publicKey); |             return byte2HexFormatted(publicKey); | ||||||
|         } catch (NoSuchAlgorithmException | CertificateEncodingException e) { |         } catch (NoSuchAlgorithmException | CertificateEncodingException e) { | ||||||
|             ErrorActivity.reportError(application, e, null, null, |             ErrorActivity.reportError(application, new ErrorInfo(e, | ||||||
|                     ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", |                     UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key")); | ||||||
|                             "Could not retrieve SHA1 key", R.string.app_ui_crash)); |  | ||||||
|             return ""; |             return ""; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; | |||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.preference.PreferenceManager; | import androidx.preference.PreferenceManager; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.error.ReCaptchaActivity; | ||||||
| import org.schabi.newpipe.extractor.downloader.Downloader; | import org.schabi.newpipe.extractor.downloader.Downloader; | ||||||
| import org.schabi.newpipe.extractor.downloader.Request; | import org.schabi.newpipe.extractor.downloader.Request; | ||||||
| import org.schabi.newpipe.extractor.downloader.Response; | import org.schabi.newpipe.extractor.downloader.Response; | ||||||
|   | |||||||
| @@ -60,6 +60,7 @@ import org.schabi.newpipe.databinding.DrawerHeaderBinding; | |||||||
| import org.schabi.newpipe.databinding.DrawerLayoutBinding; | import org.schabi.newpipe.databinding.DrawerLayoutBinding; | ||||||
| import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding; | import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding; | ||||||
| import org.schabi.newpipe.databinding.ToolbarLayoutBinding; | import org.schabi.newpipe.databinding.ToolbarLayoutBinding; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
| @@ -72,7 +73,6 @@ import org.schabi.newpipe.player.Player; | |||||||
| import org.schabi.newpipe.player.event.OnKeyDownListener; | import org.schabi.newpipe.player.event.OnKeyDownListener; | ||||||
| import org.schabi.newpipe.player.helper.PlayerHolder; | import org.schabi.newpipe.player.helper.PlayerHolder; | ||||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.util.Constants; | import org.schabi.newpipe.util.Constants; | ||||||
| import org.schabi.newpipe.util.DeviceUtils; | import org.schabi.newpipe.util.DeviceUtils; | ||||||
| import org.schabi.newpipe.util.KioskTranslator; | import org.schabi.newpipe.util.KioskTranslator; | ||||||
| @@ -153,7 +153,7 @@ public class MainActivity extends AppCompatActivity { | |||||||
|         try { |         try { | ||||||
|             setupDrawer(); |             setupDrawer(); | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             ErrorActivity.reportUiError(this, e); |             ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (DeviceUtils.isTv(this)) { |         if (DeviceUtils.isTv(this)) { | ||||||
| @@ -238,7 +238,7 @@ public class MainActivity extends AppCompatActivity { | |||||||
|                 try { |                 try { | ||||||
|                     tabSelected(item); |                     tabSelected(item); | ||||||
|                 } catch (final Exception e) { |                 } catch (final Exception e) { | ||||||
|                     ErrorActivity.reportUiError(this, e); |                     ErrorActivity.reportUiErrorInSnackbar(this, "Selecting main page tab", e); | ||||||
|                 } |                 } | ||||||
|                 break; |                 break; | ||||||
|             case R.id.menu_options_about_group: |             case R.id.menu_options_about_group: | ||||||
| @@ -340,7 +340,7 @@ public class MainActivity extends AppCompatActivity { | |||||||
|             try { |             try { | ||||||
|                 showTabs(); |                 showTabs(); | ||||||
|             } catch (final Exception e) { |             } catch (final Exception e) { | ||||||
|                 ErrorActivity.reportUiError(this, e); |                 ErrorActivity.reportUiErrorInSnackbar(this, "Showing main page tabs", e); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -487,7 +487,7 @@ public class MainActivity extends AppCompatActivity { | |||||||
|             drawerHeaderBinding.drawerHeaderActionButton.setContentDescription( |             drawerHeaderBinding.drawerHeaderActionButton.setContentDescription( | ||||||
|                     getString(R.string.drawer_header_description) + selectedServiceName); |                     getString(R.string.drawer_header_description) + selectedServiceName); | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             ErrorActivity.reportUiError(this, e); |             ErrorActivity.reportUiErrorInSnackbar(this, "Setting up service toggle", e); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         final SharedPreferences sharedPreferences |         final SharedPreferences sharedPreferences | ||||||
| @@ -679,19 +679,16 @@ public class MainActivity extends AppCompatActivity { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean onOptionsItemSelected(final MenuItem item) { |     public boolean onOptionsItemSelected(@NonNull final MenuItem item) { | ||||||
|         if (DEBUG) { |         if (DEBUG) { | ||||||
|             Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); |             Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); | ||||||
|         } |         } | ||||||
|         final int id = item.getItemId(); |  | ||||||
|  |  | ||||||
|         switch (id) { |         if (item.getItemId() == android.R.id.home) { | ||||||
|             case android.R.id.home: |             onHomeButtonPressed(); | ||||||
|                 onHomeButtonPressed(); |             return true; | ||||||
|                 return true; |  | ||||||
|             default: |  | ||||||
|                 return super.onOptionsItemSelected(item); |  | ||||||
|         } |         } | ||||||
|  |         return super.onOptionsItemSelected(item); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -799,7 +796,7 @@ public class MainActivity extends AppCompatActivity { | |||||||
|                 NavigationHelper.gotoMainFragment(getSupportFragmentManager()); |                 NavigationHelper.gotoMainFragment(getSupportFragmentManager()); | ||||||
|             } |             } | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             ErrorActivity.reportUiError(this, e); |             ErrorActivity.reportUiErrorInSnackbar(this, "Handling intent", e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,15 +33,29 @@ import androidx.preference.PreferenceManager; | |||||||
| import org.schabi.newpipe.databinding.ListRadioIconItemBinding; | import org.schabi.newpipe.databinding.ListRadioIconItemBinding; | ||||||
| import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding; | import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding; | ||||||
| import org.schabi.newpipe.download.DownloadDialog; | import org.schabi.newpipe.download.DownloadDialog; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.ReCaptchaActivity; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.Info; | import org.schabi.newpipe.extractor.Info; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.StreamingService.LinkType; | import org.schabi.newpipe.extractor.StreamingService.LinkType; | ||||||
| import org.schabi.newpipe.extractor.channel.ChannelInfo; | import org.schabi.newpipe.extractor.channel.ChannelInfo; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.PaidContentException; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.PrivateContentException; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; | ||||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
|  | import org.schabi.newpipe.ktx.ExceptionUtils; | ||||||
| import org.schabi.newpipe.player.MainPlayer; | import org.schabi.newpipe.player.MainPlayer; | ||||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | import org.schabi.newpipe.player.helper.PlayerHelper; | ||||||
| import org.schabi.newpipe.player.helper.PlayerHolder; | import org.schabi.newpipe.player.helper.PlayerHolder; | ||||||
| @@ -49,7 +63,6 @@ import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; | |||||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||||
| import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; | import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; | ||||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.Constants; | import org.schabi.newpipe.util.Constants; | ||||||
| import org.schabi.newpipe.util.DeviceUtils; | import org.schabi.newpipe.util.DeviceUtils; | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | import org.schabi.newpipe.util.ExtractorHelper; | ||||||
| @@ -84,13 +97,6 @@ import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; | |||||||
|  * Get the url from the intent and open it in the chosen preferred player. |  * Get the url from the intent and open it in the chosen preferred player. | ||||||
|  */ |  */ | ||||||
| public class RouterActivity extends AppCompatActivity { | public class RouterActivity extends AppCompatActivity { | ||||||
|     public static final String INTERNAL_ROUTE_KEY = "internalRoute"; |  | ||||||
|     /** |  | ||||||
|      * Removes invisible separators (\p{Z}) and punctuation characters including |  | ||||||
|      * brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for |  | ||||||
|      * more details. |  | ||||||
|      */ |  | ||||||
|     private static final String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; |  | ||||||
|     protected final CompositeDisposable disposables = new CompositeDisposable(); |     protected final CompositeDisposable disposables = new CompositeDisposable(); | ||||||
|     @State |     @State | ||||||
|     protected int currentServiceId = -1; |     protected int currentServiceId = -1; | ||||||
| @@ -100,7 +106,6 @@ public class RouterActivity extends AppCompatActivity { | |||||||
|     protected int selectedRadioPosition = -1; |     protected int selectedRadioPosition = -1; | ||||||
|     protected int selectedPreviously = -1; |     protected int selectedPreviously = -1; | ||||||
|     protected String currentUrl; |     protected String currentUrl; | ||||||
|     protected boolean internalRoute = false; |  | ||||||
|     private StreamingService currentService; |     private StreamingService currentService; | ||||||
|     private boolean selectionIsDownload = false; |     private boolean selectionIsDownload = false; | ||||||
|  |  | ||||||
| @@ -123,7 +128,7 @@ public class RouterActivity extends AppCompatActivity { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void onSaveInstanceState(final Bundle outState) { |     protected void onSaveInstanceState(@NonNull final Bundle outState) { | ||||||
|         super.onSaveInstanceState(outState); |         super.onSaveInstanceState(outState); | ||||||
|         Icepick.saveInstanceState(this, outState); |         Icepick.saveInstanceState(this, outState); | ||||||
|     } |     } | ||||||
| @@ -145,37 +150,79 @@ public class RouterActivity extends AppCompatActivity { | |||||||
|     private void handleUrl(final String url) { |     private void handleUrl(final String url) { | ||||||
|         disposables.add(Observable |         disposables.add(Observable | ||||||
|                 .fromCallable(() -> { |                 .fromCallable(() -> { | ||||||
|                     if (currentServiceId == -1) { |                     try { | ||||||
|                         currentService = NewPipe.getServiceByUrl(url); |                         if (currentServiceId == -1) { | ||||||
|                         currentServiceId = currentService.getServiceId(); |                             currentService = NewPipe.getServiceByUrl(url); | ||||||
|                         currentLinkType = currentService.getLinkTypeByUrl(url); |                             currentServiceId = currentService.getServiceId(); | ||||||
|                         currentUrl = url; |                             currentLinkType = currentService.getLinkTypeByUrl(url); | ||||||
|                     } else { |                             currentUrl = url; | ||||||
|                         currentService = NewPipe.getService(currentServiceId); |                         } 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()) |                 .subscribeOn(Schedulers.io()) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(result -> { |                 .subscribe(isUrlSupported -> { | ||||||
|                     if (result) { |                     if (isUrlSupported) { | ||||||
|                         onSuccess(); |                         onSuccess(); | ||||||
|                     } else { |                     } else { | ||||||
|                         showUnsupportedUrlDialog(url); |                         showUnsupportedUrlDialog(url); | ||||||
|                     } |                     } | ||||||
|                 }, throwable -> handleError(throwable, url))); |                 }, throwable -> handleError(this, new ErrorInfo(throwable, | ||||||
|  |                         UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url)))); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void handleError(final Throwable throwable, final String url) { |     /** | ||||||
|         throwable.printStackTrace(); |      * @param context the context. It will be {@code finish()}ed at the end of the handling if it is | ||||||
|  |      *                an instance of {@link RouterActivity}. | ||||||
|  |      * @param errorInfo the error information | ||||||
|  |      */ | ||||||
|  |     private static void handleError(final Context context, final ErrorInfo errorInfo) { | ||||||
|  |         if (errorInfo.getThrowable() != null) { | ||||||
|  |             errorInfo.getThrowable().printStackTrace(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (throwable instanceof ExtractionException) { |         if (errorInfo.getThrowable() instanceof ReCaptchaException) { | ||||||
|             showUnsupportedUrlDialog(url); |             Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); | ||||||
|  |             // Starting ReCaptcha Challenge Activity | ||||||
|  |             final Intent intent = new Intent(context, ReCaptchaActivity.class); | ||||||
|  |             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||||
|  |             context.startActivity(intent); | ||||||
|  |         } else if (errorInfo.getThrowable() != null | ||||||
|  |                 && ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) { | ||||||
|  |             Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); | ||||||
|  |         } else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) { | ||||||
|  |             Toast.makeText(context, R.string.restricted_video_no_stream, | ||||||
|  |                     Toast.LENGTH_LONG).show(); | ||||||
|  |         } else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) { | ||||||
|  |             Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show(); | ||||||
|  |         } else if (errorInfo.getThrowable() instanceof PaidContentException) { | ||||||
|  |             Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show(); | ||||||
|  |         } else if (errorInfo.getThrowable() instanceof PrivateContentException) { | ||||||
|  |             Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show(); | ||||||
|  |         } else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) { | ||||||
|  |             Toast.makeText(context, R.string.soundcloud_go_plus_content, | ||||||
|  |                     Toast.LENGTH_LONG).show(); | ||||||
|  |         } else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) { | ||||||
|  |             Toast.makeText(context, R.string.youtube_music_premium_content, | ||||||
|  |                     Toast.LENGTH_LONG).show(); | ||||||
|  |         } else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) { | ||||||
|  |             Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); | ||||||
|  |         } else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) { | ||||||
|  |             Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show(); | ||||||
|         } else { |         } else { | ||||||
|             ExtractorHelper.handleGeneralException(this, -1, url, throwable, |             ErrorActivity.reportError(context, errorInfo); | ||||||
|                     UserAction.SOMETHING_ELSE, null); |         } | ||||||
|             finish(); |  | ||||||
|  |         if (context instanceof RouterActivity) { | ||||||
|  |             ((RouterActivity) context).finish(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -500,7 +547,8 @@ public class RouterActivity extends AppCompatActivity { | |||||||
|                     .subscribe(intent -> { |                     .subscribe(intent -> { | ||||||
|                         startActivity(intent); |                         startActivity(intent); | ||||||
|                         finish(); |                         finish(); | ||||||
|                     }, throwable -> handleError(throwable, currentUrl)) |                     }, throwable -> handleError(this, new ErrorInfo(throwable, | ||||||
|  |                             UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl))) | ||||||
|             ); |             ); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @@ -580,6 +628,7 @@ public class RouterActivity extends AppCompatActivity { | |||||||
|             this.playerChoice = playerChoice; |             this.playerChoice = playerChoice; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         @NonNull | ||||||
|         @Override |         @Override | ||||||
|         public String toString() { |         public String toString() { | ||||||
|             return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; |             return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; | ||||||
| @@ -646,9 +695,9 @@ public class RouterActivity extends AppCompatActivity { | |||||||
|                             if (fetcher != null) { |                             if (fetcher != null) { | ||||||
|                                 fetcher.dispose(); |                                 fetcher.dispose(); | ||||||
|                             } |                             } | ||||||
|                         }, throwable -> ExtractorHelper.handleGeneralException(this, |                         }, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction, | ||||||
|                                 choice.serviceId, choice.url, throwable, finalUserAction, |                                 choice.url + " opened with " + choice.playerChoice, | ||||||
|                                 ", opened with " + choice.playerChoice)); |                                 choice.serviceId))); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity | |||||||
| import org.schabi.newpipe.database.stream.model.StreamEntity | import org.schabi.newpipe.database.stream.model.StreamEntity | ||||||
| import org.schabi.newpipe.database.stream.model.StreamStateEntity | import org.schabi.newpipe.database.stream.model.StreamStateEntity | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem | import org.schabi.newpipe.extractor.stream.StreamInfoItem | ||||||
| import kotlin.jvm.Throws |  | ||||||
|  |  | ||||||
| data class PlaylistStreamEntry( | data class PlaylistStreamEntry( | ||||||
|     @Embedded |     @Embedded | ||||||
|   | |||||||
| @@ -37,6 +37,9 @@ import org.schabi.newpipe.MainActivity; | |||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.RouterActivity; | import org.schabi.newpipe.RouterActivity; | ||||||
| import org.schabi.newpipe.databinding.DownloadDialogBinding; | import org.schabi.newpipe.databinding.DownloadDialogBinding; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.MediaFormat; | import org.schabi.newpipe.extractor.MediaFormat; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.localization.Localization; | import org.schabi.newpipe.extractor.localization.Localization; | ||||||
| @@ -45,9 +48,6 @@ import org.schabi.newpipe.extractor.stream.Stream; | |||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
| import org.schabi.newpipe.extractor.stream.SubtitlesStream; | import org.schabi.newpipe.extractor.stream.SubtitlesStream; | ||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.settings.NewPipeSettings; | import org.schabi.newpipe.settings.NewPipeSettings; | ||||||
| import org.schabi.newpipe.util.FilePickerActivityHelper; | import org.schabi.newpipe.util.FilePickerActivityHelper; | ||||||
| import org.schabi.newpipe.util.FilenameUtils; | import org.schabi.newpipe.util.FilenameUtils; | ||||||
| @@ -61,7 +61,6 @@ import org.schabi.newpipe.util.ThemeHelper; | |||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
|  |  | ||||||
| @@ -591,17 +590,6 @@ public class DownloadDialog extends DialogFragment | |||||||
|                 .show(); |                 .show(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void showErrorActivity(final Exception e) { |  | ||||||
|         ErrorActivity.reportError( |  | ||||||
|                 context, |  | ||||||
|                 Collections.singletonList(e), |  | ||||||
|                 null, |  | ||||||
|                 null, |  | ||||||
|                 ErrorInfo |  | ||||||
|                         .make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error) |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void prepareSelectedDownload() { |     private void prepareSelectedDownload() { | ||||||
|         final StoredDirectoryHelper mainStorage; |         final StoredDirectoryHelper mainStorage; | ||||||
|         final MediaFormat format; |         final MediaFormat format; | ||||||
| @@ -705,7 +693,8 @@ public class DownloadDialog extends DialogFragment | |||||||
|                         mainStorage.getTag()); |                         mainStorage.getTag()); | ||||||
|             } |             } | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             showErrorActivity(e); |             ErrorActivity.reportErrorInSnackbar(this, | ||||||
|  |                     new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage")); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| package org.schabi.newpipe.report; | package org.schabi.newpipe.error; | ||||||
| 
 | 
 | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| 
 | 
 | ||||||
|  | import org.acra.ReportField; | ||||||
| import org.acra.data.CrashReportData; | import org.acra.data.CrashReportData; | ||||||
| import org.acra.sender.ReportSender; | import org.acra.sender.ReportSender; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| @@ -32,8 +33,12 @@ public class AcraReportSender implements ReportSender { | |||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void send(@NonNull final Context context, @NonNull final CrashReportData report) { |     public void send(@NonNull final Context context, @NonNull final CrashReportData report) { | ||||||
|         ErrorActivity.reportError(context, report, |         ErrorActivity.reportError(context, new ErrorInfo( | ||||||
|                 ErrorInfo.make(UserAction.UI_ERROR, "none", |                 new String[]{report.getString(ReportField.STACK_TRACE)}, | ||||||
|                         "App crash, UI failure", R.string.app_ui_crash)); |                 UserAction.UI_ERROR, | ||||||
|  |                 ErrorInfo.SERVICE_NONE, | ||||||
|  |                 "ACRA report", | ||||||
|  |                 R.string.app_ui_crash, | ||||||
|  |                 null)); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package org.schabi.newpipe.report; | package org.schabi.newpipe.error; | ||||||
| 
 | 
 | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| 
 | 
 | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package org.schabi.newpipe.report; | package org.schabi.newpipe.error; | ||||||
| 
 | 
 | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
| import android.app.AlertDialog; | import android.app.AlertDialog; | ||||||
| @@ -8,7 +8,6 @@ import android.graphics.Color; | |||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.os.Build; | import android.os.Build; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.os.Handler; |  | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.view.Menu; | import android.view.Menu; | ||||||
| import android.view.MenuInflater; | import android.view.MenuInflater; | ||||||
| @@ -18,14 +17,11 @@ import android.view.View; | |||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.app.ActionBar; | import androidx.appcompat.app.ActionBar; | ||||||
| import androidx.appcompat.app.AppCompatActivity; | import androidx.appcompat.app.AppCompatActivity; | ||||||
| import androidx.core.app.NavUtils; | import androidx.fragment.app.Fragment; | ||||||
| 
 | 
 | ||||||
| import com.google.android.material.snackbar.Snackbar; | import com.google.android.material.snackbar.Snackbar; | ||||||
| import com.grack.nanojson.JsonWriter; | import com.grack.nanojson.JsonWriter; | ||||||
| 
 | 
 | ||||||
| import org.acra.ReportField; |  | ||||||
| import org.acra.data.CrashReportData; |  | ||||||
| import org.schabi.newpipe.ActivityCommunicator; |  | ||||||
| import org.schabi.newpipe.BuildConfig; | import org.schabi.newpipe.BuildConfig; | ||||||
| import org.schabi.newpipe.MainActivity; | import org.schabi.newpipe.MainActivity; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| @@ -34,14 +30,9 @@ import org.schabi.newpipe.util.Localization; | |||||||
| import org.schabi.newpipe.util.ShareUtils; | import org.schabi.newpipe.util.ShareUtils; | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | import org.schabi.newpipe.util.ThemeHelper; | ||||||
| 
 | 
 | ||||||
| import java.io.PrintWriter; | import java.time.LocalDateTime; | ||||||
| import java.io.StringWriter; | import java.time.format.DateTimeFormatter; | ||||||
| import java.text.SimpleDateFormat; |  | ||||||
| import java.util.Arrays; | import java.util.Arrays; | ||||||
| import java.util.Date; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.TimeZone; |  | ||||||
| import java.util.Vector; |  | ||||||
| 
 | 
 | ||||||
| import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; | import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; | ||||||
| 
 | 
 | ||||||
| @@ -70,108 +61,77 @@ public class ErrorActivity extends AppCompatActivity { | |||||||
|     public static final String TAG = ErrorActivity.class.toString(); |     public static final String TAG = ErrorActivity.class.toString(); | ||||||
|     // BUNDLE TAGS |     // BUNDLE TAGS | ||||||
|     public static final String ERROR_INFO = "error_info"; |     public static final String ERROR_INFO = "error_info"; | ||||||
|     public static final String ERROR_LIST = "error_list"; |  | ||||||
| 
 | 
 | ||||||
|     public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"; |     public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"; | ||||||
|     public static final String ERROR_EMAIL_SUBJECT |     public static final String ERROR_EMAIL_SUBJECT = "Exception in "; | ||||||
|             = "Exception in NewPipe " + BuildConfig.VERSION_NAME; |  | ||||||
| 
 | 
 | ||||||
|     public static final String ERROR_GITHUB_ISSUE_URL |     public static final String ERROR_GITHUB_ISSUE_URL | ||||||
|             = "https://github.com/TeamNewPipe/NewPipe/issues"; |             = "https://github.com/TeamNewPipe/NewPipe/issues"; | ||||||
| 
 | 
 | ||||||
|     private String[] errorList; |     public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER | ||||||
|  |             = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     private ErrorInfo errorInfo; |     private ErrorInfo errorInfo; | ||||||
|     private Class returnActivity; |  | ||||||
|     private String currentTimeStamp; |     private String currentTimeStamp; | ||||||
| 
 | 
 | ||||||
|     private ActivityErrorBinding activityErrorBinding; |     private ActivityErrorBinding activityErrorBinding; | ||||||
| 
 | 
 | ||||||
|     public static void reportUiError(final AppCompatActivity activity, final Throwable el) { |     public static void reportError(final Context context, final ErrorInfo errorInfo) { | ||||||
|         reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UserAction.UI_ERROR, |         final Intent intent = new Intent(context, ErrorActivity.class); | ||||||
|                 "none", "", R.string.app_ui_crash)); |         intent.putExtra(ERROR_INFO, errorInfo); | ||||||
|  |         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||||
|  |         context.startActivity(intent); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static void reportError(final Context context, final List<Throwable> el, |     public static void reportErrorInSnackbar(final Context context, final ErrorInfo errorInfo) { | ||||||
|                                    final Class returnActivity, final View rootView, |         final View rootView = context instanceof Activity | ||||||
|                                    final ErrorInfo errorInfo) { |                 ? ((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) { |         if (rootView != null) { | ||||||
|             Snackbar.make(rootView, R.string.error_snackbar_message, 3 * 1000) |             Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG) | ||||||
|                     .setActionTextColor(Color.YELLOW) |                     .setActionTextColor(Color.YELLOW) | ||||||
|                     .setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v -> |                     .setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v -> | ||||||
|                             startErrorActivity(returnActivity, context, errorInfo, el)).show(); |                             reportError(context, errorInfo)).show(); | ||||||
|         } else { |         } else { | ||||||
|             startErrorActivity(returnActivity, context, errorInfo, el); |             reportError(context, errorInfo); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static void startErrorActivity(final Class returnActivity, final Context context, |  | ||||||
|                                            final ErrorInfo errorInfo, final List<Throwable> el) { |  | ||||||
|         final ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); |  | ||||||
|         ac.setReturnActivity(returnActivity); |  | ||||||
|         final Intent intent = new Intent(context, ErrorActivity.class); |  | ||||||
|         intent.putExtra(ERROR_INFO, errorInfo); |  | ||||||
|         intent.putExtra(ERROR_LIST, elToSl(el)); |  | ||||||
|         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |  | ||||||
|         context.startActivity(intent); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     public static void reportError(final Context context, final Throwable e, |     //////////////////////////////////////////////////////////////////////// | ||||||
|                                    final Class returnActivity, final View rootView, |     // Activity lifecycle | ||||||
|                                    final ErrorInfo errorInfo) { |     //////////////////////////////////////////////////////////////////////// | ||||||
|         List<Throwable> el = null; |  | ||||||
|         if (e != null) { |  | ||||||
|             el = new Vector<>(); |  | ||||||
|             el.add(e); |  | ||||||
|         } |  | ||||||
|         reportError(context, el, returnActivity, rootView, errorInfo); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // async call |  | ||||||
|     public static void reportError(final Handler handler, final Context context, |  | ||||||
|                                    final Throwable e, final Class returnActivity, |  | ||||||
|                                    final View rootView, final ErrorInfo errorInfo) { |  | ||||||
| 
 |  | ||||||
|         List<Throwable> el = null; |  | ||||||
|         if (e != null) { |  | ||||||
|             el = new Vector<>(); |  | ||||||
|             el.add(e); |  | ||||||
|         } |  | ||||||
|         reportError(handler, context, el, returnActivity, rootView, errorInfo); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // async call |  | ||||||
|     public static void reportError(final Handler handler, final Context context, |  | ||||||
|                                    final List<Throwable> el, final Class returnActivity, |  | ||||||
|                                    final View rootView, final ErrorInfo errorInfo) { |  | ||||||
|         handler.post(() -> reportError(context, el, returnActivity, rootView, errorInfo)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static void reportError(final Context context, final CrashReportData report, |  | ||||||
|                                    final ErrorInfo errorInfo) { |  | ||||||
|         final String[] el = {report.getString(ReportField.STACK_TRACE)}; |  | ||||||
| 
 |  | ||||||
|         final Intent intent = new Intent(context, ErrorActivity.class); |  | ||||||
|         intent.putExtra(ERROR_INFO, errorInfo); |  | ||||||
|         intent.putExtra(ERROR_LIST, el); |  | ||||||
|         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |  | ||||||
|         context.startActivity(intent); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static String getStackTrace(final Throwable throwable) { |  | ||||||
|         final StringWriter sw = new StringWriter(); |  | ||||||
|         final PrintWriter pw = new PrintWriter(sw, true); |  | ||||||
|         throwable.printStackTrace(pw); |  | ||||||
|         return sw.getBuffer().toString(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // errorList to StringList |  | ||||||
|     private static String[] elToSl(final List<Throwable> stackTraces) { |  | ||||||
|         final String[] out = new String[stackTraces.size()]; |  | ||||||
|         for (int i = 0; i < stackTraces.size(); i++) { |  | ||||||
|             out[i] = getStackTrace(stackTraces.get(i)); |  | ||||||
|         } |  | ||||||
|         return out; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected void onCreate(final Bundle savedInstanceState) { |     protected void onCreate(final Bundle savedInstanceState) { | ||||||
| @@ -193,38 +153,28 @@ public class ErrorActivity extends AppCompatActivity { | |||||||
|             actionBar.setDisplayShowTitleEnabled(true); |             actionBar.setDisplayShowTitleEnabled(true); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         final ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); |  | ||||||
|         returnActivity = ac.getReturnActivity(); |  | ||||||
|         errorInfo = intent.getParcelableExtra(ERROR_INFO); |         errorInfo = intent.getParcelableExtra(ERROR_INFO); | ||||||
|         errorList = intent.getStringArrayExtra(ERROR_LIST); |  | ||||||
| 
 | 
 | ||||||
|         // important add guru meditation |         // important add guru meditation | ||||||
|         addGuruMeditation(); |         addGuruMeditation(); | ||||||
|         currentTimeStamp = getCurrentTimeStamp(); |         currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now()); | ||||||
| 
 | 
 | ||||||
|         activityErrorBinding.errorReportEmailButton.setOnClickListener(v -> |         activityErrorBinding.errorReportEmailButton.setOnClickListener(v -> | ||||||
|                 openPrivacyPolicyDialog(this, "EMAIL")); |                 openPrivacyPolicyDialog(this, "EMAIL")); | ||||||
| 
 | 
 | ||||||
|         activityErrorBinding.errorReportCopyButton.setOnClickListener(v -> { |         activityErrorBinding.errorReportCopyButton.setOnClickListener(v -> | ||||||
|                 ShareUtils.copyToClipboard(this, buildMarkdown()); |                 ShareUtils.copyToClipboard(this, buildMarkdown())); | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         activityErrorBinding.errorReportGitHubButton.setOnClickListener(v -> |         activityErrorBinding.errorReportGitHubButton.setOnClickListener(v -> | ||||||
|                 openPrivacyPolicyDialog(this, "GITHUB")); |                 openPrivacyPolicyDialog(this, "GITHUB")); | ||||||
| 
 | 
 | ||||||
|         // normal bugreport |         // normal bugreport | ||||||
|         buildInfo(errorInfo); |         buildInfo(errorInfo); | ||||||
|         if (errorInfo.getMessage() != 0) { |         activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId()); | ||||||
|             activityErrorBinding.errorMessageView.setText(errorInfo.getMessage()); |         activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces())); | ||||||
|         } else { |  | ||||||
|             activityErrorBinding.errorMessageView.setVisibility(View.GONE); |  | ||||||
|             activityErrorBinding.messageWhatHappenedView.setVisibility(View.GONE); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         activityErrorBinding.errorView.setText(formErrorText(errorList)); |  | ||||||
| 
 | 
 | ||||||
|         // print stack trace once again for debugging: |         // print stack trace once again for debugging: | ||||||
|         for (final String e : errorList) { |         for (final String e : errorInfo.getStackTraces()) { | ||||||
|             Log.e(TAG, e); |             Log.e(TAG, e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -239,15 +189,14 @@ public class ErrorActivity extends AppCompatActivity { | |||||||
|     @Override |     @Override | ||||||
|     public boolean onOptionsItemSelected(final MenuItem item) { |     public boolean onOptionsItemSelected(final MenuItem item) { | ||||||
|         final int id = item.getItemId(); |         final int id = item.getItemId(); | ||||||
|         switch (id) { |         if (id == android.R.id.home) { | ||||||
|             case android.R.id.home: |             onBackPressed(); | ||||||
|                 goToReturnActivity(); |         } else if (id == R.id.menu_item_share_error) { | ||||||
|                 break; |             ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson()); | ||||||
|             case R.id.menu_item_share_error: |         } else { | ||||||
|                 ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson()); |             return false; | ||||||
|                 break; |  | ||||||
|         } |         } | ||||||
|         return false; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void openPrivacyPolicyDialog(final Context context, final String action) { |     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) |                         final Intent i = new Intent(Intent.ACTION_SENDTO) | ||||||
|                                 .setData(Uri.parse("mailto:")) // only email apps should handle this |                                 .setData(Uri.parse("mailto:")) // only email apps should handle this | ||||||
|                                 .putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS}) |                                 .putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS}) | ||||||
|                                 .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) |                                 .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT | ||||||
|  |                                         + getString(R.string.app_name) + " " | ||||||
|  |                                         + BuildConfig.VERSION_NAME) | ||||||
|                                 .putExtra(Intent.EXTRA_TEXT, buildJson()); |                                 .putExtra(Intent.EXTRA_TEXT, buildJson()); | ||||||
|                         if (i.resolveActivity(getPackageManager()) != null) { |                         if (i.resolveActivity(getPackageManager()) != null) { | ||||||
|                             ShareUtils.openIntentInApp(context, i); |                             ShareUtils.openIntentInApp(context, i); | ||||||
| @@ -310,17 +261,6 @@ public class ErrorActivity extends AppCompatActivity { | |||||||
|         return checkedReturnActivity; |         return checkedReturnActivity; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void goToReturnActivity() { |  | ||||||
|         final Class<? extends Activity> checkedReturnActivity = getReturnActivity(returnActivity); |  | ||||||
|         if (checkedReturnActivity == null) { |  | ||||||
|             super.onBackPressed(); |  | ||||||
|         } else { |  | ||||||
|             final Intent intent = new Intent(this, checkedReturnActivity); |  | ||||||
|             intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); |  | ||||||
|             NavUtils.navigateUpTo(this, intent); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void buildInfo(final ErrorInfo info) { |     private void buildInfo(final ErrorInfo info) { | ||||||
|         String text = ""; |         String text = ""; | ||||||
| 
 | 
 | ||||||
| @@ -355,7 +295,7 @@ public class ErrorActivity extends AppCompatActivity { | |||||||
|                     .value("version", BuildConfig.VERSION_NAME) |                     .value("version", BuildConfig.VERSION_NAME) | ||||||
|                     .value("os", getOsString()) |                     .value("os", getOsString()) | ||||||
|                     .value("time", currentTimeStamp) |                     .value("time", currentTimeStamp) | ||||||
|                     .array("exceptions", Arrays.asList(errorList)) |                     .array("exceptions", Arrays.asList(errorInfo.getStackTraces())) | ||||||
|                     .value("user_comment", activityErrorBinding.errorCommentBox.getText() |                     .value("user_comment", activityErrorBinding.errorCommentBox.getText() | ||||||
|                             .toString()) |                             .toString()) | ||||||
|                     .end() |                     .end() | ||||||
| @@ -393,27 +333,27 @@ public class ErrorActivity extends AppCompatActivity { | |||||||
| 
 | 
 | ||||||
|             // Collapse all logs to a single paragraph when there are more than one |             // Collapse all logs to a single paragraph when there are more than one | ||||||
|             // to keep the GitHub issue clean. |             // to keep the GitHub issue clean. | ||||||
|             if (errorList.length > 1) { |             if (errorInfo.getStackTraces().length > 1) { | ||||||
|                 htmlErrorReport |                 htmlErrorReport | ||||||
|                         .append("<details><summary><b>Exceptions (") |                         .append("<details><summary><b>Exceptions (") | ||||||
|                         .append(errorList.length) |                         .append(errorInfo.getStackTraces().length) | ||||||
|                         .append(")</b></summary><p>\n"); |                         .append(")</b></summary><p>\n"); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // add the logs |             // add the logs | ||||||
|             for (int i = 0; i < errorList.length; i++) { |             for (int i = 0; i < errorInfo.getStackTraces().length; i++) { | ||||||
|                 htmlErrorReport.append("<details><summary><b>Crash log "); |                 htmlErrorReport.append("<details><summary><b>Crash log "); | ||||||
|                 if (errorList.length > 1) { |                 if (errorInfo.getStackTraces().length > 1) { | ||||||
|                     htmlErrorReport.append(i + 1); |                     htmlErrorReport.append(i + 1); | ||||||
|                 } |                 } | ||||||
|                 htmlErrorReport.append("</b>") |                 htmlErrorReport.append("</b>") | ||||||
|                         .append("</summary><p>\n") |                         .append("</summary><p>\n") | ||||||
|                         .append("\n```\n").append(errorList[i]).append("\n```\n") |                         .append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n") | ||||||
|                         .append("</details>\n"); |                         .append("</details>\n"); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // make sure to close everything |             // make sure to close everything | ||||||
|             if (errorList.length > 1) { |             if (errorInfo.getStackTraces().length > 1) { | ||||||
|                 htmlErrorReport.append("</p></details>\n"); |                 htmlErrorReport.append("</p></details>\n"); | ||||||
|             } |             } | ||||||
|             htmlErrorReport.append("<hr>\n"); |             htmlErrorReport.append("<hr>\n"); | ||||||
| @@ -460,17 +400,4 @@ public class ErrorActivity extends AppCompatActivity { | |||||||
|         text += "\n" + getString(R.string.guru_meditation); |         text += "\n" + getString(R.string.guru_meditation); | ||||||
|         activityErrorBinding.errorSorryView.setText(text); |         activityErrorBinding.errorSorryView.setText(text); | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onBackPressed() { |  | ||||||
|         //super.onBackPressed(); |  | ||||||
|         goToReturnActivity(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public String getCurrentTimeStamp() { |  | ||||||
|         final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm"); |  | ||||||
|         df.setTimeZone(TimeZone.getTimeZone("GMT")); |  | ||||||
|         return df.format(new Date()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
							
								
								
									
										113
									
								
								app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
									
									
									
									
									
										Normal 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 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										132
									
								
								app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt
									
									
									
									
									
										Normal 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 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package org.schabi.newpipe; | package org.schabi.newpipe.error; | ||||||
| 
 | 
 | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.content.SharedPreferences; | import android.content.SharedPreferences; | ||||||
| @@ -20,6 +20,9 @@ import androidx.preference.PreferenceManager; | |||||||
| import androidx.webkit.WebViewClientCompat; | import androidx.webkit.WebViewClientCompat; | ||||||
| 
 | 
 | ||||||
| import org.schabi.newpipe.databinding.ActivityRecaptchaBinding; | import org.schabi.newpipe.databinding.ActivityRecaptchaBinding; | ||||||
|  | import org.schabi.newpipe.DownloaderImpl; | ||||||
|  | import org.schabi.newpipe.MainActivity; | ||||||
|  | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | import org.schabi.newpipe.util.ThemeHelper; | ||||||
| 
 | 
 | ||||||
| import java.io.UnsupportedEncodingException; | import java.io.UnsupportedEncodingException; | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package org.schabi.newpipe.report; | package org.schabi.newpipe.error; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The user actions that can cause an error. |  * The user actions that can cause an error. | ||||||
| @@ -6,9 +6,12 @@ package org.schabi.newpipe.report; | |||||||
| public enum UserAction { | public enum UserAction { | ||||||
|     USER_REPORT("user report"), |     USER_REPORT("user report"), | ||||||
|     UI_ERROR("ui error"), |     UI_ERROR("ui error"), | ||||||
|     SUBSCRIPTION("subscription"), |     SUBSCRIPTION_CHANGE("subscription change"), | ||||||
|  |     SUBSCRIPTION_UPDATE("subscription update"), | ||||||
|  |     SUBSCRIPTION_GET("get subscription"), | ||||||
|  |     SUBSCRIPTION_IMPORT_EXPORT("subscription import or export"), | ||||||
|     LOAD_IMAGE("load image"), |     LOAD_IMAGE("load image"), | ||||||
|     SOMETHING_ELSE("something"), |     SOMETHING_ELSE("something else"), | ||||||
|     SEARCHED("searched"), |     SEARCHED("searched"), | ||||||
|     GET_SUGGESTIONS("get suggestions"), |     GET_SUGGESTIONS("get suggestions"), | ||||||
|     REQUESTED_STREAM("requested stream"), |     REQUESTED_STREAM("requested stream"), | ||||||
| @@ -17,11 +20,15 @@ public enum UserAction { | |||||||
|     REQUESTED_KIOSK("requested kiosk"), |     REQUESTED_KIOSK("requested kiosk"), | ||||||
|     REQUESTED_COMMENTS("requested comments"), |     REQUESTED_COMMENTS("requested comments"), | ||||||
|     REQUESTED_FEED("requested feed"), |     REQUESTED_FEED("requested feed"), | ||||||
|  |     REQUESTED_BOOKMARK("bookmark"), | ||||||
|     DELETE_FROM_HISTORY("delete from history"), |     DELETE_FROM_HISTORY("delete from history"), | ||||||
|     PLAY_STREAM("Play stream"), |     PLAY_STREAM("play stream"), | ||||||
|  |     DOWNLOAD_OPEN_DIALOG("download open dialog"), | ||||||
|     DOWNLOAD_POSTPROCESSING("download post-processing"), |     DOWNLOAD_POSTPROCESSING("download post-processing"), | ||||||
|     DOWNLOAD_FAILED("download failed"), |     DOWNLOAD_FAILED("download failed"), | ||||||
|     PREFERENCES_MIGRATION("migration of preferences"); |     PREFERENCES_MIGRATION("migration of preferences"), | ||||||
|  |     SHARE_TO_NEWPIPE("share to newpipe"), | ||||||
|  |     CHECK_FOR_NEW_APP_VERSION("check for new app version"); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     private final String message; |     private final String message; | ||||||
| @@ -1,48 +1,23 @@ | |||||||
| package org.schabi.newpipe.fragments; | package org.schabi.newpipe.fragments; | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.widget.Button; |  | ||||||
| import android.widget.ProgressBar; | import android.widget.ProgressBar; | ||||||
| import android.widget.TextView; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.annotation.StringRes; |  | ||||||
|  |  | ||||||
| import com.jakewharton.rxbinding4.view.RxView; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.BaseFragment; | import org.schabi.newpipe.BaseFragment; | ||||||
| import org.schabi.newpipe.MainActivity; |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.ReCaptchaActivity; | import org.schabi.newpipe.error.ErrorActivity; | ||||||
| import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException; | import org.schabi.newpipe.error.ErrorInfo; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; | import org.schabi.newpipe.error.ErrorPanelHelper; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.PaidContentException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.PrivateContentException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; |  | ||||||
| import org.schabi.newpipe.ktx.ExceptionUtils; |  | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.InfoCache; | import org.schabi.newpipe.util.InfoCache; | ||||||
|  |  | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.concurrent.TimeUnit; |  | ||||||
| import java.util.concurrent.atomic.AtomicBoolean; | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  |  | ||||||
| import icepick.State; | import icepick.State; | ||||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; |  | ||||||
| import io.reactivex.rxjava3.disposables.Disposable; |  | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.ktx.ViewUtils.animate; | import static org.schabi.newpipe.ktx.ViewUtils.animate; | ||||||
|  |  | ||||||
| @@ -56,11 +31,10 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC | |||||||
|     @Nullable |     @Nullable | ||||||
|     private ProgressBar loadingProgressBar; |     private ProgressBar loadingProgressBar; | ||||||
|  |  | ||||||
|     private Disposable errorDisposable; |     private ErrorPanelHelper errorPanelHelper; | ||||||
|  |     @Nullable | ||||||
|     protected View errorPanelRoot; |     @State | ||||||
|     private Button errorButtonRetry; |     protected ErrorInfo lastPanelError = null; | ||||||
|     private TextView errorTextView; |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) { |     public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) { | ||||||
| @@ -74,12 +48,18 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC | |||||||
|         wasLoading.set(isLoading.get()); |         wasLoading.set(isLoading.get()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onResume() { | ||||||
|  |         super.onResume(); | ||||||
|  |         if (lastPanelError != null) { | ||||||
|  |             showError(lastPanelError); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onDestroy() { |     public void onDestroy() { | ||||||
|         super.onDestroy(); |         super.onDestroy(); | ||||||
|         if (errorDisposable != null) { |         errorPanelHelper.dispose(); | ||||||
|             errorDisposable.dispose(); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -89,22 +69,9 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC | |||||||
|     @Override |     @Override | ||||||
|     protected void initViews(final View rootView, final Bundle savedInstanceState) { |     protected void initViews(final View rootView, final Bundle savedInstanceState) { | ||||||
|         super.initViews(rootView, savedInstanceState); |         super.initViews(rootView, savedInstanceState); | ||||||
|  |  | ||||||
|         emptyStateView = rootView.findViewById(R.id.empty_state_view); |         emptyStateView = rootView.findViewById(R.id.empty_state_view); | ||||||
|         loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar); |         loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar); | ||||||
|  |         errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked); | ||||||
|         errorPanelRoot = rootView.findViewById(R.id.error_panel); |  | ||||||
|         errorButtonRetry = rootView.findViewById(R.id.error_button_retry); |  | ||||||
|         errorTextView = rootView.findViewById(R.id.error_message_view); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void initListeners() { |  | ||||||
|         super.initListeners(); |  | ||||||
|         errorDisposable = RxView.clicks(errorButtonRetry) |  | ||||||
|                 .debounce(300, TimeUnit.MILLISECONDS) |  | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |  | ||||||
|                 .subscribe(o -> onRetryButtonClicked()); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected void onRetryButtonClicked() { |     protected void onRetryButtonClicked() { | ||||||
| @@ -143,7 +110,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC | |||||||
|         if (loadingProgressBar != null) { |         if (loadingProgressBar != null) { | ||||||
|             animate(loadingProgressBar, true, 400); |             animate(loadingProgressBar, true, 400); | ||||||
|         } |         } | ||||||
|         animate(errorPanelRoot, false, 150); |         hideErrorPanel(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -154,10 +121,9 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC | |||||||
|         if (loadingProgressBar != null) { |         if (loadingProgressBar != null) { | ||||||
|             animate(loadingProgressBar, false, 0); |             animate(loadingProgressBar, false, 0); | ||||||
|         } |         } | ||||||
|         animate(errorPanelRoot, false, 150); |         hideErrorPanel(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void showEmptyState() { |     public void showEmptyState() { | ||||||
|         isLoading.set(false); |         isLoading.set(false); | ||||||
|         if (emptyStateView != null) { |         if (emptyStateView != null) { | ||||||
| @@ -166,26 +132,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC | |||||||
|         if (loadingProgressBar != null) { |         if (loadingProgressBar != null) { | ||||||
|             animate(loadingProgressBar, false, 0); |             animate(loadingProgressBar, false, 0); | ||||||
|         } |         } | ||||||
|         animate(errorPanelRoot, false, 150); |         hideErrorPanel(); | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void showError(final String message, final boolean showRetryButton) { |  | ||||||
|         if (DEBUG) { |  | ||||||
|             Log.d(TAG, "showError() called with: " |  | ||||||
|                     + "message = [" + message + "], showRetryButton = [" + showRetryButton + "]"); |  | ||||||
|         } |  | ||||||
|         isLoading.set(false); |  | ||||||
|         InfoCache.getInstance().clearCache(); |  | ||||||
|         hideLoading(); |  | ||||||
|  |  | ||||||
|         errorTextView.setText(message); |  | ||||||
|         if (showRetryButton) { |  | ||||||
|             animate(errorButtonRetry, true, 600); |  | ||||||
|         } else { |  | ||||||
|             animate(errorButtonRetry, false, 0); |  | ||||||
|         } |  | ||||||
|         animate(errorPanelRoot, true, 300); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -196,138 +143,69 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC | |||||||
|         hideLoading(); |         hideLoading(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void handleError() { | ||||||
|  |         isLoading.set(false); | ||||||
|  |         InfoCache.getInstance().clearCache(); | ||||||
|  |         if (emptyStateView != null) { | ||||||
|  |             animate(emptyStateView, false, 150); | ||||||
|  |         } | ||||||
|  |         if (loadingProgressBar != null) { | ||||||
|  |             animate(loadingProgressBar, false, 0); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Error handling |     // Error handling | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     /** |     public final void showError(final ErrorInfo errorInfo) { | ||||||
|      * Default implementation handles some general exceptions. |         handleError(); | ||||||
|      * |  | ||||||
|      * @param exception The exception that should be handled |  | ||||||
|      * @return If the exception was handled |  | ||||||
|      */ |  | ||||||
|     protected boolean onError(final Throwable exception) { |  | ||||||
|         if (DEBUG) { |  | ||||||
|             Log.d(TAG, "onError() called with: exception = [" + exception + "]"); |  | ||||||
|         } |  | ||||||
|         isLoading.set(false); |  | ||||||
|  |  | ||||||
|         if (isDetached() || isRemoving()) { |         if (isDetached() || isRemoving()) { | ||||||
|             if (DEBUG) { |             if (DEBUG) { | ||||||
|                 Log.w(TAG, "onError() is detached or removing = [" + exception + "]"); |                 Log.w(TAG, "showError() is detached or removing = [" + errorInfo + "]"); | ||||||
|             } |             } | ||||||
|             return true; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (ExceptionUtils.isInterruptedCaused(exception)) { |         errorPanelHelper.showError(errorInfo); | ||||||
|  |         lastPanelError = errorInfo; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public final void showTextError(@NonNull final String errorString) { | ||||||
|  |         handleError(); | ||||||
|  |  | ||||||
|  |         if (isDetached() || isRemoving()) { | ||||||
|             if (DEBUG) { |             if (DEBUG) { | ||||||
|                 Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]"); |                 Log.w(TAG, "showTextError() is detached or removing = [" + errorString + "]"); | ||||||
|             } |             } | ||||||
|             return true; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (exception instanceof ReCaptchaException) { |         errorPanelHelper.showTextError(errorString); | ||||||
|             onReCaptchaException((ReCaptchaException) exception); |  | ||||||
|             return true; |  | ||||||
|         } else if (ExceptionUtils.isNetworkRelated(exception)) { |  | ||||||
|             showError(getString(R.string.network_error), true); |  | ||||||
|             return true; |  | ||||||
|         } else if (exception instanceof AgeRestrictedContentException) { |  | ||||||
|             showError(getString(R.string.restricted_video_no_stream), false); |  | ||||||
|             return true; |  | ||||||
|         } else if (exception instanceof GeographicRestrictionException) { |  | ||||||
|             showError(getString(R.string.georestricted_content), false); |  | ||||||
|             return true; |  | ||||||
|         } else if (exception instanceof PaidContentException) { |  | ||||||
|             showError(getString(R.string.paid_content), false); |  | ||||||
|             return true; |  | ||||||
|         } else if (exception instanceof PrivateContentException) { |  | ||||||
|             showError(getString(R.string.private_content), false); |  | ||||||
|             return true; |  | ||||||
|         } else if (exception instanceof SoundCloudGoPlusContentException) { |  | ||||||
|             showError(getString(R.string.soundcloud_go_plus_content), false); |  | ||||||
|             return true; |  | ||||||
|         } else if (exception instanceof YoutubeMusicPremiumContentException) { |  | ||||||
|             showError(getString(R.string.youtube_music_premium_content), false); |  | ||||||
|             return true; |  | ||||||
|         } else if (exception instanceof ContentNotAvailableException) { |  | ||||||
|             showError(getString(R.string.content_not_available), false); |  | ||||||
|             return true; |  | ||||||
|         } else if (exception instanceof ContentNotSupportedException) { |  | ||||||
|             showError(getString(R.string.content_not_supported), false); |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onReCaptchaException(final ReCaptchaException exception) { |     public final void hideErrorPanel() { | ||||||
|         if (DEBUG) { |         errorPanelHelper.hide(); | ||||||
|             Log.d(TAG, "onReCaptchaException() called"); |         lastPanelError = null; | ||||||
|         } |  | ||||||
|         Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); |  | ||||||
|         // Starting ReCaptcha Challenge Activity |  | ||||||
|         final Intent intent = new Intent(activity, ReCaptchaActivity.class); |  | ||||||
|         intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, exception.getUrl()); |  | ||||||
|         startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST); |  | ||||||
|  |  | ||||||
|         showError(getString(R.string.recaptcha_request_toast), false); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onUnrecoverableError(final Throwable exception, final UserAction userAction, |     public final boolean isErrorPanelVisible() { | ||||||
|                                      final String serviceName, final String request, |         return errorPanelHelper.isVisible(); | ||||||
|                                      @StringRes final int errorId) { |  | ||||||
|         onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName, |  | ||||||
|                 request, errorId); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void onUnrecoverableError(final List<Throwable> exception, final UserAction userAction, |  | ||||||
|                                      final String serviceName, final String request, |  | ||||||
|                                      @StringRes final int errorId) { |  | ||||||
|         if (DEBUG) { |  | ||||||
|             Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, |  | ||||||
|                 ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName, |  | ||||||
|                         request == null ? "none" : request, errorId)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void showSnackBarError(final Throwable exception, final UserAction userAction, |  | ||||||
|                                   final String serviceName, final String request, |  | ||||||
|                                   @StringRes final int errorId) { |  | ||||||
|         showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request, |  | ||||||
|                 errorId); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Show a SnackBar and only call |      * Show a SnackBar and only call | ||||||
|      * {@link ErrorActivity#reportError(Context, List, Class, View, ErrorInfo)} |      * {@link ErrorActivity#reportErrorInSnackbar(androidx.fragment.app.Fragment, ErrorInfo)} | ||||||
|      * IF we a find a valid view (otherwise the error screen appears). |      * IF we a find a valid view (otherwise the error screen appears). | ||||||
|      * |      * | ||||||
|      * @param exception List of the exceptions to show |      * @param errorInfo The error information | ||||||
|      * @param userAction The user action that caused the exception |  | ||||||
|      * @param serviceName The service where the exception happened |  | ||||||
|      * @param request The page that was requested |  | ||||||
|      * @param errorId The ID of the error |  | ||||||
|      */ |      */ | ||||||
|     public void showSnackBarError(final List<Throwable> exception, final UserAction userAction, |     public void showSnackBarError(final ErrorInfo errorInfo) { | ||||||
|                                   final String serviceName, final String request, |  | ||||||
|                                   @StringRes final int errorId) { |  | ||||||
|         if (DEBUG) { |         if (DEBUG) { | ||||||
|             Log.d(TAG, "showSnackBarError() called with: " |             Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]"); | ||||||
|                     + "exception = [" + exception + "], userAction = [" + userAction + "], " |  | ||||||
|                     + "request = [" + request + "], errorId = [" + errorId + "]"); |  | ||||||
|         } |         } | ||||||
|         View rootView = activity != null ? activity.findViewById(android.R.id.content) : null; |         ErrorActivity.reportErrorInSnackbar(this, errorInfo); | ||||||
|         if (rootView == null && getView() != null) { |  | ||||||
|             rootView = getView(); |  | ||||||
|         } |  | ||||||
|         if (rootView == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView, |  | ||||||
|                 ErrorInfo.make(userAction, serviceName, request, errorId)); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,9 +11,18 @@ import org.schabi.newpipe.BaseFragment; | |||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  |  | ||||||
| public class EmptyFragment extends BaseFragment { | public class EmptyFragment extends BaseFragment { | ||||||
|  |     final boolean showMessage; | ||||||
|  |  | ||||||
|  |     public EmptyFragment(final boolean showMessage) { | ||||||
|  |         this.showMessage = showMessage; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, |     public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, | ||||||
|                              final Bundle savedInstanceState) { |                              final Bundle savedInstanceState) { | ||||||
|         return inflater.inflate(R.layout.fragment_empty, container, false); |         final View view = inflater.inflate(R.layout.fragment_empty, container, false); | ||||||
|  |         view.findViewById(R.id.empty_state_view).setVisibility( | ||||||
|  |                 showMessage ? View.VISIBLE : View.GONE); | ||||||
|  |         return view; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ import android.view.ViewGroup; | |||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.app.ActionBar; | import androidx.appcompat.app.ActionBar; | ||||||
| import androidx.appcompat.app.AppCompatActivity; |  | ||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
| import androidx.fragment.app.FragmentManager; | import androidx.fragment.app.FragmentManager; | ||||||
| import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround; | import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround; | ||||||
| @@ -25,10 +24,8 @@ import com.google.android.material.tabs.TabLayout; | |||||||
| import org.schabi.newpipe.BaseFragment; | import org.schabi.newpipe.BaseFragment; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.databinding.FragmentMainBinding; | import org.schabi.newpipe.databinding.FragmentMainBinding; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.settings.tabs.Tab; | import org.schabi.newpipe.settings.tabs.Tab; | ||||||
| import org.schabi.newpipe.settings.tabs.TabsManager; | import org.schabi.newpipe.settings.tabs.TabsManager; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| @@ -128,7 +125,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | |||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { |     public void onCreateOptionsMenu(@NonNull final Menu menu, | ||||||
|  |                                     @NonNull final MenuInflater inflater) { | ||||||
|         super.onCreateOptionsMenu(menu, inflater); |         super.onCreateOptionsMenu(menu, inflater); | ||||||
|         if (DEBUG) { |         if (DEBUG) { | ||||||
|             Log.d(TAG, "onCreateOptionsMenu() called with: " |             Log.d(TAG, "onCreateOptionsMenu() called with: " | ||||||
| @@ -144,15 +142,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean onOptionsItemSelected(final MenuItem item) { |     public boolean onOptionsItemSelected(final MenuItem item) { | ||||||
|         switch (item.getItemId()) { |         if (item.getItemId() == R.id.action_search) { | ||||||
|             case R.id.action_search: |             try { | ||||||
|                 try { |                 NavigationHelper.openSearchFragment(getFM(), | ||||||
|                     NavigationHelper.openSearchFragment(getFM(), |                         ServiceHelper.getSelectedServiceId(activity), ""); | ||||||
|                             ServiceHelper.getSelectedServiceId(activity), ""); |             } catch (final Exception e) { | ||||||
|                 } catch (final Exception e) { |                 ErrorActivity.reportUiErrorInSnackbar(this, "Opening search fragment", e); | ||||||
|                     ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); |             } | ||||||
|                 } |             return true; | ||||||
|                 return true; |  | ||||||
|         } |         } | ||||||
|         return super.onOptionsItemSelected(item); |         return super.onOptionsItemSelected(item); | ||||||
|     } |     } | ||||||
| @@ -241,8 +238,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (throwable != null) { |             if (throwable != null) { | ||||||
|                 ErrorActivity.reportError(context, throwable, null, null, ErrorInfo |                 ErrorActivity.reportUiErrorInSnackbar(context, "Getting fragment item", throwable); | ||||||
|                         .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); |  | ||||||
|                 return new BlankFragment(); |                 return new BlankFragment(); | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -254,7 +250,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public int getItemPosition(final Object object) { |         public int getItemPosition(@NonNull final Object object) { | ||||||
|             // Causes adapter to reload all Fragments when |             // Causes adapter to reload all Fragments when | ||||||
|             // notifyDataSetChanged is called |             // notifyDataSetChanged is called | ||||||
|             return POSITION_NONE; |             return POSITION_NONE; | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ public interface ViewContract<I> { | |||||||
|  |  | ||||||
|     void showEmptyState(); |     void showEmptyState(); | ||||||
|  |  | ||||||
|     void showError(String message, boolean showRetryButton); |  | ||||||
|  |  | ||||||
|     void handleResult(I result); |     void handleResult(I result); | ||||||
|  |  | ||||||
|  |     void handleError(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -37,12 +37,10 @@ import androidx.annotation.NonNull; | |||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.annotation.StringRes; | import androidx.annotation.StringRes; | ||||||
| import androidx.appcompat.app.AlertDialog; | import androidx.appcompat.app.AlertDialog; | ||||||
| import androidx.appcompat.app.AppCompatActivity; |  | ||||||
| import androidx.appcompat.content.res.AppCompatResources; | import androidx.appcompat.content.res.AppCompatResources; | ||||||
| import androidx.appcompat.widget.Toolbar; | import androidx.appcompat.widget.Toolbar; | ||||||
| import androidx.coordinatorlayout.widget.CoordinatorLayout; | import androidx.coordinatorlayout.widget.CoordinatorLayout; | ||||||
| import androidx.core.content.ContextCompat; | import androidx.core.content.ContextCompat; | ||||||
| import androidx.fragment.app.Fragment; |  | ||||||
| import androidx.preference.PreferenceManager; | import androidx.preference.PreferenceManager; | ||||||
|  |  | ||||||
| import com.google.android.exoplayer2.ExoPlaybackException; | import com.google.android.exoplayer2.ExoPlaybackException; | ||||||
| @@ -56,14 +54,15 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene | |||||||
|  |  | ||||||
| import org.schabi.newpipe.App; | import org.schabi.newpipe.App; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.ReCaptchaActivity; |  | ||||||
| import org.schabi.newpipe.databinding.FragmentVideoDetailBinding; | import org.schabi.newpipe.databinding.FragmentVideoDetailBinding; | ||||||
| import org.schabi.newpipe.download.DownloadDialog; | import org.schabi.newpipe.download.DownloadDialog; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.ReCaptchaActivity; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.ServiceList; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
| import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; |  | ||||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | import org.schabi.newpipe.extractor.stream.AudioStream; | ||||||
| import org.schabi.newpipe.extractor.stream.Stream; | import org.schabi.newpipe.extractor.stream.Stream; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
| @@ -71,6 +70,7 @@ import org.schabi.newpipe.extractor.stream.StreamType; | |||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
| import org.schabi.newpipe.fragments.BackPressable; | import org.schabi.newpipe.fragments.BackPressable; | ||||||
| import org.schabi.newpipe.fragments.BaseStateFragment; | import org.schabi.newpipe.fragments.BaseStateFragment; | ||||||
|  | import org.schabi.newpipe.fragments.EmptyFragment; | ||||||
| import org.schabi.newpipe.fragments.list.comments.CommentsFragment; | import org.schabi.newpipe.fragments.list.comments.CommentsFragment; | ||||||
| import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; | import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; | ||||||
| import org.schabi.newpipe.ktx.AnimationType; | import org.schabi.newpipe.ktx.AnimationType; | ||||||
| @@ -86,9 +86,6 @@ import org.schabi.newpipe.player.helper.PlayerHolder; | |||||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.Constants; | import org.schabi.newpipe.util.Constants; | ||||||
| import org.schabi.newpipe.util.DeviceUtils; | import org.schabi.newpipe.util.DeviceUtils; | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | import org.schabi.newpipe.util.ExtractorHelper; | ||||||
| @@ -151,6 +148,7 @@ public final class VideoDetailFragment | |||||||
|     private static final String COMMENTS_TAB_TAG = "COMMENTS"; |     private static final String COMMENTS_TAB_TAG = "COMMENTS"; | ||||||
|     private static final String RELATED_TAB_TAG = "NEXT VIDEO"; |     private static final String RELATED_TAB_TAG = "NEXT VIDEO"; | ||||||
|     private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB"; |     private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB"; | ||||||
|  |     private static final String EMPTY_TAB_TAG = "EMPTY TAB"; | ||||||
|  |  | ||||||
|     // tabs |     // tabs | ||||||
|     private boolean showComments; |     private boolean showComments; | ||||||
| @@ -526,7 +524,7 @@ public final class VideoDetailFragment | |||||||
|             NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(), |             NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(), | ||||||
|                     subChannelUrl, subChannelName); |                     subChannelUrl, subChannelName); | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); |             ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -684,13 +682,12 @@ public final class VideoDetailFragment | |||||||
|         binding.detailThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); |         binding.detailThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); | ||||||
|  |  | ||||||
|         if (!isEmpty(info.getThumbnailUrl())) { |         if (!isEmpty(info.getThumbnailUrl())) { | ||||||
|             final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); |  | ||||||
|             final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { |             final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { | ||||||
|                 @Override |                 @Override | ||||||
|                 public void onLoadingFailed(final String imageUri, final View view, |                 public void onLoadingFailed(final String imageUri, final View view, | ||||||
|                                             final FailReason failReason) { |                                             final FailReason failReason) { | ||||||
|                     showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE, |                     showSnackBarError(new ErrorInfo(failReason.getCause(), UserAction.LOAD_IMAGE, | ||||||
|                             infoServiceName, imageUri, R.string.could_not_load_thumbnails); |                             imageUri, info)); | ||||||
|                 } |                 } | ||||||
|             }; |             }; | ||||||
|  |  | ||||||
| @@ -906,10 +903,8 @@ public final class VideoDetailFragment | |||||||
|                             openVideoPlayer(); |                             openVideoPlayer(); | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 }, throwable -> { |                 }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, | ||||||
|                     isLoading.set(false); |                         url == null ? "no url" : url, serviceId))); | ||||||
|                     onError(throwable); |  | ||||||
|                 }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -932,18 +927,22 @@ public final class VideoDetailFragment | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (showRelatedStreams && binding.relatedStreamsLayout == null) { |         if (showRelatedStreams && binding.relatedStreamsLayout == null) { | ||||||
|             //temp empty fragment. will be updated in handleResult |             // temp empty fragment. will be updated in handleResult | ||||||
|             pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG); |             pageAdapter.addFragment(new EmptyFragment(false), RELATED_TAB_TAG); | ||||||
|             tabIcons.add(R.drawable.ic_art_track_white_24dp); |             tabIcons.add(R.drawable.ic_art_track_white_24dp); | ||||||
|             tabContentDescriptions.add(R.string.related_streams_tab_description); |             tabContentDescriptions.add(R.string.related_streams_tab_description); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (showDescription) { |         if (showDescription) { | ||||||
|             // temp empty fragment. will be updated in handleResult |             // temp empty fragment. will be updated in handleResult | ||||||
|             pageAdapter.addFragment(new Fragment(), DESCRIPTION_TAB_TAG); |             pageAdapter.addFragment(new EmptyFragment(false), DESCRIPTION_TAB_TAG); | ||||||
|             tabIcons.add(R.drawable.ic_description_white_24dp); |             tabIcons.add(R.drawable.ic_description_white_24dp); | ||||||
|             tabContentDescriptions.add(R.string.description_tab_description); |             tabContentDescriptions.add(R.string.description_tab_description); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (pageAdapter.getCount() == 0) { | ||||||
|  |             pageAdapter.addFragment(new EmptyFragment(true), EMPTY_TAB_TAG); | ||||||
|  |         } | ||||||
|         pageAdapter.notifyDataSetUpdate(); |         pageAdapter.notifyDataSetUpdate(); | ||||||
|  |  | ||||||
|         if (pageAdapter.getCount() >= 2) { |         if (pageAdapter.getCount() >= 2) { | ||||||
| @@ -1327,8 +1326,8 @@ public final class VideoDetailFragment | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void showError(final String message, final boolean showRetryButton) { |     public void handleError() { | ||||||
|         super.showError(message, showRetryButton); |         super.handleError(); | ||||||
|         setErrorImage(R.drawable.not_available_monkey); |         setErrorImage(R.drawable.not_available_monkey); | ||||||
|  |  | ||||||
|         if (binding.relatedStreamsLayout != null) { // hide related streams for tablets |         if (binding.relatedStreamsLayout != null) { // hide related streams for tablets | ||||||
| @@ -1341,8 +1340,8 @@ public final class VideoDetailFragment | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void hideAgeRestrictedContent() { |     private void hideAgeRestrictedContent() { | ||||||
|         showError(getString(R.string.restricted_video, |         showTextError(getString(R.string.restricted_video, | ||||||
|                 getString(R.string.show_age_restricted_content_title)), false); |                 getString(R.string.show_age_restricted_content_title))); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void setupBroadcastReceiver() { |     private void setupBroadcastReceiver() { | ||||||
| @@ -1548,11 +1547,8 @@ public final class VideoDetailFragment | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!info.getErrors().isEmpty()) { |         if (!info.getErrors().isEmpty()) { | ||||||
|             showSnackBarError(info.getErrors(), |             showSnackBarError(new ErrorInfo(info.getErrors(), | ||||||
|                     UserAction.REQUESTED_STREAM, |                     UserAction.REQUESTED_STREAM, info.getUrl(), info)); | ||||||
|                     NewPipe.getNameOfService(info.getServiceId()), |  | ||||||
|                     info.getUrl(), |  | ||||||
|                     0); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM |         binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM | ||||||
| @@ -1592,6 +1588,10 @@ public final class VideoDetailFragment | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void openDownloadDialog() { |     public void openDownloadDialog() { | ||||||
|  |         if (currentInfo == null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); |             final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); | ||||||
|             downloadDialog.setVideoStreams(sortedVideoStreams); |             downloadDialog.setVideoStreams(sortedVideoStreams); | ||||||
| @@ -1601,18 +1601,9 @@ public final class VideoDetailFragment | |||||||
|  |  | ||||||
|             downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); |             downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             final ErrorInfo info = ErrorInfo.make(UserAction.UI_ERROR, |             ErrorActivity.reportErrorInSnackbar(activity, | ||||||
|                     ServiceList.all() |                     new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, "Showing download dialog", | ||||||
|                             .get(currentInfo |                             currentInfo)); | ||||||
|                                     .getServiceId()) |  | ||||||
|                             .getServiceInfo() |  | ||||||
|                             .getName(), "", |  | ||||||
|                     R.string.could_not_setup_download_menu); |  | ||||||
|  |  | ||||||
|             ErrorActivity.reportError(activity, |  | ||||||
|                     e, |  | ||||||
|                     activity.getClass(), |  | ||||||
|                     activity.findViewById(android.R.id.content), info); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -1620,24 +1611,6 @@ public final class VideoDetailFragment | |||||||
|     // Stream Results |     // Stream Results | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected boolean onError(final Throwable exception) { |  | ||||||
|         if (super.onError(exception)) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         final int errorId = exception instanceof YoutubeStreamExtractor.DeobfuscateException |  | ||||||
|                 ? R.string.youtube_signature_deobfuscation_error |  | ||||||
|                 : exception instanceof ExtractionException |  | ||||||
|                 ? R.string.parsing_error |  | ||||||
|                 : R.string.general_error; |  | ||||||
|  |  | ||||||
|         onUnrecoverableError(exception, UserAction.REQUESTED_STREAM, |  | ||||||
|                 NewPipe.getNameOfService(serviceId), url, errorId); |  | ||||||
|  |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void updateProgressInfo(@NonNull final StreamInfo info) { |     private void updateProgressInfo(@NonNull final StreamInfo info) { | ||||||
|         if (positionSubscriber != null) { |         if (positionSubscriber != null) { | ||||||
|             positionSubscriber.dispose(); |             positionSubscriber.dispose(); | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ import android.view.View; | |||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.app.ActionBar; | import androidx.appcompat.app.ActionBar; | ||||||
| import androidx.appcompat.app.AppCompatActivity; |  | ||||||
| import androidx.preference.PreferenceManager; | import androidx.preference.PreferenceManager; | ||||||
| import androidx.recyclerview.widget.GridLayoutManager; | import androidx.recyclerview.widget.GridLayoutManager; | ||||||
| import androidx.recyclerview.widget.RecyclerView; | import androidx.recyclerview.widget.RecyclerView; | ||||||
| @@ -22,6 +21,7 @@ import androidx.viewbinding.ViewBinding; | |||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.databinding.PignateFooterBinding; | import org.schabi.newpipe.databinding.PignateFooterBinding; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.channel.ChannelInfoItem; | import org.schabi.newpipe.extractor.channel.ChannelInfoItem; | ||||||
| import org.schabi.newpipe.extractor.comments.CommentsInfoItem; | import org.schabi.newpipe.extractor.comments.CommentsInfoItem; | ||||||
| @@ -33,7 +33,6 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | |||||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | import org.schabi.newpipe.info_list.InfoItemDialog; | ||||||
| import org.schabi.newpipe.info_list.InfoListAdapter; | import org.schabi.newpipe.info_list.InfoListAdapter; | ||||||
| import org.schabi.newpipe.player.helper.PlayerHolder; | import org.schabi.newpipe.player.helper.PlayerHolder; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.util.KoreUtil; | import org.schabi.newpipe.util.KoreUtil; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.OnClickGesture; | import org.schabi.newpipe.util.OnClickGesture; | ||||||
| @@ -47,6 +46,7 @@ import java.util.List; | |||||||
| import java.util.Queue; | import java.util.Queue; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.ktx.ViewUtils.animate; | import static org.schabi.newpipe.ktx.ViewUtils.animate; | ||||||
|  | import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; | ||||||
|  |  | ||||||
| public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> | public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> | ||||||
|         implements ListViewContract<I, N>, StateSaver.WriteRead, |         implements ListViewContract<I, N>, StateSaver.WriteRead, | ||||||
| @@ -292,7 +292,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> | |||||||
|                             selectedItem.getUrl(), |                             selectedItem.getUrl(), | ||||||
|                             selectedItem.getName()); |                             selectedItem.getName()); | ||||||
|                 } catch (final Exception e) { |                 } catch (final Exception e) { | ||||||
|                     ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); |                     ErrorActivity.reportUiErrorInSnackbar( | ||||||
|  |                             BaseListFragment.this, "Opening channel fragment", e); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| @@ -307,7 +308,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> | |||||||
|                             selectedItem.getUrl(), |                             selectedItem.getUrl(), | ||||||
|                             selectedItem.getName()); |                             selectedItem.getName()); | ||||||
|                 } catch (final Exception e) { |                 } catch (final Exception e) { | ||||||
|                     ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); |                     ErrorActivity.reportUiErrorInSnackbar(BaseListFragment.this, | ||||||
|  |                             "Opening playlist fragment", e); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| @@ -406,23 +408,23 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> | |||||||
|     // Contract |     // Contract | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void showLoading() { | ||||||
|  |         super.showLoading(); | ||||||
|  |         animateHideRecyclerViewAllowingScrolling(itemsList); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void hideLoading() { |     public void hideLoading() { | ||||||
|         super.hideLoading(); |         super.hideLoading(); | ||||||
|         animate(itemsList, true, 300); |         animate(itemsList, true, 300); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void showError(final String message, final boolean showRetryButton) { |  | ||||||
|         super.showError(message, showRetryButton); |  | ||||||
|         showListFooter(false); |  | ||||||
|         animate(itemsList, false, 200); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void showEmptyState() { |     public void showEmptyState() { | ||||||
|         super.showEmptyState(); |         super.showEmptyState(); | ||||||
|         showListFooter(false); |         showListFooter(false); | ||||||
|  |         animateHideRecyclerViewAllowingScrolling(itemsList); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -439,6 +441,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> | |||||||
|         isLoading.set(false); |         isLoading.set(false); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void handleError() { | ||||||
|  |         super.handleError(); | ||||||
|  |         showListFooter(false); | ||||||
|  |         animateHideRecyclerViewAllowingScrolling(itemsList); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, |     public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, | ||||||
|                                           final String key) { |                                           final String key) { | ||||||
|   | |||||||
| @@ -7,12 +7,17 @@ import android.view.View; | |||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.ListExtractor; | import org.schabi.newpipe.extractor.ListExtractor; | ||||||
| import org.schabi.newpipe.extractor.ListInfo; | import org.schabi.newpipe.extractor.ListInfo; | ||||||
| import org.schabi.newpipe.extractor.Page; | import org.schabi.newpipe.extractor.Page; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; | ||||||
| import org.schabi.newpipe.util.Constants; | import org.schabi.newpipe.util.Constants; | ||||||
| import org.schabi.newpipe.views.NewPipeRecyclerView; | import org.schabi.newpipe.views.NewPipeRecyclerView; | ||||||
|  |  | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
| import java.util.Queue; | import java.util.Queue; | ||||||
|  |  | ||||||
| import icepick.State; | import icepick.State; | ||||||
| @@ -30,10 +35,15 @@ public abstract class BaseListInfoFragment<I extends ListInfo> | |||||||
|     @State |     @State | ||||||
|     protected String url; |     protected String url; | ||||||
|  |  | ||||||
|  |     private final UserAction errorUserAction; | ||||||
|     protected I currentInfo; |     protected I currentInfo; | ||||||
|     protected Page currentNextPage; |     protected Page currentNextPage; | ||||||
|     protected Disposable currentWorker; |     protected Disposable currentWorker; | ||||||
|  |  | ||||||
|  |     protected BaseListInfoFragment(final UserAction errorUserAction) { | ||||||
|  |         this.errorUserAction = errorUserAction; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void initViews(final View rootView, final Bundle savedInstanceState) { |     protected void initViews(final View rootView, final Bundle savedInstanceState) { | ||||||
|         super.initViews(rootView, savedInstanceState); |         super.initViews(rootView, savedInstanceState); | ||||||
| @@ -133,7 +143,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo> | |||||||
|                     currentInfo = result; |                     currentInfo = result; | ||||||
|                     currentNextPage = result.getNextPage(); |                     currentNextPage = result.getNextPage(); | ||||||
|                     handleResult(result); |                     handleResult(result); | ||||||
|                 }, this::onError); |                 }, throwable -> | ||||||
|  |                         showError(new ErrorInfo(throwable, errorUserAction, | ||||||
|  |                                 "Start loading: " + url, serviceId))); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -161,10 +173,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo> | |||||||
|                 .subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> { |                 .subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> { | ||||||
|                     isLoading.set(false); |                     isLoading.set(false); | ||||||
|                     handleNextItems(InfoItemsPage); |                     handleNextItems(InfoItemsPage); | ||||||
|                 }, (@NonNull Throwable throwable) -> { |                 }, (@NonNull Throwable throwable) -> | ||||||
|                     isLoading.set(false); |                         dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable, | ||||||
|                     onError(throwable); |                                 errorUserAction, "Loading more items: " + url, serviceId))); | ||||||
|                 }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void forbidDownwardFocusScroll() { |     private void forbidDownwardFocusScroll() { | ||||||
| @@ -182,10 +193,16 @@ public abstract class BaseListInfoFragment<I extends ListInfo> | |||||||
|     @Override |     @Override | ||||||
|     public void handleNextItems(final ListExtractor.InfoItemsPage result) { |     public void handleNextItems(final ListExtractor.InfoItemsPage result) { | ||||||
|         super.handleNextItems(result); |         super.handleNextItems(result); | ||||||
|  |  | ||||||
|         currentNextPage = result.getNextPage(); |         currentNextPage = result.getNextPage(); | ||||||
|         infoListAdapter.addInfoItemList(result.getItems()); |         infoListAdapter.addInfoItemList(result.getItems()); | ||||||
|  |  | ||||||
|         showListFooter(hasMoreItems()); |         showListFooter(hasMoreItems()); | ||||||
|  |  | ||||||
|  |         if (!result.getErrors().isEmpty()) { | ||||||
|  |             dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction, | ||||||
|  |                     "Get next items of: " + url, serviceId)); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -213,6 +230,18 @@ public abstract class BaseListInfoFragment<I extends ListInfo> | |||||||
|                 showEmptyState(); |                 showEmptyState(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (!result.getErrors().isEmpty()) { | ||||||
|  |             final List<Throwable> errors = new ArrayList<>(result.getErrors()); | ||||||
|  |             // handling ContentNotSupportedException not to show the error but an appropriate string | ||||||
|  |             // so that crashes won't be sent uselessly and the user will understand what happened | ||||||
|  |             errors.removeIf(throwable -> throwable instanceof ContentNotSupportedException); | ||||||
|  |  | ||||||
|  |             if (!errors.isEmpty()) { | ||||||
|  |                 dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), | ||||||
|  |                         errorUserAction, "Start loading: " + url, serviceId)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -224,4 +253,14 @@ public abstract class BaseListInfoFragment<I extends ListInfo> | |||||||
|         this.url = u; |         this.url = u; | ||||||
|         this.name = !TextUtils.isEmpty(title) ? title : ""; |         this.name = !TextUtils.isEmpty(title) ? title : ""; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private void dynamicallyShowErrorPanelOrSnackbar(final ErrorInfo errorInfo) { | ||||||
|  |         if (infoListAdapter.getItemCount() == 0) { | ||||||
|  |             // show error panel only if no items already visible | ||||||
|  |             showError(errorInfo); | ||||||
|  |         } else { | ||||||
|  |             isLoading.set(false); | ||||||
|  |             showSnackBarError(errorInfo); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -16,7 +16,6 @@ import android.widget.Button; | |||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.app.ActionBar; | import androidx.appcompat.app.ActionBar; | ||||||
| import androidx.appcompat.app.AppCompatActivity; |  | ||||||
| import androidx.core.content.ContextCompat; | import androidx.core.content.ContextCompat; | ||||||
| import androidx.viewbinding.ViewBinding; | import androidx.viewbinding.ViewBinding; | ||||||
|  |  | ||||||
| @@ -27,20 +26,19 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; | |||||||
| import org.schabi.newpipe.databinding.ChannelHeaderBinding; | import org.schabi.newpipe.databinding.ChannelHeaderBinding; | ||||||
| import org.schabi.newpipe.databinding.FragmentChannelBinding; | import org.schabi.newpipe.databinding.FragmentChannelBinding; | ||||||
| import org.schabi.newpipe.databinding.PlaylistControlBinding; | import org.schabi.newpipe.databinding.PlaylistControlBinding; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.ListExtractor; | import org.schabi.newpipe.extractor.ListExtractor; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; |  | ||||||
| import org.schabi.newpipe.extractor.channel.ChannelInfo; | import org.schabi.newpipe.extractor.channel.ChannelInfo; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; | import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; |  | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||||
| import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | ||||||
| import org.schabi.newpipe.ktx.AnimationType; | import org.schabi.newpipe.ktx.AnimationType; | ||||||
| import org.schabi.newpipe.local.subscription.SubscriptionManager; | import org.schabi.newpipe.local.subscription.SubscriptionManager; | ||||||
| import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; | import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; | ||||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | import org.schabi.newpipe.util.ExtractorHelper; | ||||||
| import org.schabi.newpipe.util.ImageDisplayConstants; | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
| @@ -91,6 +89,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|         return instance; |         return instance; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public ChannelFragment() { | ||||||
|  |         super(UserAction.REQUESTED_CHANNEL); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void setUserVisibleHint(final boolean isVisibleToUser) { |     public void setUserVisibleHint(final boolean isVisibleToUser) { | ||||||
|         super.setUserVisibleHint(isVisibleToUser); |         super.setUserVisibleHint(isVisibleToUser); | ||||||
| @@ -217,9 +219,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|     private void monitorSubscription(final ChannelInfo info) { |     private void monitorSubscription(final ChannelInfo info) { | ||||||
|         final Consumer<Throwable> onError = (Throwable throwable) -> { |         final Consumer<Throwable> onError = (Throwable throwable) -> { | ||||||
|             animate(headerBinding.channelSubscribeButton, false, 100); |             animate(headerBinding.channelSubscribeButton, false, 100); | ||||||
|             showSnackBarError(throwable, UserAction.SUBSCRIPTION, |             showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET, | ||||||
|                     NewPipe.getNameOfService(currentInfo.getServiceId()), |                     "Get subscription status", currentInfo)); | ||||||
|                     "Get subscription status", 0); |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         final Observable<List<SubscriptionEntity>> observable = subscriptionManager |         final Observable<List<SubscriptionEntity>> observable = subscriptionManager | ||||||
| @@ -269,11 +270,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         final Consumer<Throwable> onError = (@NonNull Throwable throwable) -> |         final Consumer<Throwable> onError = (@NonNull Throwable throwable) -> | ||||||
|                 onUnrecoverableError(throwable, |                 showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE, | ||||||
|                         UserAction.SUBSCRIPTION, |                         "Updating subscription for " + info.getUrl(), info)); | ||||||
|                         NewPipe.getNameOfService(info.getServiceId()), |  | ||||||
|                         "Updating Subscription for " + info.getUrl(), |  | ||||||
|                         R.string.subscription_update_failed); |  | ||||||
|  |  | ||||||
|         disposables.add(subscriptionManager.updateChannelInfo(info) |         disposables.add(subscriptionManager.updateChannelInfo(info) | ||||||
|                 .subscribeOn(Schedulers.io()) |                 .subscribeOn(Schedulers.io()) | ||||||
| @@ -290,11 +288,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         final Consumer<Throwable> onError = (@NonNull Throwable throwable) -> |         final Consumer<Throwable> onError = (@NonNull Throwable throwable) -> | ||||||
|                 onUnrecoverableError(throwable, |                 showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_CHANGE, | ||||||
|                         UserAction.SUBSCRIPTION, |                         "Changing subscription for " + currentInfo.getUrl(), currentInfo)); | ||||||
|                         NewPipe.getNameOfService(currentInfo.getServiceId()), |  | ||||||
|                         "Subscription Change", |  | ||||||
|                         R.string.subscription_change_failed); |  | ||||||
|  |  | ||||||
|         /* Emit clicks from main thread unto io thread */ |         /* Emit clicks from main thread unto io thread */ | ||||||
|         return RxView.clicks(subscribeButton) |         return RxView.clicks(subscribeButton) | ||||||
| @@ -408,7 +403,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|                                 currentInfo.getParentChannelUrl(), |                                 currentInfo.getParentChannelUrl(), | ||||||
|                                 currentInfo.getParentChannelName()); |                                 currentInfo.getParentChannelName()); | ||||||
|                     } catch (final Exception e) { |                     } catch (final Exception e) { | ||||||
|                         ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); |                         ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e); | ||||||
|                     } |                     } | ||||||
|                 } else if (DEBUG) { |                 } else if (DEBUG) { | ||||||
|                     Log.i(TAG, "Can't open parent channel because we got no channel URL"); |                     Log.i(TAG, "Can't open parent channel because we got no channel URL"); | ||||||
| @@ -469,27 +464,13 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|  |  | ||||||
|         playlistControlBinding.getRoot().setVisibility(View.VISIBLE); |         playlistControlBinding.getRoot().setVisibility(View.VISIBLE); | ||||||
|  |  | ||||||
|         final List<Throwable> errors = new ArrayList<>(result.getErrors()); |         for (final Throwable throwable : result.getErrors()) { | ||||||
|         if (!errors.isEmpty()) { |             if (throwable instanceof ContentNotSupportedException) { | ||||||
|  |                 showContentNotSupported(); | ||||||
|             // 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); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (disposables != null) { |         disposables.clear(); | ||||||
|             disposables.clear(); |  | ||||||
|         } |  | ||||||
|         if (subscribeButtonMonitor != null) { |         if (subscribeButtonMonitor != null) { | ||||||
|             subscribeButtonMonitor.dispose(); |             subscribeButtonMonitor.dispose(); | ||||||
|         } |         } | ||||||
| @@ -539,38 +520,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|                 currentInfo.getNextPage(), streamItems, index); |                 currentInfo.getNextPage(), streamItems, index); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void handleNextItems(final ListExtractor.InfoItemsPage result) { |  | ||||||
|         super.handleNextItems(result); |  | ||||||
|  |  | ||||||
|         if (!result.getErrors().isEmpty()) { |  | ||||||
|             showSnackBarError(result.getErrors(), |  | ||||||
|                     UserAction.REQUESTED_CHANNEL, |  | ||||||
|                     NewPipe.getNameOfService(serviceId), |  | ||||||
|                     "Get next page of: " + url, |  | ||||||
|                     R.string.general_error); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|     // OnError |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected boolean onError(final Throwable exception) { |  | ||||||
|         if (super.onError(exception)) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         final int errorId = exception instanceof ExtractionException |  | ||||||
|                 ? R.string.parsing_error : R.string.general_error; |  | ||||||
|  |  | ||||||
|         onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, |  | ||||||
|                 NewPipe.getNameOfService(serviceId), url, errorId); |  | ||||||
|  |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Utils |     // Utils | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|   | |||||||
| @@ -11,12 +11,11 @@ import androidx.annotation.NonNull; | |||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.ListExtractor; | import org.schabi.newpipe.extractor.ListExtractor; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; |  | ||||||
| import org.schabi.newpipe.extractor.comments.CommentsInfo; | import org.schabi.newpipe.extractor.comments.CommentsInfo; | ||||||
| import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | ||||||
| import org.schabi.newpipe.ktx.ViewUtils; | import org.schabi.newpipe.ktx.ViewUtils; | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | import org.schabi.newpipe.util.ExtractorHelper; | ||||||
|  |  | ||||||
| import io.reactivex.rxjava3.core.Single; | import io.reactivex.rxjava3.core.Single; | ||||||
| @@ -25,13 +24,17 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; | |||||||
| public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> { | public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> { | ||||||
|     private final CompositeDisposable disposables = new CompositeDisposable(); |     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 String name) { | ||||||
|         final CommentsFragment instance = new CommentsFragment(); |         final CommentsFragment instance = new CommentsFragment(); | ||||||
|         instance.setInitialData(serviceId, url, name); |         instance.setInitialData(serviceId, url, name); | ||||||
|         return instance; |         return instance; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public CommentsFragment() { | ||||||
|  |         super(UserAction.REQUESTED_COMMENTS); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // LifeCycle |     // LifeCycle | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -67,52 +70,13 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> { | |||||||
|     // Contract |     // Contract | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void showLoading() { |  | ||||||
|         super.showLoading(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void handleResult(@NonNull final CommentsInfo result) { |     public void handleResult(@NonNull final CommentsInfo result) { | ||||||
|         super.handleResult(result); |         super.handleResult(result); | ||||||
|  |  | ||||||
|         ViewUtils.slideUp(requireView(), 120, 150, 0.06f); |         ViewUtils.slideUp(requireView(), 120, 150, 0.06f); | ||||||
|  |  | ||||||
|         if (!result.getErrors().isEmpty()) { |  | ||||||
|             showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, |  | ||||||
|                     NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         disposables.clear(); |         disposables.clear(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void handleNextItems(final ListExtractor.InfoItemsPage result) { |  | ||||||
|         super.handleNextItems(result); |  | ||||||
|  |  | ||||||
|         if (!result.getErrors().isEmpty()) { |  | ||||||
|             showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, |  | ||||||
|                     NewPipe.getNameOfService(serviceId), "Get next page of: " + url, |  | ||||||
|                     R.string.general_error); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|     // OnError |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected boolean onError(final Throwable exception) { |  | ||||||
|         if (super.onError(exception)) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         hideLoading(); |  | ||||||
|         showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, |  | ||||||
|                 NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments); |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Utils |     // Utils | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|   | |||||||
| @@ -2,14 +2,16 @@ package org.schabi.newpipe.fragments.list.kiosk; | |||||||
|  |  | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
| import org.schabi.newpipe.extractor.kiosk.KioskList; | import org.schabi.newpipe.extractor.kiosk.KioskList; | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.KioskTranslator; | import org.schabi.newpipe.util.KioskTranslator; | ||||||
| import org.schabi.newpipe.util.ServiceHelper; | import org.schabi.newpipe.util.ServiceHelper; | ||||||
|  |  | ||||||
| public class DefaultKioskFragment extends KioskFragment { | public class DefaultKioskFragment extends KioskFragment { | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onCreate(final Bundle savedInstanceState) { |     public void onCreate(final Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
| @@ -46,8 +48,8 @@ public class DefaultKioskFragment extends KioskFragment { | |||||||
|             currentInfo = null; |             currentInfo = null; | ||||||
|             currentNextPage = null; |             currentNextPage = null; | ||||||
|         } catch (final ExtractionException e) { |         } catch (final ExtractionException e) { | ||||||
|             onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none", |             showError(new ErrorInfo(e, UserAction.REQUESTED_KIOSK, | ||||||
|                     "Loading default kiosk from selected service", 0); |                     "Loading default kiosk for selected service")); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ import androidx.annotation.Nullable; | |||||||
| import androidx.appcompat.app.ActionBar; | import androidx.appcompat.app.ActionBar; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.ListExtractor; | import org.schabi.newpipe.extractor.ListExtractor; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| @@ -20,7 +22,6 @@ import org.schabi.newpipe.extractor.kiosk.KioskInfo; | |||||||
| import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; | import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; | ||||||
| import org.schabi.newpipe.extractor.localization.ContentCountry; | import org.schabi.newpipe.extractor.localization.ContentCountry; | ||||||
| import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | import org.schabi.newpipe.util.ExtractorHelper; | ||||||
| import org.schabi.newpipe.util.KioskTranslator; | import org.schabi.newpipe.util.KioskTranslator; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
| @@ -28,8 +29,6 @@ import org.schabi.newpipe.util.Localization; | |||||||
| import icepick.State; | import icepick.State; | ||||||
| import io.reactivex.rxjava3.core.Single; | import io.reactivex.rxjava3.core.Single; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.ktx.ViewUtils.animate; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Created by Christian Schabesberger on 23.09.17. |  * Created by Christian Schabesberger on 23.09.17. | ||||||
|  * <p> |  * <p> | ||||||
| @@ -82,6 +81,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> { | |||||||
|         return instance; |         return instance; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public KioskFragment() { | ||||||
|  |         super(UserAction.REQUESTED_KIOSK); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // LifeCycle |     // LifeCycle | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -102,9 +105,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> { | |||||||
|             try { |             try { | ||||||
|                 setTitle(kioskTranslatedName); |                 setTitle(kioskTranslatedName); | ||||||
|             } catch (final Exception e) { |             } catch (final Exception e) { | ||||||
|                 onUnrecoverableError(e, UserAction.UI_ERROR, |                 showSnackBarError(new ErrorInfo(e, UserAction.UI_ERROR, "Setting kiosk title")); | ||||||
|                         "none", |  | ||||||
|                         "none", R.string.app_ui_crash); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -157,34 +158,11 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> { | |||||||
|     // Contract |     // Contract | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void showLoading() { |  | ||||||
|         super.showLoading(); |  | ||||||
|         animate(itemsList, false, 100); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void handleResult(@NonNull final KioskInfo result) { |     public void handleResult(@NonNull final KioskInfo result) { | ||||||
|         super.handleResult(result); |         super.handleResult(result); | ||||||
|  |  | ||||||
|         name = kioskTranslatedName; |         name = kioskTranslatedName; | ||||||
|         setTitle(kioskTranslatedName); |         setTitle(kioskTranslatedName); | ||||||
|  |  | ||||||
|         if (!result.getErrors().isEmpty()) { |  | ||||||
|             showSnackBarError(result.getErrors(), |  | ||||||
|                     UserAction.REQUESTED_KIOSK, |  | ||||||
|                     NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void handleNextItems(final ListExtractor.InfoItemsPage result) { |  | ||||||
|         super.handleNextItems(result); |  | ||||||
|  |  | ||||||
|         if (!result.getErrors().isEmpty()) { |  | ||||||
|             showSnackBarError(result.getErrors(), |  | ||||||
|                     UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), |  | ||||||
|                     "Get next page of: " + url, 0); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ import android.view.ViewGroup; | |||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.app.AppCompatActivity; |  | ||||||
| import androidx.appcompat.content.res.AppCompatResources; | import androidx.appcompat.content.res.AppCompatResources; | ||||||
| import androidx.viewbinding.ViewBinding; | import androidx.viewbinding.ViewBinding; | ||||||
|  |  | ||||||
| @@ -25,11 +24,12 @@ import org.schabi.newpipe.R; | |||||||
| import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | ||||||
| import org.schabi.newpipe.databinding.PlaylistControlBinding; | import org.schabi.newpipe.databinding.PlaylistControlBinding; | ||||||
| import org.schabi.newpipe.databinding.PlaylistHeaderBinding; | import org.schabi.newpipe.databinding.PlaylistHeaderBinding; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.ListExtractor; | import org.schabi.newpipe.extractor.ListExtractor; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; |  | ||||||
| import org.schabi.newpipe.extractor.ServiceList; | import org.schabi.newpipe.extractor.ServiceList; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; |  | ||||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | ||||||
| import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; | import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||||
| @@ -40,8 +40,6 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager; | |||||||
| import org.schabi.newpipe.player.helper.PlayerHolder; | import org.schabi.newpipe.player.helper.PlayerHolder; | ||||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||||
| import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; | import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | import org.schabi.newpipe.util.ExtractorHelper; | ||||||
| import org.schabi.newpipe.util.ImageDisplayConstants; | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| import org.schabi.newpipe.util.KoreUtil; | import org.schabi.newpipe.util.KoreUtil; | ||||||
| @@ -62,6 +60,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; | |||||||
| import io.reactivex.rxjava3.disposables.Disposable; | import io.reactivex.rxjava3.disposables.Disposable; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.ktx.ViewUtils.animate; | import static org.schabi.newpipe.ktx.ViewUtils.animate; | ||||||
|  | import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; | ||||||
| import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; | import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; | ||||||
|  |  | ||||||
| public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||||
| @@ -87,6 +86,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | |||||||
|         return instance; |         return instance; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public PlaylistFragment() { | ||||||
|  |         super(UserAction.REQUESTED_PLAYLIST); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // LifeCycle |     // LifeCycle | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -262,7 +265,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | |||||||
|     public void showLoading() { |     public void showLoading() { | ||||||
|         super.showLoading(); |         super.showLoading(); | ||||||
|         animate(headerBinding.getRoot(), false, 200); |         animate(headerBinding.getRoot(), false, 200); | ||||||
|         animate(itemsList, false, 100); |         animateHideRecyclerViewAllowingScrolling(itemsList); | ||||||
|  |  | ||||||
|         IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView); |         IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView); | ||||||
|         animate(headerBinding.uploaderLayout, false, 200); |         animate(headerBinding.uploaderLayout, false, 200); | ||||||
| @@ -284,7 +287,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | |||||||
|                         NavigationHelper.openChannelFragment(getFM(), result.getServiceId(), |                         NavigationHelper.openChannelFragment(getFM(), result.getServiceId(), | ||||||
|                                 result.getUploaderUrl(), result.getUploaderName()); |                                 result.getUploaderUrl(), result.getUploaderName()); | ||||||
|                     } catch (final Exception e) { |                     } catch (final Exception e) { | ||||||
|                         ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); |                         ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e); | ||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
| @@ -315,8 +318,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | |||||||
|                 .localizeStreamCount(getContext(), result.getStreamCount())); |                 .localizeStreamCount(getContext(), result.getStreamCount())); | ||||||
|  |  | ||||||
|         if (!result.getErrors().isEmpty()) { |         if (!result.getErrors().isEmpty()) { | ||||||
|             showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, |             showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST, | ||||||
|                     NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); |                     result.getUrl(), result)); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         remotePlaylistManager.getPlaylist(result) |         remotePlaylistManager.getPlaylist(result) | ||||||
| @@ -363,33 +366,6 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void handleNextItems(final ListExtractor.InfoItemsPage result) { |  | ||||||
|         super.handleNextItems(result); |  | ||||||
|  |  | ||||||
|         if (!result.getErrors().isEmpty()) { |  | ||||||
|             showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, |  | ||||||
|                     NewPipe.getNameOfService(serviceId), "Get next page of: " + url, 0); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|     // OnError |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected boolean onError(final Throwable exception) { |  | ||||||
|         if (super.onError(exception)) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         final int errorId = exception instanceof ExtractionException |  | ||||||
|                 ? R.string.parsing_error : R.string.general_error; |  | ||||||
|         onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, |  | ||||||
|                 NewPipe.getNameOfService(serviceId), url, errorId); |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Utils |     // Utils | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -434,8 +410,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onError(final Throwable t) { |             public void onError(final Throwable throwable) { | ||||||
|                 PlaylistFragment.this.onError(t); |                 showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, | ||||||
|  |                         "Get playlist bookmarks")); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
| @@ -460,12 +437,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | |||||||
|         if (currentInfo != null && playlistEntity == null) { |         if (currentInfo != null && playlistEntity == null) { | ||||||
|             action = remotePlaylistManager.onBookmark(currentInfo) |             action = remotePlaylistManager.onBookmark(currentInfo) | ||||||
|                     .observeOn(AndroidSchedulers.mainThread()) |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                     .subscribe(ignored -> { /* Do nothing */ }, this::onError); |                     .subscribe(ignored -> { /* Do nothing */ }, throwable -> | ||||||
|  |                             showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, | ||||||
|  |                                     "Adding playlist bookmark"))); | ||||||
|         } else if (playlistEntity != null) { |         } else if (playlistEntity != null) { | ||||||
|             action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) |             action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid()) | ||||||
|                     .observeOn(AndroidSchedulers.mainThread()) |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                     .doFinally(() -> playlistEntity = null) |                     .doFinally(() -> playlistEntity = null) | ||||||
|                     .subscribe(ignored -> { /* Do nothing */ }, this::onError); |                     .subscribe(ignored -> { /* Do nothing */ }, throwable -> | ||||||
|  |                             showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, | ||||||
|  |                                     "Deleting playlist bookmark"))); | ||||||
|         } else { |         } else { | ||||||
|             action = Disposable.empty(); |             action = Disposable.empty(); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -35,16 +35,18 @@ import androidx.recyclerview.widget.ItemTouchHelper; | |||||||
| import androidx.recyclerview.widget.RecyclerView; | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.ReCaptchaActivity; |  | ||||||
| import org.schabi.newpipe.database.history.model.SearchHistoryEntry; | import org.schabi.newpipe.database.history.model.SearchHistoryEntry; | ||||||
| import org.schabi.newpipe.databinding.FragmentSearchBinding; | import org.schabi.newpipe.databinding.FragmentSearchBinding; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.ReCaptchaActivity; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.ListExtractor; | import org.schabi.newpipe.extractor.ListExtractor; | ||||||
| import org.schabi.newpipe.extractor.MetaInfo; | import org.schabi.newpipe.extractor.MetaInfo; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.Page; | import org.schabi.newpipe.extractor.Page; | ||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ParsingException; |  | ||||||
| import org.schabi.newpipe.extractor.search.SearchExtractor; | import org.schabi.newpipe.extractor.search.SearchExtractor; | ||||||
| import org.schabi.newpipe.extractor.search.SearchInfo; | import org.schabi.newpipe.extractor.search.SearchInfo; | ||||||
| import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory; | import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory; | ||||||
| @@ -54,9 +56,6 @@ import org.schabi.newpipe.fragments.list.BaseListFragment; | |||||||
| import org.schabi.newpipe.ktx.AnimationType; | import org.schabi.newpipe.ktx.AnimationType; | ||||||
| import org.schabi.newpipe.ktx.ExceptionUtils; | import org.schabi.newpipe.ktx.ExceptionUtils; | ||||||
| import org.schabi.newpipe.local.history.HistoryRecordManager; | import org.schabi.newpipe.local.history.HistoryRecordManager; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.Constants; | import org.schabi.newpipe.util.Constants; | ||||||
| import org.schabi.newpipe.util.DeviceUtils; | import org.schabi.newpipe.util.DeviceUtils; | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | import org.schabi.newpipe.util.ExtractorHelper; | ||||||
| @@ -162,11 +161,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|     private EditText searchEditText; |     private EditText searchEditText; | ||||||
|     private View searchClear; |     private View searchClear; | ||||||
|  |  | ||||||
|     private TextView correctSuggestion; |  | ||||||
|     private TextView metaInfoTextView; |  | ||||||
|     private View metaInfoSeparator; |  | ||||||
|  |  | ||||||
|     private View suggestionsPanel; |  | ||||||
|     private boolean suggestionsPanelVisible = false; |     private boolean suggestionsPanelVisible = false; | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////*/ |     /*////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -258,20 +252,23 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|         try { |         try { | ||||||
|             service = NewPipe.getService(serviceId); |             service = NewPipe.getService(serviceId); | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             ErrorActivity.reportError(getActivity(), e, requireActivity().getClass(), |             ErrorActivity.reportUiErrorInSnackbar(this, | ||||||
|                     requireActivity().findViewById(android.R.id.content), |                     "Getting service for id " + serviceId, e); | ||||||
|                     ErrorInfo.make(UserAction.UI_ERROR, |         } | ||||||
|                             "", |  | ||||||
|                             "", R.string.general_error)); |         if (suggestionDisposable == null || suggestionDisposable.isDisposed()) { | ||||||
|  |             initSuggestionObserver(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!TextUtils.isEmpty(searchString)) { |         if (!TextUtils.isEmpty(searchString)) { | ||||||
|             if (wasLoading.getAndSet(false)) { |             if (wasLoading.getAndSet(false)) { | ||||||
|                 search(searchString, contentFilter, sortFilter); |                 search(searchString, contentFilter, sortFilter); | ||||||
|  |                 return; | ||||||
|             } else if (infoListAdapter.getItemsList().isEmpty()) { |             } else if (infoListAdapter.getItemsList().isEmpty()) { | ||||||
|                 if (savedState == null) { |                 if (savedState == null) { | ||||||
|                     search(searchString, contentFilter, sortFilter); |                     search(searchString, contentFilter, sortFilter); | ||||||
|                 } else if (!isLoading.get() && !wasSearchFocused) { |                     return; | ||||||
|  |                 } else if (!isLoading.get() && !wasSearchFocused && lastPanelError == null) { | ||||||
|                     infoListAdapter.clearStreamItemList(); |                     infoListAdapter.clearStreamItemList(); | ||||||
|                     showEmptyState(); |                     showEmptyState(); | ||||||
|                 } |                 } | ||||||
| @@ -281,11 +278,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|         handleSearchSuggestion(); |         handleSearchSuggestion(); | ||||||
|  |  | ||||||
|         disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo), |         disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo), | ||||||
|                     metaInfoTextView, metaInfoSeparator)); |                     searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator)); | ||||||
|  |  | ||||||
|         if (suggestionDisposable == null || suggestionDisposable.isDisposed()) { |  | ||||||
|             initSuggestionObserver(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (TextUtils.isEmpty(searchString) || wasSearchFocused) { |         if (TextUtils.isEmpty(searchString) || wasSearchFocused) { | ||||||
|             showKeyboardSearch(); |             showKeyboardSearch(); | ||||||
| @@ -367,10 +360,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|         searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container); |         searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container); | ||||||
|         searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text); |         searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text); | ||||||
|         searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear); |         searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear); | ||||||
|  |  | ||||||
|         correctSuggestion = rootView.findViewById(R.id.correct_suggestion); |  | ||||||
|         metaInfoTextView = rootView.findViewById(R.id.search_meta_info_text_view); |  | ||||||
|         metaInfoSeparator = rootView.findViewById(R.id.search_meta_info_separator); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -413,7 +402,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|                 searchEditText.setText(""); |                 searchEditText.setText(""); | ||||||
|                 showKeyboardSearch(); |                 showKeyboardSearch(); | ||||||
|             } |             } | ||||||
|             animate(errorPanelRoot, false, 200); |             hideErrorPanel(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -540,7 +529,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|             if (DEBUG) { |             if (DEBUG) { | ||||||
|                 Log.d(TAG, "onClick() called with: v = [" + v + "]"); |                 Log.d(TAG, "onClick() called with: v = [" + v + "]"); | ||||||
|             } |             } | ||||||
|             if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { |             if (isSuggestionsEnabled && !isErrorPanelVisible()) { | ||||||
|                 showSuggestionsPanel(); |                 showSuggestionsPanel(); | ||||||
|             } |             } | ||||||
|             if (DeviceUtils.isTv(getContext())) { |             if (DeviceUtils.isTv(getContext())) { | ||||||
| @@ -553,8 +542,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|                 Log.d(TAG, "onFocusChange() called with: " |                 Log.d(TAG, "onFocusChange() called with: " | ||||||
|                         + "v = [" + v + "], hasFocus = [" + hasFocus + "]"); |                         + "v = [" + v + "], hasFocus = [" + hasFocus + "]"); | ||||||
|             } |             } | ||||||
|             if (isSuggestionsEnabled && hasFocus |             if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) { | ||||||
|                     && errorPanelRoot.getVisibility() != View.VISIBLE) { |  | ||||||
|                 showSuggestionsPanel(); |                 showSuggestionsPanel(); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| @@ -704,9 +692,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|                             .subscribe( |                             .subscribe( | ||||||
|                                     howManyDeleted -> suggestionPublisher |                                     howManyDeleted -> suggestionPublisher | ||||||
|                                             .onNext(searchEditText.getText().toString()), |                                             .onNext(searchEditText.getText().toString()), | ||||||
|                                     throwable -> showSnackBarError(throwable, |                                     throwable -> showSnackBarError(new ErrorInfo(throwable, | ||||||
|                                             UserAction.DELETE_FROM_HISTORY, "none", |                                             UserAction.DELETE_FROM_HISTORY, | ||||||
|                                             "Deleting item failed", R.string.general_error)); |                                             "Deleting item failed"))); | ||||||
|                     disposables.add(onDelete); |                     disposables.add(onDelete); | ||||||
|                 }) |                 }) | ||||||
|                 .show(); |                 .show(); | ||||||
| @@ -733,14 +721,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|             suggestionDisposable.dispose(); |             suggestionDisposable.dispose(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         final Observable<String> observable = suggestionPublisher |         suggestionDisposable = suggestionPublisher | ||||||
|                 .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) |                 .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) | ||||||
|                 .startWithItem(searchString != null |                 .startWithItem(searchString != null | ||||||
|                         ? searchString |                         ? searchString | ||||||
|                         : "") |                         : "") | ||||||
|                 .filter(ss -> isSuggestionsEnabled); |                 .filter(ss -> isSuggestionsEnabled) | ||||||
|  |  | ||||||
|         suggestionDisposable = observable |  | ||||||
|                 .switchMap(query -> { |                 .switchMap(query -> { | ||||||
|                     final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager |                     final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager | ||||||
|                             .getRelatedSearches(query, 3, 25); |                             .getRelatedSearches(query, 3, 25); | ||||||
| @@ -763,8 +749,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|                             .suggestionsFor(serviceId, query) |                             .suggestionsFor(serviceId, query) | ||||||
|                             .onErrorReturn(throwable -> { |                             .onErrorReturn(throwable -> { | ||||||
|                                 if (!ExceptionUtils.isNetworkRelated(throwable)) { |                                 if (!ExceptionUtils.isNetworkRelated(throwable)) { | ||||||
|                                     showSnackBarError(throwable, UserAction.GET_SUGGESTIONS, |                                     showSnackBarError(new ErrorInfo(throwable, | ||||||
|                                             NewPipe.getNameOfService(serviceId), searchString, 0); |                                             UserAction.GET_SUGGESTIONS, searchString, serviceId)); | ||||||
|                                 } |                                 } | ||||||
|                                 return new ArrayList<>(); |                                 return new ArrayList<>(); | ||||||
|                             }) |                             }) | ||||||
| @@ -800,7 +786,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|                     if (listNotification.isOnNext()) { |                     if (listNotification.isOnNext()) { | ||||||
|                         handleSuggestions(listNotification.getValue()); |                         handleSuggestions(listNotification.getValue()); | ||||||
|                     } else if (listNotification.isOnError()) { |                     } else if (listNotification.isOnError()) { | ||||||
|                         onSuggestionError(listNotification.getError()); |                         showError(new ErrorInfo(listNotification.getError(), | ||||||
|  |                                 UserAction.GET_SUGGESTIONS, searchString, serviceId)); | ||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|     } |     } | ||||||
| @@ -832,8 +819,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|                         .subscribe(intent -> { |                         .subscribe(intent -> { | ||||||
|                             getFM().popBackStackImmediate(); |                             getFM().popBackStackImmediate(); | ||||||
|                             activity.startActivity(intent); |                             activity.startActivity(intent); | ||||||
|                         }, throwable -> |                         }, throwable -> showTextError(getString(R.string.unsupported_url)))); | ||||||
|                                 showError(getString(R.string.unsupported_url), false))); |  | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|         } catch (final Exception ignored) { |         } catch (final Exception ignored) { | ||||||
| @@ -844,15 +830,16 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|         this.searchString = theSearchString; |         this.searchString = theSearchString; | ||||||
|         infoListAdapter.clearStreamItemList(); |         infoListAdapter.clearStreamItemList(); | ||||||
|         hideSuggestionsPanel(); |         hideSuggestionsPanel(); | ||||||
|  |         showMetaInfoInTextView(null, searchBinding.searchMetaInfoTextView, | ||||||
|  |                 searchBinding.searchMetaInfoSeparator); | ||||||
|         hideKeyboardSearch(); |         hideKeyboardSearch(); | ||||||
|  |  | ||||||
|         disposables.add(historyRecordManager.onSearched(serviceId, theSearchString) |         disposables.add(historyRecordManager.onSearched(serviceId, theSearchString) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe( |                 .subscribe( | ||||||
|                         ignored -> { |                         ignored -> { }, | ||||||
|                         }, |                         throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED, | ||||||
|                         error -> showSnackBarError(error, UserAction.SEARCHED, |                                 theSearchString, serviceId)) | ||||||
|                                 NewPipe.getNameOfService(serviceId), theSearchString, 0) |  | ||||||
|                 )); |                 )); | ||||||
|         suggestionPublisher.onNext(theSearchString); |         suggestionPublisher.onNext(theSearchString); | ||||||
|         startLoading(false); |         startLoading(false); | ||||||
| @@ -872,7 +859,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|                 .subscribeOn(Schedulers.io()) |                 .subscribeOn(Schedulers.io()) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .doOnEvent((searchResult, throwable) -> isLoading.set(false)) |                 .doOnEvent((searchResult, throwable) -> isLoading.set(false)) | ||||||
|                 .subscribe(this::handleResult, this::onError); |                 .subscribe(this::handleResult, this::onItemError); | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -895,7 +882,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|                 .subscribeOn(Schedulers.io()) |                 .subscribeOn(Schedulers.io()) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .doOnEvent((nextItemsResult, throwable) -> isLoading.set(false)) |                 .doOnEvent((nextItemsResult, throwable) -> isLoading.set(false)) | ||||||
|                 .subscribe(this::handleNextItems, this::onError); |                 .subscribe(this::handleNextItems, this::onItemError); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -909,6 +896,15 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|         hideKeyboardSearch(); |         hideKeyboardSearch(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private void onItemError(final Throwable exception) { | ||||||
|  |         if (exception instanceof SearchExtractor.NothingFoundException) { | ||||||
|  |             infoListAdapter.clearStreamItemList(); | ||||||
|  |             showEmptyState(); | ||||||
|  |         } else { | ||||||
|  |             showError(new ErrorInfo(exception, UserAction.SEARCHED, searchString, serviceId)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Utils |     // Utils | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -945,26 +941,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|         searchBinding.suggestionsList.smoothScrollToPosition(0); |         searchBinding.suggestionsList.smoothScrollToPosition(0); | ||||||
|         searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions)); |         searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions)); | ||||||
|  |  | ||||||
|         if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) { |         if (suggestionsPanelVisible && isErrorPanelVisible()) { | ||||||
|             hideLoading(); |             hideLoading(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onSuggestionError(final Throwable exception) { |  | ||||||
|         if (DEBUG) { |  | ||||||
|             Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]"); |  | ||||||
|         } |  | ||||||
|         if (super.onError(exception)) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         final int errorId = exception instanceof ParsingException |  | ||||||
|                 ? R.string.parsing_error |  | ||||||
|                 : R.string.general_error; |  | ||||||
|         onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, |  | ||||||
|                 NewPipe.getNameOfService(serviceId), searchString, errorId); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Contract |     // Contract | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -975,13 +956,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|         showListFooter(false); |         showListFooter(false); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void showError(final String message, final boolean showRetryButton) { |  | ||||||
|         super.showError(message, showRetryButton); |  | ||||||
|         hideSuggestionsPanel(); |  | ||||||
|         hideKeyboardSearch(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Search Results |     // Search Results | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -992,8 +966,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|         if (!exceptions.isEmpty() |         if (!exceptions.isEmpty() | ||||||
|                 && !(exceptions.size() == 1 |                 && !(exceptions.size() == 1 | ||||||
|                 && exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) { |                 && exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) { | ||||||
|             showSnackBarError(result.getErrors(), UserAction.SEARCHED, |             showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED, | ||||||
|                     NewPipe.getNameOfService(serviceId), searchString, 0); |                     searchString, serviceId)); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         searchSuggestion = result.getSearchSuggestion(); |         searchSuggestion = result.getSearchSuggestion(); | ||||||
| @@ -1002,8 +976,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|         // List<MetaInfo> cannot be bundled without creating some containers |         // List<MetaInfo> cannot be bundled without creating some containers | ||||||
|         metaInfo = new MetaInfo[result.getMetaInfo().size()]; |         metaInfo = new MetaInfo[result.getMetaInfo().size()]; | ||||||
|         metaInfo = result.getMetaInfo().toArray(metaInfo); |         metaInfo = result.getMetaInfo().toArray(metaInfo); | ||||||
|         disposables.add(showMetaInfoInTextView(result.getMetaInfo(), metaInfoTextView, |         disposables.add(showMetaInfoInTextView(result.getMetaInfo(), | ||||||
|                 metaInfoSeparator)); |                 searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator)); | ||||||
|  |  | ||||||
|         handleSearchSuggestion(); |         handleSearchSuggestion(); | ||||||
|  |  | ||||||
| @@ -1061,33 +1035,20 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|         nextPage = result.getNextPage(); |         nextPage = result.getNextPage(); | ||||||
|  |  | ||||||
|         if (!result.getErrors().isEmpty()) { |         if (!result.getErrors().isEmpty()) { | ||||||
|             showSnackBarError(result.getErrors(), UserAction.SEARCHED, |             showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED, | ||||||
|                     NewPipe.getNameOfService(serviceId), |  | ||||||
|                     "\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", " |                     "\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", " | ||||||
|                             + "pageIds: " + nextPage.getIds() + ", " |                             + "pageIds: " + nextPage.getIds() + ", " | ||||||
|                             + "pageCookies: " + nextPage.getCookies(), 0); |                             + "pageCookies: " + nextPage.getCookies(), | ||||||
|  |                     serviceId)); | ||||||
|         } |         } | ||||||
|         super.handleNextItems(result); |         super.handleNextItems(result); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected boolean onError(final Throwable exception) { |     public void handleError() { | ||||||
|         if (super.onError(exception)) { |         super.handleError(); | ||||||
|             return true; |         hideSuggestionsPanel(); | ||||||
|         } |         hideKeyboardSearch(); | ||||||
|  |  | ||||||
|         if (exception instanceof SearchExtractor.NothingFoundException) { |  | ||||||
|             infoListAdapter.clearStreamItemList(); |  | ||||||
|             showEmptyState(); |  | ||||||
|         } else { |  | ||||||
|             final int errorId = exception instanceof ParsingException |  | ||||||
|                     ? R.string.parsing_error |  | ||||||
|                     : R.string.general_error; |  | ||||||
|             onUnrecoverableError(exception, UserAction.SEARCHED, |  | ||||||
|                     NewPipe.getNameOfService(serviceId), searchString, errorId); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return true; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -1113,9 +1074,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | |||||||
|                 .subscribe( |                 .subscribe( | ||||||
|                         howManyDeleted -> suggestionPublisher |                         howManyDeleted -> suggestionPublisher | ||||||
|                                 .onNext(searchEditText.getText().toString()), |                                 .onNext(searchEditText.getText().toString()), | ||||||
|                         throwable -> showSnackBarError(throwable, |                         throwable -> showSnackBarError(new ErrorInfo(throwable, | ||||||
|                                 UserAction.DELETE_FROM_HISTORY, "none", |                                 UserAction.DELETE_FROM_HISTORY, "Deleting item failed"))); | ||||||
|                                 "Deleting item failed", R.string.general_error)); |  | ||||||
|         disposables.add(onDelete); |         disposables.add(onDelete); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -16,12 +16,11 @@ import androidx.viewbinding.ViewBinding; | |||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.databinding.RelatedStreamsHeaderBinding; | import org.schabi.newpipe.databinding.RelatedStreamsHeaderBinding; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.ListExtractor; | import org.schabi.newpipe.extractor.ListExtractor; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; |  | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
| import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | import org.schabi.newpipe.fragments.list.BaseListInfoFragment; | ||||||
| import org.schabi.newpipe.ktx.ViewUtils; | import org.schabi.newpipe.ktx.ViewUtils; | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.RelatedStreamInfo; | import org.schabi.newpipe.util.RelatedStreamInfo; | ||||||
|  |  | ||||||
| import java.io.Serializable; | import java.io.Serializable; | ||||||
| @@ -47,6 +46,10 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf | |||||||
|         return instance; |         return instance; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public RelatedVideosFragment() { | ||||||
|  |         super(UserAction.REQUESTED_STREAM); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // LifeCycle |     // LifeCycle | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -125,43 +128,9 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf | |||||||
|         } |         } | ||||||
|         ViewUtils.slideUp(requireView(), 120, 96, 0.06f); |         ViewUtils.slideUp(requireView(), 120, 96, 0.06f); | ||||||
|  |  | ||||||
|         if (!result.getErrors().isEmpty()) { |  | ||||||
|             showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, |  | ||||||
|                     NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         disposables.clear(); |         disposables.clear(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void handleNextItems(final ListExtractor.InfoItemsPage result) { |  | ||||||
|         super.handleNextItems(result); |  | ||||||
|  |  | ||||||
|         if (!result.getErrors().isEmpty()) { |  | ||||||
|             showSnackBarError(result.getErrors(), |  | ||||||
|                     UserAction.REQUESTED_STREAM, |  | ||||||
|                     NewPipe.getNameOfService(serviceId), |  | ||||||
|                     "Get next page of: " + url, |  | ||||||
|                     R.string.general_error); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|     // OnError |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected boolean onError(final Throwable exception) { |  | ||||||
|         if (super.onError(exception)) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         hideLoading(); |  | ||||||
|         showSnackBarError(exception, UserAction.REQUESTED_STREAM, |  | ||||||
|                 NewPipe.getNameOfService(serviceId), url, R.string.general_error); |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Utils |     // Utils | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -190,11 +159,9 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf | |||||||
|     @Override |     @Override | ||||||
|     protected void onRestoreInstanceState(@NonNull final Bundle savedState) { |     protected void onRestoreInstanceState(@NonNull final Bundle savedState) { | ||||||
|         super.onRestoreInstanceState(savedState); |         super.onRestoreInstanceState(savedState); | ||||||
|         if (savedState != null) { |         final Serializable serializable = savedState.getSerializable(INFO_KEY); | ||||||
|             final Serializable serializable = savedState.getSerializable(INFO_KEY); |         if (serializable instanceof RelatedStreamInfo) { | ||||||
|             if (serializable instanceof RelatedStreamInfo) { |             this.relatedStreamInfo = (RelatedStreamInfo) serializable; | ||||||
|                 this.relatedStreamInfo = (RelatedStreamInfo) serializable; |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,11 +14,11 @@ import androidx.appcompat.app.AppCompatActivity; | |||||||
| import androidx.preference.PreferenceManager; | import androidx.preference.PreferenceManager; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.comments.CommentsInfoItem; | import org.schabi.newpipe.extractor.comments.CommentsInfoItem; | ||||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||||
| import org.schabi.newpipe.local.history.HistoryRecordManager; | import org.schabi.newpipe.local.history.HistoryRecordManager; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.util.CommentTextOnTouchListener; | import org.schabi.newpipe.util.CommentTextOnTouchListener; | ||||||
| import org.schabi.newpipe.util.DeviceUtils; | import org.schabi.newpipe.util.DeviceUtils; | ||||||
| import org.schabi.newpipe.util.ImageDisplayConstants; | import org.schabi.newpipe.util.ImageDisplayConstants; | ||||||
| @@ -171,15 +171,15 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { | |||||||
|         if (TextUtils.isEmpty(item.getUploaderUrl())) { |         if (TextUtils.isEmpty(item.getUploaderUrl())) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |         final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext(); | ||||||
|         try { |         try { | ||||||
|             final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext(); |  | ||||||
|             NavigationHelper.openChannelFragment( |             NavigationHelper.openChannelFragment( | ||||||
|                     activity.getSupportFragmentManager(), |                     activity.getSupportFragmentManager(), | ||||||
|                     item.getServiceId(), |                     item.getServiceId(), | ||||||
|                     item.getUploaderUrl(), |                     item.getUploaderUrl(), | ||||||
|                     item.getUploaderName()); |                     item.getUploaderName()); | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e); |             ErrorActivity.reportUiErrorInSnackbar(activity, "Opening channel fragment", e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -319,6 +319,16 @@ fun View.slideUp(duration: Long, delay: Long, @FloatRange(from = 0.0, to = 1.0) | |||||||
|         .start() |         .start() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Instead of hiding normally using [animate], which would make | ||||||
|  |  * the recycler view unable to capture touches after being hidden, this just animates the alpha | ||||||
|  |  * value setting it to `0.0` after `200` milliseconds. | ||||||
|  |  */ | ||||||
|  | fun View.animateHideRecyclerViewAllowingScrolling() { | ||||||
|  |     // not hiding normally because the view needs to still capture touches and allow scroll | ||||||
|  |     animate().alpha(0.0f).setDuration(200).start() | ||||||
|  | } | ||||||
|  |  | ||||||
| enum class AnimationType { | enum class AnimationType { | ||||||
|     ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA |     ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA | ||||||
| } | } | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ import org.schabi.newpipe.fragments.BaseStateFragment; | |||||||
| import org.schabi.newpipe.fragments.list.ListViewContract; | import org.schabi.newpipe.fragments.list.ListViewContract; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.ktx.ViewUtils.animate; | import static org.schabi.newpipe.ktx.ViewUtils.animate; | ||||||
|  | import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * This fragment is design to be used with persistent data such as |  * This fragment is design to be used with persistent data such as | ||||||
| @@ -184,7 +185,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I> | |||||||
|     public void showLoading() { |     public void showLoading() { | ||||||
|         super.showLoading(); |         super.showLoading(); | ||||||
|         if (itemsList != null) { |         if (itemsList != null) { | ||||||
|             animate(itemsList, false, 200); |             animateHideRecyclerViewAllowingScrolling(itemsList); | ||||||
|         } |         } | ||||||
|         if (headerRootBinding != null) { |         if (headerRootBinding != null) { | ||||||
|             animate(headerRootBinding.getRoot(), false, 200); |             animate(headerRootBinding.getRoot(), false, 200); | ||||||
| @@ -202,19 +203,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I> | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void showError(final String message, final boolean showRetryButton) { |  | ||||||
|         super.showError(message, showRetryButton); |  | ||||||
|         showListFooter(false); |  | ||||||
|  |  | ||||||
|         if (itemsList != null) { |  | ||||||
|             animate(itemsList, false, 200); |  | ||||||
|         } |  | ||||||
|         if (headerRootBinding != null) { |  | ||||||
|             animate(headerRootBinding.getRoot(), false, 200); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void showEmptyState() { |     public void showEmptyState() { | ||||||
|         super.showEmptyState(); |         super.showEmptyState(); | ||||||
| @@ -249,9 +237,18 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I> | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected boolean onError(final Throwable exception) { |     public void handleError() { | ||||||
|  |         super.handleError(); | ||||||
|         resetFragment(); |         resetFragment(); | ||||||
|         return super.onError(exception); |  | ||||||
|  |         showListFooter(false); | ||||||
|  |  | ||||||
|  |         if (itemsList != null) { | ||||||
|  |             animateHideRecyclerViewAllowingScrolling(itemsList); | ||||||
|  |         } | ||||||
|  |         if (headerRootBinding != null) { | ||||||
|  |             animate(headerRootBinding.getRoot(), false, 200); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -23,10 +23,11 @@ import org.schabi.newpipe.database.LocalItem; | |||||||
| import org.schabi.newpipe.database.playlist.PlaylistLocalItem; | import org.schabi.newpipe.database.playlist.PlaylistLocalItem; | ||||||
| import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; | import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; | ||||||
| import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.local.BaseLocalListFragment; | import org.schabi.newpipe.local.BaseLocalListFragment; | ||||||
| import org.schabi.newpipe.local.playlist.LocalPlaylistManager; | import org.schabi.newpipe.local.playlist.LocalPlaylistManager; | ||||||
| import org.schabi.newpipe.local.playlist.RemotePlaylistManager; | import org.schabi.newpipe.local.playlist.RemotePlaylistManager; | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.OnClickGesture; | import org.schabi.newpipe.util.OnClickGesture; | ||||||
|  |  | ||||||
| @@ -206,7 +207,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL | |||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onError(final Throwable exception) { |             public void onError(final Throwable exception) { | ||||||
|                 BookmarkFragment.this.onError(exception); |                 showError(new ErrorInfo(exception, | ||||||
|  |                         UserAction.REQUESTED_BOOKMARK, "Loading playlists")); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
| @@ -237,17 +239,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL | |||||||
|     // Fragment Error Handling |     // Fragment Error Handling | ||||||
|     /////////////////////////////////////////////////////////////////////////// |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected boolean onError(final Throwable exception) { |  | ||||||
|         if (super.onError(exception)) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, |  | ||||||
|                 "none", "Bookmark", R.string.general_error); |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void resetFragment() { |     protected void resetFragment() { | ||||||
|         super.resetFragment(); |         super.resetFragment(); | ||||||
| @@ -295,8 +286,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL | |||||||
|                 .setPositiveButton(R.string.delete, (dialog, i) -> |                 .setPositiveButton(R.string.delete, (dialog, i) -> | ||||||
|                         disposables.add(deleteReactor |                         disposables.add(deleteReactor | ||||||
|                                 .observeOn(AndroidSchedulers.mainThread()) |                                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                                 .subscribe(ignored -> { /*Do nothing on success*/ }, this::onError)) |                                 .subscribe(ignored -> { /*Do nothing on success*/ }, throwable -> | ||||||
|                 ) |                                         showError(new ErrorInfo(throwable, | ||||||
|  |                                                 UserAction.REQUESTED_BOOKMARK, | ||||||
|  |                                                 "Deleting playlist"))))) | ||||||
|                 .setNegativeButton(R.string.cancel, null) |                 .setNegativeButton(R.string.cancel, null) | ||||||
|                 .show(); |                 .show(); | ||||||
|     } |     } | ||||||
| @@ -314,7 +307,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL | |||||||
|         localPlaylistManager.renamePlaylist(id, name); |         localPlaylistManager.renamePlaylist(id, name); | ||||||
|         final Disposable disposable = localPlaylistManager.renamePlaylist(id, name) |         final Disposable disposable = localPlaylistManager.renamePlaylist(id, name) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(longs -> { /*Do nothing on success*/ }, this::onError); |                 .subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError( | ||||||
|  |                         new ErrorInfo(throwable, | ||||||
|  |                                 UserAction.REQUESTED_BOOKMARK, | ||||||
|  |                                 "Changing playlist name"))); | ||||||
|         disposables.add(disposable); |         disposables.add(disposable); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -38,17 +38,18 @@ import icepick.State | |||||||
| import org.schabi.newpipe.R | import org.schabi.newpipe.R | ||||||
| import org.schabi.newpipe.database.feed.model.FeedGroupEntity | import org.schabi.newpipe.database.feed.model.FeedGroupEntity | ||||||
| import org.schabi.newpipe.databinding.FragmentFeedBinding | import org.schabi.newpipe.databinding.FragmentFeedBinding | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo | ||||||
|  | import org.schabi.newpipe.error.UserAction | ||||||
| import org.schabi.newpipe.fragments.list.BaseListFragment | import org.schabi.newpipe.fragments.list.BaseListFragment | ||||||
| import org.schabi.newpipe.ktx.animate | import org.schabi.newpipe.ktx.animate | ||||||
|  | import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling | ||||||
| import org.schabi.newpipe.local.feed.service.FeedLoadService | import org.schabi.newpipe.local.feed.service.FeedLoadService | ||||||
| import org.schabi.newpipe.report.UserAction |  | ||||||
| import org.schabi.newpipe.util.Localization | import org.schabi.newpipe.util.Localization | ||||||
| import java.util.Calendar | import java.util.Calendar | ||||||
|  |  | ||||||
| class FeedFragment : BaseListFragment<FeedState, Unit>() { | class FeedFragment : BaseListFragment<FeedState, Unit>() { | ||||||
|     private var _feedBinding: FragmentFeedBinding? = null |     private var _feedBinding: FragmentFeedBinding? = null | ||||||
|     private val feedBinding get() = _feedBinding!! |     private val feedBinding get() = _feedBinding!! | ||||||
|     private val errorBinding get() = _feedBinding!!.errorPanel |  | ||||||
|  |  | ||||||
|     private lateinit var viewModel: FeedViewModel |     private lateinit var viewModel: FeedViewModel | ||||||
|     @State |     @State | ||||||
| @@ -106,7 +107,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() { | |||||||
|     override fun initListeners() { |     override fun initListeners() { | ||||||
|         super.initListeners() |         super.initListeners() | ||||||
|         feedBinding.refreshRootView.setOnClickListener { reloadContent() } |         feedBinding.refreshRootView.setOnClickListener { reloadContent() } | ||||||
|         feedBinding.swiperefresh.setOnRefreshListener { reloadContent() } |         feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // ///////////////////////////////////////////////////////////////////////// |     // ///////////////////////////////////////////////////////////////////////// | ||||||
| @@ -171,50 +172,26 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() { | |||||||
|     // ///////////////////////////////////////////////////////////////////////// |     // ///////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|     override fun showLoading() { |     override fun showLoading() { | ||||||
|  |         super.showLoading() | ||||||
|  |         feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling() | ||||||
|         feedBinding.refreshRootView.animate(false, 0) |         feedBinding.refreshRootView.animate(false, 0) | ||||||
|         feedBinding.itemsList.animate(false, 0) |  | ||||||
|  |  | ||||||
|         feedBinding.loadingProgressBar.animate(true, 200) |  | ||||||
|         feedBinding.loadingProgressText.animate(true, 200) |         feedBinding.loadingProgressText.animate(true, 200) | ||||||
|  |         feedBinding.swipeRefreshLayout.isRefreshing = true | ||||||
|         feedBinding.emptyStateView.root.animate(false, 0) |  | ||||||
|         errorBinding.root.animate(false, 0) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun hideLoading() { |     override fun hideLoading() { | ||||||
|  |         super.hideLoading() | ||||||
|         feedBinding.refreshRootView.animate(true, 200) |         feedBinding.refreshRootView.animate(true, 200) | ||||||
|         feedBinding.itemsList.animate(true, 300) |  | ||||||
|  |  | ||||||
|         feedBinding.loadingProgressBar.animate(false, 0) |  | ||||||
|         feedBinding.loadingProgressText.animate(false, 0) |         feedBinding.loadingProgressText.animate(false, 0) | ||||||
|  |         feedBinding.swipeRefreshLayout.isRefreshing = false | ||||||
|         feedBinding.emptyStateView.root.animate(false, 0) |  | ||||||
|         errorBinding.root.animate(false, 0) |  | ||||||
|         feedBinding.swiperefresh.isRefreshing = false |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun showEmptyState() { |     override fun showEmptyState() { | ||||||
|  |         super.showEmptyState() | ||||||
|  |         feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling() | ||||||
|         feedBinding.refreshRootView.animate(true, 200) |         feedBinding.refreshRootView.animate(true, 200) | ||||||
|         feedBinding.itemsList.animate(false, 0) |  | ||||||
|  |  | ||||||
|         feedBinding.loadingProgressBar.animate(false, 0) |  | ||||||
|         feedBinding.loadingProgressText.animate(false, 0) |         feedBinding.loadingProgressText.animate(false, 0) | ||||||
|  |         feedBinding.swipeRefreshLayout.isRefreshing = false | ||||||
|         feedBinding.emptyStateView.root.animate(true, 800) |  | ||||||
|         errorBinding.root.animate(false, 0) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun showError(message: String, showRetryButton: Boolean) { |  | ||||||
|         infoListAdapter.clearStreamItemList() |  | ||||||
|         feedBinding.refreshRootView.animate(false, 120) |  | ||||||
|         feedBinding.itemsList.animate(false, 120) |  | ||||||
|  |  | ||||||
|         feedBinding.loadingProgressBar.animate(false, 120) |  | ||||||
|         feedBinding.loadingProgressText.animate(false, 120) |  | ||||||
|  |  | ||||||
|         errorBinding.errorMessageView.text = message |  | ||||||
|         errorBinding.errorButtonRetry.animate(showRetryButton, if (showRetryButton) 600 else 0) |  | ||||||
|         errorBinding.root.animate(true, 300) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun handleResult(result: FeedState) { |     override fun handleResult(result: FeedState) { | ||||||
| @@ -227,6 +204,15 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() { | |||||||
|         updateRefreshViewState() |         updateRefreshViewState() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override fun handleError() { | ||||||
|  |         super.handleError() | ||||||
|  |         infoListAdapter.clearStreamItemList() | ||||||
|  |         feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling() | ||||||
|  |         feedBinding.refreshRootView.animate(false, 0) | ||||||
|  |         feedBinding.loadingProgressText.animate(false, 0) | ||||||
|  |         feedBinding.swipeRefreshLayout.isRefreshing = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private fun handleProgressState(progressState: FeedState.ProgressState) { |     private fun handleProgressState(progressState: FeedState.ProgressState) { | ||||||
|         showLoading() |         showLoading() | ||||||
|  |  | ||||||
| @@ -266,13 +252,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() { | |||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (loadedState.itemsErrors.isNotEmpty()) { |  | ||||||
|             showSnackBarError( |  | ||||||
|                 loadedState.itemsErrors, UserAction.REQUESTED_FEED, |  | ||||||
|                 "none", "Loading feed", R.string.general_error |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (loadedState.items.isEmpty()) { |         if (loadedState.items.isEmpty()) { | ||||||
|             showEmptyState() |             showEmptyState() | ||||||
|         } else { |         } else { | ||||||
| @@ -281,12 +260,13 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private fun handleErrorState(errorState: FeedState.ErrorState): Boolean { |     private fun handleErrorState(errorState: FeedState.ErrorState): Boolean { | ||||||
|         hideLoading() |         return if (errorState.error == null) { | ||||||
|         errorState.error?.let { |             hideLoading() | ||||||
|             onError(errorState.error) |             false | ||||||
|             return true |         } else { | ||||||
|  |             showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed")) | ||||||
|  |             true | ||||||
|         } |         } | ||||||
|         return false |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private fun updateRelativeTimeViews() { |     private fun updateRelativeTimeViews() { | ||||||
| @@ -320,18 +300,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() { | |||||||
|         listState = null |         listState = null | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun onError(exception: Throwable): Boolean { |  | ||||||
|         if (super.onError(exception)) return true |  | ||||||
|  |  | ||||||
|         if (useAsFrontPage) { |  | ||||||
|             showSnackBarError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0) |  | ||||||
|             return true |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         onUnrecoverableError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0) |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     companion object { |     companion object { | ||||||
|         const val KEY_GROUP_ID = "ARG_GROUP_ID" |         const val KEY_GROUP_ID = "ARG_GROUP_ID" | ||||||
|         const val KEY_GROUP_NAME = "ARG_GROUP_NAME" |         const val KEY_GROUP_NAME = "ARG_GROUP_NAME" | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ import android.widget.Toast; | |||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.app.AlertDialog; |  | ||||||
| import androidx.viewbinding.ViewBinding; | import androidx.viewbinding.ViewBinding; | ||||||
|  |  | ||||||
| import com.google.android.material.snackbar.Snackbar; | import com.google.android.material.snackbar.Snackbar; | ||||||
| @@ -27,6 +26,8 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry; | |||||||
| import org.schabi.newpipe.database.stream.model.StreamEntity; | import org.schabi.newpipe.database.stream.model.StreamEntity; | ||||||
| import org.schabi.newpipe.databinding.PlaylistControlBinding; | import org.schabi.newpipe.databinding.PlaylistControlBinding; | ||||||
| import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding; | import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamType; | import org.schabi.newpipe.extractor.stream.StreamType; | ||||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | import org.schabi.newpipe.info_list.InfoItemDialog; | ||||||
| @@ -34,10 +35,7 @@ import org.schabi.newpipe.local.BaseLocalListFragment; | |||||||
| import org.schabi.newpipe.player.helper.PlayerHolder; | import org.schabi.newpipe.player.helper.PlayerHolder; | ||||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; | import org.schabi.newpipe.settings.HistorySettingsFragment; | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.settings.SettingsActivity; |  | ||||||
| import org.schabi.newpipe.util.KoreUtil; | import org.schabi.newpipe.util.KoreUtil; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.OnClickGesture; | import org.schabi.newpipe.util.OnClickGesture; | ||||||
| @@ -49,6 +47,7 @@ import java.util.Arrays; | |||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.Comparator; | import java.util.Comparator; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Objects; | ||||||
|  |  | ||||||
| import icepick.State; | import icepick.State; | ||||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | ||||||
| @@ -163,48 +162,11 @@ public class StatisticsPlaylistFragment | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean onOptionsItemSelected(final MenuItem item) { |     public boolean onOptionsItemSelected(final MenuItem item) { | ||||||
|         switch (item.getItemId()) { |         if (item.getItemId() == R.id.action_history_clear) { | ||||||
|             case R.id.action_history_clear: |             HistorySettingsFragment | ||||||
|                 new AlertDialog.Builder(activity) |                     .openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables); | ||||||
|                         .setTitle(R.string.delete_view_history_alert) |         } else { | ||||||
|                         .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) |             return super.onOptionsItemSelected(item); | ||||||
|                         .setPositiveButton(R.string.delete, ((dialog, which) -> { |  | ||||||
|                             final Disposable onDelete = recordManager.deleteWholeStreamHistory() |  | ||||||
|                                     .observeOn(AndroidSchedulers.mainThread()) |  | ||||||
|                                     .subscribe( |  | ||||||
|                                             howManyDeleted -> Toast.makeText(getContext(), |  | ||||||
|                                                     R.string.watch_history_deleted, |  | ||||||
|                                                     Toast.LENGTH_SHORT).show(), |  | ||||||
|                                             throwable -> ErrorActivity.reportError(getContext(), |  | ||||||
|                                                     throwable, |  | ||||||
|                                                     SettingsActivity.class, null, |  | ||||||
|                                                     ErrorInfo.make( |  | ||||||
|                                                             UserAction.DELETE_FROM_HISTORY, |  | ||||||
|                                                             "none", |  | ||||||
|                                                             "Delete view history", |  | ||||||
|                                                             R.string.general_error))); |  | ||||||
|  |  | ||||||
|                             final Disposable onClearOrphans = recordManager.removeOrphanedRecords() |  | ||||||
|                                     .observeOn(AndroidSchedulers.mainThread()) |  | ||||||
|                                     .subscribe( |  | ||||||
|                                             howManyDeleted -> { |  | ||||||
|                                             }, |  | ||||||
|                                             throwable -> ErrorActivity.reportError(getContext(), |  | ||||||
|                                                     throwable, |  | ||||||
|                                                     SettingsActivity.class, null, |  | ||||||
|                                                     ErrorInfo.make( |  | ||||||
|                                                             UserAction.DELETE_FROM_HISTORY, |  | ||||||
|                                                             "none", |  | ||||||
|                                                             "Delete search history", |  | ||||||
|                                                             R.string.general_error))); |  | ||||||
|                             disposables.add(onClearOrphans); |  | ||||||
|                             disposables.add(onDelete); |  | ||||||
|                         })) |  | ||||||
|                         .create() |  | ||||||
|                         .show(); |  | ||||||
|                 break; |  | ||||||
|             default: |  | ||||||
|                 return super.onOptionsItemSelected(item); |  | ||||||
|         } |         } | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| @@ -228,7 +190,7 @@ public class StatisticsPlaylistFragment | |||||||
|     @Override |     @Override | ||||||
|     public void onPause() { |     public void onPause() { | ||||||
|         super.onPause(); |         super.onPause(); | ||||||
|         itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); |         itemsListState = Objects.requireNonNull(itemsList.getLayoutManager()).onSaveInstanceState(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -287,7 +249,8 @@ public class StatisticsPlaylistFragment | |||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onError(final Throwable exception) { |             public void onError(final Throwable exception) { | ||||||
|                 StatisticsPlaylistFragment.this.onError(exception); |                 showError( | ||||||
|  |                         new ErrorInfo(exception, UserAction.SOMETHING_ELSE, "History Statistics")); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
| @@ -313,7 +276,7 @@ public class StatisticsPlaylistFragment | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         itemListAdapter.addItems(processResult(result)); |         itemListAdapter.addItems(processResult(result)); | ||||||
|         if (itemsListState != null) { |         if (itemsListState != null && itemsList.getLayoutManager() != null) { | ||||||
|             itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); |             itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); | ||||||
|             itemsListState = null; |             itemsListState = null; | ||||||
|         } |         } | ||||||
| @@ -341,17 +304,6 @@ public class StatisticsPlaylistFragment | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected boolean onError(final Throwable exception) { |  | ||||||
|         if (super.onError(exception)) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, |  | ||||||
|                 "none", "History Statistics", R.string.general_error); |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Utils |     // Utils | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -439,9 +391,8 @@ public class StatisticsPlaylistFragment | |||||||
|                                             Toast.LENGTH_SHORT).show(); |                                             Toast.LENGTH_SHORT).show(); | ||||||
|                                 } |                                 } | ||||||
|                             }, |                             }, | ||||||
|                             throwable -> showSnackBarError(throwable, |                             throwable -> showSnackBarError(new ErrorInfo(throwable, | ||||||
|                                     UserAction.DELETE_FROM_HISTORY, "none", |                                     UserAction.DELETE_FROM_HISTORY, "Deleting item"))); | ||||||
|                                     "Deleting item failed", R.string.general_error)); |  | ||||||
|  |  | ||||||
|             disposables.add(onDelete); |             disposables.add(onDelete); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -34,6 +34,8 @@ import org.schabi.newpipe.database.stream.model.StreamEntity; | |||||||
| import org.schabi.newpipe.database.stream.model.StreamStateEntity; | import org.schabi.newpipe.database.stream.model.StreamStateEntity; | ||||||
| import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding; | import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding; | ||||||
| import org.schabi.newpipe.databinding.PlaylistControlBinding; | import org.schabi.newpipe.databinding.PlaylistControlBinding; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamType; | import org.schabi.newpipe.extractor.stream.StreamType; | ||||||
| import org.schabi.newpipe.info_list.InfoItemDialog; | import org.schabi.newpipe.info_list.InfoItemDialog; | ||||||
| @@ -42,7 +44,6 @@ import org.schabi.newpipe.local.history.HistoryRecordManager; | |||||||
| import org.schabi.newpipe.player.helper.PlayerHolder; | import org.schabi.newpipe.player.helper.PlayerHolder; | ||||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.KoreUtil; | import org.schabi.newpipe.util.KoreUtil; | ||||||
| import org.schabi.newpipe.util.Localization; | import org.schabi.newpipe.util.Localization; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| @@ -110,7 +111,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|     @Override |     @Override | ||||||
|     public void onCreate(final Bundle savedInstanceState) { |     public void onCreate(final Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); |         playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext())); | ||||||
|         debouncedSaveSignal = PublishSubject.create(); |         debouncedSaveSignal = PublishSubject.create(); | ||||||
|  |  | ||||||
|         disposables = new CompositeDisposable(); |         disposables = new CompositeDisposable(); | ||||||
| @@ -334,7 +335,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onError(final Throwable exception) { |             public void onError(final Throwable exception) { | ||||||
|                 LocalPlaylistFragment.this.onError(exception); |                 showError(new ErrorInfo(exception, UserAction.REQUESTED_BOOKMARK, | ||||||
|  |                         "Loading local playlist")); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
| @@ -344,25 +346,23 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean onOptionsItemSelected(final MenuItem item) { |     public boolean onOptionsItemSelected(final MenuItem item) { | ||||||
|         switch (item.getItemId()) { |         if (item.getItemId() == R.id.menu_item_remove_watched) { | ||||||
|             case R.id.menu_item_remove_watched: |             if (!isRemovingWatched) { | ||||||
|                 if (!isRemovingWatched) { |                 new AlertDialog.Builder(requireContext()) | ||||||
|                     new AlertDialog.Builder(requireContext()) |                         .setMessage(R.string.remove_watched_popup_warning) | ||||||
|                             .setMessage(R.string.remove_watched_popup_warning) |                         .setTitle(R.string.remove_watched_popup_title) | ||||||
|                             .setTitle(R.string.remove_watched_popup_title) |                         .setPositiveButton(R.string.yes, | ||||||
|                             .setPositiveButton(R.string.yes, |                                 (DialogInterface d, int id) -> removeWatchedStreams(false)) | ||||||
|                                     (DialogInterface d, int id) -> removeWatchedStreams(false)) |                         .setNeutralButton( | ||||||
|                             .setNeutralButton( |                                 R.string.remove_watched_popup_yes_and_partially_watched_videos, | ||||||
|                                     R.string.remove_watched_popup_yes_and_partially_watched_videos, |                                 (DialogInterface d, int id) -> removeWatchedStreams(true)) | ||||||
|                                     (DialogInterface d, int id) -> removeWatchedStreams(true)) |                         .setNegativeButton(R.string.cancel, | ||||||
|                             .setNegativeButton(R.string.cancel, |                                 (DialogInterface d, int id) -> d.cancel()) | ||||||
|                                     (DialogInterface d, int id) -> d.cancel()) |                         .create() | ||||||
|                             .create() |                         .show(); | ||||||
|                             .show(); |             } | ||||||
|                 } |         } else { | ||||||
|                 break; |             return super.onOptionsItemSelected(item); | ||||||
|             default: |  | ||||||
|                 return super.onOptionsItemSelected(item); |  | ||||||
|         } |         } | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| @@ -455,7 +455,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|  |  | ||||||
|                     hideLoading(); |                     hideLoading(); | ||||||
|                     isRemovingWatched = false; |                     isRemovingWatched = false; | ||||||
|                 }, this::onError)); |                 }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, | ||||||
|  |                         "Removing watched videos, partially watched=" + removePartiallyWatched)))); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -511,17 +512,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected boolean onError(final Throwable exception) { |  | ||||||
|         if (super.onError(exception)) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, |  | ||||||
|                 "none", "Local Playlist", R.string.general_error); |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Playlist Metadata/Streams Manipulation |     // Playlist Metadata/Streams Manipulation | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -562,7 +552,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|  |  | ||||||
|         final Disposable disposable = playlistManager.renamePlaylist(playlistId, title) |         final Disposable disposable = playlistManager.renamePlaylist(playlistId, title) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(longs -> { /*Do nothing on success*/ }, this::onError); |                 .subscribe(longs -> { /*Do nothing on success*/ }, throwable -> | ||||||
|  |                         showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, | ||||||
|  |                                 "Renaming playlist"))); | ||||||
|         disposables.add(disposable); |         disposables.add(disposable); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -583,7 +575,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|         final Disposable disposable = playlistManager |         final Disposable disposable = playlistManager | ||||||
|                 .changePlaylistThumbnail(playlistId, thumbnailUrl) |                 .changePlaylistThumbnail(playlistId, thumbnailUrl) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(ignore -> successToast.show(), this::onError); |                 .subscribe(ignore -> successToast.show(), throwable -> | ||||||
|  |                         showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, | ||||||
|  |                                 "Changing playlist thumbnail"))); | ||||||
|         disposables.add(disposable); |         disposables.add(disposable); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -632,7 +626,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|         return debouncedSaveSignal |         return debouncedSaveSignal | ||||||
|                 .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) |                 .debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(ignored -> saveImmediate(), this::onError); |                 .subscribe(ignored -> saveImmediate(), throwable -> | ||||||
|  |                         showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE, | ||||||
|  |                                 "Debounced saver"))); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void saveImmediate() { |     private void saveImmediate() { | ||||||
| @@ -669,7 +665,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|                                 isModified.set(false); |                                 isModified.set(false); | ||||||
|                             } |                             } | ||||||
|                         }, |                         }, | ||||||
|                         this::onError |                         throwable -> showError(new ErrorInfo(throwable, | ||||||
|  |                                 UserAction.REQUESTED_BOOKMARK, "Saving playlist")) | ||||||
|                 ); |                 ); | ||||||
|         disposables.add(disposable); |         disposables.add(disposable); | ||||||
|     } |     } | ||||||
| @@ -683,7 +680,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|         return new ItemTouchHelper.SimpleCallback(directions, |         return new ItemTouchHelper.SimpleCallback(directions, | ||||||
|                 ItemTouchHelper.ACTION_STATE_IDLE) { |                 ItemTouchHelper.ACTION_STATE_IDLE) { | ||||||
|             @Override |             @Override | ||||||
|             public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView, |             public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView, | ||||||
|                                                     final int viewSize, |                                                     final int viewSize, | ||||||
|                                                     final int viewSizeOutOfBounds, |                                                     final int viewSizeOutOfBounds, | ||||||
|                                                     final int totalSize, |                                                     final int totalSize, | ||||||
| @@ -696,9 +693,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public boolean onMove(final RecyclerView recyclerView, |             public boolean onMove(@NonNull final RecyclerView recyclerView, | ||||||
|                                   final RecyclerView.ViewHolder source, |                                   @NonNull final RecyclerView.ViewHolder source, | ||||||
|                                   final RecyclerView.ViewHolder target) { |                                   @NonNull final RecyclerView.ViewHolder target) { | ||||||
|                 if (source.getItemViewType() != target.getItemViewType() |                 if (source.getItemViewType() != target.getItemViewType() | ||||||
|                         || itemListAdapter == null) { |                         || itemListAdapter == null) { | ||||||
|                     return false; |                     return false; | ||||||
| @@ -724,7 +721,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) { } |             public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, | ||||||
|  |                                  final int swipeDir) { } | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -34,6 +34,8 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity | |||||||
| import org.schabi.newpipe.databinding.DialogTitleBinding | import org.schabi.newpipe.databinding.DialogTitleBinding | ||||||
| import org.schabi.newpipe.databinding.FeedItemCarouselBinding | import org.schabi.newpipe.databinding.FeedItemCarouselBinding | ||||||
| import org.schabi.newpipe.databinding.FragmentSubscriptionBinding | import org.schabi.newpipe.databinding.FragmentSubscriptionBinding | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo | ||||||
|  | import org.schabi.newpipe.error.UserAction | ||||||
| import org.schabi.newpipe.extractor.channel.ChannelInfoItem | import org.schabi.newpipe.extractor.channel.ChannelInfoItem | ||||||
| import org.schabi.newpipe.fragments.BaseStateFragment | import org.schabi.newpipe.fragments.BaseStateFragment | ||||||
| import org.schabi.newpipe.ktx.animate | import org.schabi.newpipe.ktx.animate | ||||||
| @@ -56,7 +58,6 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService | |||||||
| import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE | import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE | ||||||
| import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE | import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE | ||||||
| import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE | import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE | ||||||
| import org.schabi.newpipe.report.UserAction |  | ||||||
| import org.schabi.newpipe.util.FilePickerActivityHelper | import org.schabi.newpipe.util.FilePickerActivityHelper | ||||||
| import org.schabi.newpipe.util.NavigationHelper | import org.schabi.newpipe.util.NavigationHelper | ||||||
| import org.schabi.newpipe.util.OnClickGesture | import org.schabi.newpipe.util.OnClickGesture | ||||||
| @@ -288,8 +289,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() { | |||||||
|         binding.itemsList.adapter = groupAdapter |         binding.itemsList.adapter = groupAdapter | ||||||
|  |  | ||||||
|         viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java) |         viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java) | ||||||
|         viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(this::handleResult) }) |         viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) } | ||||||
|         viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, { it?.let(this::handleFeedGroups) }) |         viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private fun showLongTapDialog(selectedItem: ChannelInfoItem) { |     private fun showLongTapDialog(selectedItem: ChannelInfoItem) { | ||||||
| @@ -381,7 +382,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             is SubscriptionState.ErrorState -> { |             is SubscriptionState.ErrorState -> { | ||||||
|                 result.error?.let { onError(result.error) } |                 result.error?.let { | ||||||
|  |                     showError(ErrorInfo(result.error, UserAction.SOMETHING_ELSE, "Subscriptions")) | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -412,17 +415,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() { | |||||||
|         binding.itemsList.animate(true, 200) |         binding.itemsList.animate(true, 200) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // ///////////////////////////////////////////////////////////////////////// |  | ||||||
|     // Fragment Error Handling |  | ||||||
|     // ///////////////////////////////////////////////////////////////////////// |  | ||||||
|  |  | ||||||
|     override fun onError(exception: Throwable): Boolean { |  | ||||||
|         if (super.onError(exception)) return true |  | ||||||
|  |  | ||||||
|         onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error) |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // ///////////////////////////////////////////////////////////////////////// |     // ///////////////////////////////////////////////////////////////////////// | ||||||
|     // Grid Mode |     // Grid Mode | ||||||
|     // ///////////////////////////////////////////////////////////////////////// |     // ///////////////////////////////////////////////////////////////////////// | ||||||
|   | |||||||
| @@ -22,13 +22,13 @@ import com.nononsenseapps.filepicker.Utils; | |||||||
|  |  | ||||||
| import org.schabi.newpipe.BaseFragment; | import org.schabi.newpipe.BaseFragment; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
| import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; | import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; | ||||||
| import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; | import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.Constants; | import org.schabi.newpipe.util.Constants; | ||||||
| import org.schabi.newpipe.util.FilePickerActivityHelper; | import org.schabi.newpipe.util.FilePickerActivityHelper; | ||||||
| import org.schabi.newpipe.util.ServiceHelper; | import org.schabi.newpipe.util.ServiceHelper; | ||||||
| @@ -84,10 +84,12 @@ public class SubscriptionsImportFragment extends BaseFragment { | |||||||
|  |  | ||||||
|         setupServiceVariables(); |         setupServiceVariables(); | ||||||
|         if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) { |         if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) { | ||||||
|             ErrorActivity.reportError(activity, Collections.emptyList(), null, null, |             ErrorActivity.reportErrorInSnackbar(activity, | ||||||
|                     ErrorInfo.make(UserAction.SOMETHING_ELSE, |                     new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT, | ||||||
|                             NewPipe.getNameOfService(currentServiceId), |                             NewPipe.getNameOfService(currentServiceId), | ||||||
|                             "Service don't support importing", R.string.general_error)); |                             "Service does not support importing subscriptions", | ||||||
|  |                             R.string.general_error, | ||||||
|  |                             null)); | ||||||
|             activity.finish(); |             activity.finish(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -42,7 +42,6 @@ import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem | |||||||
| import org.schabi.newpipe.util.DeviceUtils | import org.schabi.newpipe.util.DeviceUtils | ||||||
| import org.schabi.newpipe.util.ThemeHelper | import org.schabi.newpipe.util.ThemeHelper | ||||||
| import java.io.Serializable | import java.io.Serializable | ||||||
| import kotlin.collections.contains |  | ||||||
|  |  | ||||||
| class FeedGroupDialog : DialogFragment(), BackPressable { | class FeedGroupDialog : DialogFragment(), BackPressable { | ||||||
|     private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null |     private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null | ||||||
|   | |||||||
| @@ -24,6 +24,10 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo | |||||||
| import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem | import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem | ||||||
| import org.schabi.newpipe.util.ThemeHelper | import org.schabi.newpipe.util.ThemeHelper | ||||||
| import java.util.Collections | import java.util.Collections | ||||||
|  | import kotlin.collections.ArrayList | ||||||
|  | import kotlin.collections.List | ||||||
|  | import kotlin.collections.map | ||||||
|  | import kotlin.collections.sortedBy | ||||||
|  |  | ||||||
| class FeedGroupReorderDialog : DialogFragment() { | class FeedGroupReorderDialog : DialogFragment() { | ||||||
|     private var _binding: DialogFeedGroupReorderBinding? = null |     private var _binding: DialogFeedGroupReorderBinding? = null | ||||||
|   | |||||||
| @@ -35,15 +35,14 @@ import androidx.core.app.ServiceCompat; | |||||||
|  |  | ||||||
| import org.reactivestreams.Publisher; | import org.reactivestreams.Publisher; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; | import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; | ||||||
| import org.schabi.newpipe.ktx.ExceptionUtils; | import org.schabi.newpipe.ktx.ExceptionUtils; | ||||||
| import org.schabi.newpipe.local.subscription.SubscriptionManager; | import org.schabi.newpipe.local.subscription.SubscriptionManager; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
|  |  | ||||||
| import java.io.FileNotFoundException; | import java.io.FileNotFoundException; | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
| import java.util.concurrent.atomic.AtomicInteger; | import java.util.concurrent.atomic.AtomicInteger; | ||||||
|  |  | ||||||
| @@ -152,13 +151,10 @@ public abstract class BaseImportExportService extends Service { | |||||||
|         postErrorResult(null, null); |         postErrorResult(null, null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected void stopAndReportError(@Nullable final Throwable error, final String request) { |     protected void stopAndReportError(final Throwable throwable, final String request) { | ||||||
|         stopService(); |         stopService(); | ||||||
|  |         ErrorActivity.reportError(this, new ErrorInfo( | ||||||
|         final ErrorInfo errorInfo = ErrorInfo |                 throwable, UserAction.SUBSCRIPTION_IMPORT_EXPORT, request)); | ||||||
|                 .make(UserAction.SUBSCRIPTION, "unknown", request, R.string.general_error); |  | ||||||
|         ErrorActivity.reportError(this, error != null ? Collections.singletonList(error) |  | ||||||
|                         : Collections.emptyList(), null, null, errorInfo); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected void postErrorResult(final String title, final String text) { |     protected void postErrorResult(final String title, final String text) { | ||||||
|   | |||||||
| @@ -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) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -21,13 +21,11 @@ import com.nostra13.universalimageloader.core.ImageLoader; | |||||||
| import org.schabi.newpipe.DownloaderImpl; | import org.schabi.newpipe.DownloaderImpl; | ||||||
| import org.schabi.newpipe.NewPipeDatabase; | import org.schabi.newpipe.NewPipeDatabase; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.ReCaptchaActivity; | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ReCaptchaActivity; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.localization.ContentCountry; | import org.schabi.newpipe.extractor.localization.ContentCountry; | ||||||
| import org.schabi.newpipe.extractor.localization.Localization; | import org.schabi.newpipe.extractor.localization.Localization; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.FilePickerActivityHelper; | import org.schabi.newpipe.util.FilePickerActivityHelper; | ||||||
| import org.schabi.newpipe.util.ZipHelper; | import org.schabi.newpipe.util.ZipHelper; | ||||||
|  |  | ||||||
| @@ -198,7 +196,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { | |||||||
|  |  | ||||||
|             Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); |             Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             onError(e); |             ErrorActivity.reportUiErrorInSnackbar(this, "Exporting database", e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -243,20 +241,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { | |||||||
|                 System.exit(0); |                 System.exit(0); | ||||||
|             } |             } | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             onError(e); |             ErrorActivity.reportUiErrorInSnackbar(this, "Importing database", e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|     // Error |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|     protected void onError(final Throwable e) { |  | ||||||
|         final Activity activity = getActivity(); |  | ||||||
|         ErrorActivity.reportError(activity, e, |  | ||||||
|                 activity.getClass(), |  | ||||||
|                 null, |  | ||||||
|                 ErrorInfo.make(UserAction.UI_ERROR, |  | ||||||
|                         "none", "", R.string.app_ui_crash)); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,17 +1,19 @@ | |||||||
| package org.schabi.newpipe.settings; | package org.schabi.newpipe.settings; | ||||||
|  |  | ||||||
|  | import android.content.Context; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.widget.Toast; | import android.widget.Toast; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.app.AlertDialog; | import androidx.appcompat.app.AlertDialog; | ||||||
| import androidx.preference.Preference; | import androidx.preference.Preference; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.local.history.HistoryRecordManager; | import org.schabi.newpipe.local.history.HistoryRecordManager; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.InfoCache; | import org.schabi.newpipe.util.InfoCache; | ||||||
|  |  | ||||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | ||||||
| @@ -46,120 +48,103 @@ public class HistorySettingsFragment extends BasePreferenceFragment { | |||||||
|     public boolean onPreferenceTreeClick(final Preference preference) { |     public boolean onPreferenceTreeClick(final Preference preference) { | ||||||
|         if (preference.getKey().equals(cacheWipeKey)) { |         if (preference.getKey().equals(cacheWipeKey)) { | ||||||
|             InfoCache.getInstance().clearCache(); |             InfoCache.getInstance().clearCache(); | ||||||
|             Toast.makeText(preference.getContext(), R.string.metadata_cache_wipe_complete_notice, |             Toast.makeText(requireContext(), | ||||||
|                     Toast.LENGTH_SHORT).show(); |                     R.string.metadata_cache_wipe_complete_notice, Toast.LENGTH_SHORT).show(); | ||||||
|  |         } else if (preference.getKey().equals(viewsHistoryClearKey)) { | ||||||
|  |             openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables); | ||||||
|  |         } else if (preference.getKey().equals(playbackStatesClearKey)) { | ||||||
|  |             openDeletePlaybackStatesDialog(requireContext(), recordManager, disposables); | ||||||
|  |         } else if (preference.getKey().equals(searchHistoryClearKey)) { | ||||||
|  |             openDeleteSearchHistoryDialog(requireContext(), recordManager, disposables); | ||||||
|  |         } else { | ||||||
|  |             return super.onPreferenceTreeClick(preference); | ||||||
|         } |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|         if (preference.getKey().equals(viewsHistoryClearKey)) { |     private static Disposable getDeletePlaybackStatesDisposable( | ||||||
|             new AlertDialog.Builder(getActivity()) |             @NonNull final Context context, final HistoryRecordManager recordManager) { | ||||||
|                     .setTitle(R.string.delete_view_history_alert) |         return recordManager.deleteCompleteStreamStateHistory() | ||||||
|                     .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                     .setPositiveButton(R.string.delete, ((dialog, which) -> { |                 .subscribe( | ||||||
|                         final Disposable onDeletePlaybackStates |                         howManyDeleted -> Toast.makeText(context, | ||||||
|                                 = recordManager.deleteCompleteStreamStateHistory() |                                 R.string.watch_history_states_deleted,  Toast.LENGTH_SHORT).show(), | ||||||
|                                 .observeOn(AndroidSchedulers.mainThread()) |                         throwable -> ErrorActivity.reportError(context, | ||||||
|                                 .subscribe( |                                 new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, | ||||||
|                                         howManyDeleted -> Toast.makeText(getActivity(), |                                         "Delete playback states"))); | ||||||
|                                                 R.string.watch_history_states_deleted, |     } | ||||||
|                                                 Toast.LENGTH_SHORT).show(), |  | ||||||
|                                         throwable -> ErrorActivity.reportError(getContext(), |  | ||||||
|                                                 throwable, |  | ||||||
|                                                 SettingsActivity.class, null, |  | ||||||
|                                                 ErrorInfo.make( |  | ||||||
|                                                         UserAction.DELETE_FROM_HISTORY, |  | ||||||
|                                                         "none", |  | ||||||
|                                                         "Delete playback states", |  | ||||||
|                                                         R.string.general_error))); |  | ||||||
|  |  | ||||||
|                         final Disposable onDelete = recordManager.deleteWholeStreamHistory() |     private static Disposable getWholeStreamHistoryDisposable( | ||||||
|                                 .observeOn(AndroidSchedulers.mainThread()) |             @NonNull final Context context, final HistoryRecordManager recordManager) { | ||||||
|                                 .subscribe( |         return recordManager.deleteWholeStreamHistory() | ||||||
|                                         howManyDeleted -> Toast.makeText(getActivity(), |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                                                 R.string.watch_history_deleted, |                 .subscribe( | ||||||
|                                                 Toast.LENGTH_SHORT).show(), |                         howManyDeleted -> Toast.makeText(context, | ||||||
|                                         throwable -> ErrorActivity.reportError(getContext(), |                                 R.string.watch_history_deleted, Toast.LENGTH_SHORT).show(), | ||||||
|                                                 throwable, |                         throwable -> ErrorActivity.reportError(context, | ||||||
|                                                 SettingsActivity.class, null, |                                 new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, | ||||||
|                                                 ErrorInfo.make( |                                         "Delete from history"))); | ||||||
|                                                         UserAction.DELETE_FROM_HISTORY, |     } | ||||||
|                                                         "none", |  | ||||||
|                                                         "Delete view history", |  | ||||||
|                                                         R.string.general_error))); |  | ||||||
|  |  | ||||||
|                         final Disposable onClearOrphans = recordManager.removeOrphanedRecords() |     private static Disposable getRemoveOrphanedRecordsDisposable( | ||||||
|                                 .observeOn(AndroidSchedulers.mainThread()) |             @NonNull final Context context, final HistoryRecordManager recordManager) { | ||||||
|                                 .subscribe( |         return recordManager.removeOrphanedRecords() | ||||||
|                                         howManyDeleted -> { |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                                         }, |                 .subscribe( | ||||||
|                                         throwable -> ErrorActivity.reportError(getContext(), |                         howManyDeleted -> { }, | ||||||
|                                                 throwable, |                         throwable -> ErrorActivity.reportError(context, | ||||||
|                                                 SettingsActivity.class, null, |                                 new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, | ||||||
|                                                 ErrorInfo.make( |                                         "Clear orphaned records"))); | ||||||
|                                                         UserAction.DELETE_FROM_HISTORY, |     } | ||||||
|                                                         "none", |  | ||||||
|                                                         "Delete search history", |  | ||||||
|                                                         R.string.general_error))); |  | ||||||
|                         disposables.add(onDeletePlaybackStates); |  | ||||||
|                         disposables.add(onClearOrphans); |  | ||||||
|                         disposables.add(onDelete); |  | ||||||
|                     })) |  | ||||||
|                     .create() |  | ||||||
|                     .show(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (preference.getKey().equals(playbackStatesClearKey)) { |     private static Disposable getDeleteSearchHistoryDisposable( | ||||||
|             new AlertDialog.Builder(getActivity()) |             @NonNull final Context context, final HistoryRecordManager recordManager) { | ||||||
|                     .setTitle(R.string.delete_playback_states_alert) |         return recordManager.deleteCompleteSearchHistory() | ||||||
|                     .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                     .setPositiveButton(R.string.delete, ((dialog, which) -> { |                 .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 |     public static void openDeleteWatchHistoryDialog(@NonNull final Context context, | ||||||
|                                 = recordManager.deleteCompleteStreamStateHistory() |                                                     final HistoryRecordManager recordManager, | ||||||
|                                 .observeOn(AndroidSchedulers.mainThread()) |                                                     final CompositeDisposable disposables) { | ||||||
|                                 .subscribe( |         new AlertDialog.Builder(context) | ||||||
|                                         howManyDeleted -> Toast.makeText(getActivity(), |                 .setTitle(R.string.delete_view_history_alert) | ||||||
|                                                 R.string.watch_history_states_deleted, |                 .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) | ||||||
|                                                 Toast.LENGTH_SHORT).show(), |                 .setPositiveButton(R.string.delete, ((dialog, which) -> { | ||||||
|                                         throwable -> ErrorActivity.reportError(getContext(), |                     disposables.add(getDeletePlaybackStatesDisposable(context, recordManager)); | ||||||
|                                                 throwable, |                     disposables.add(getWholeStreamHistoryDisposable(context, recordManager)); | ||||||
|                                                 SettingsActivity.class, null, |                     disposables.add(getRemoveOrphanedRecordsDisposable(context, recordManager)); | ||||||
|                                                 ErrorInfo.make( |                 })) | ||||||
|                                                         UserAction.DELETE_FROM_HISTORY, |                 .create() | ||||||
|                                                         "none", |                 .show(); | ||||||
|                                                         "Delete playback states", |     } | ||||||
|                                                         R.string.general_error))); |  | ||||||
|  |  | ||||||
|                         disposables.add(onDeletePlaybackStates); |     public static void openDeletePlaybackStatesDialog(@NonNull final Context context, | ||||||
|                     })) |                                                       final HistoryRecordManager recordManager, | ||||||
|                     .create() |                                                       final CompositeDisposable disposables) { | ||||||
|                     .show(); |         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)) { |     public static void openDeleteSearchHistoryDialog(@NonNull final Context context, | ||||||
|             new AlertDialog.Builder(getActivity()) |                                                      final HistoryRecordManager recordManager, | ||||||
|                     .setTitle(R.string.delete_search_history_alert) |                                                      final CompositeDisposable disposables) { | ||||||
|                     .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) |         new AlertDialog.Builder(context) | ||||||
|                     .setPositiveButton(R.string.delete, ((dialog, which) -> { |                 .setTitle(R.string.delete_search_history_alert) | ||||||
|                         final Disposable onDelete = recordManager.deleteCompleteSearchHistory() |                 .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) | ||||||
|                                 .observeOn(AndroidSchedulers.mainThread()) |                 .setPositiveButton(R.string.delete, ((dialog, which) -> | ||||||
|                                 .subscribe( |                         disposables.add(getDeleteSearchHistoryDisposable(context, recordManager)))) | ||||||
|                                         howManyDeleted -> Toast.makeText(getActivity(), |                 .create() | ||||||
|                                                 R.string.search_history_deleted, |                 .show(); | ||||||
|                                                 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); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| package org.schabi.newpipe.settings; | package org.schabi.newpipe.settings; | ||||||
|  |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.content.DialogInterface; | import android.content.DialogInterface; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| @@ -20,10 +19,8 @@ import com.nostra13.universalimageloader.core.ImageLoader; | |||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.database.subscription.SubscriptionEntity; | import org.schabi.newpipe.database.subscription.SubscriptionEntity; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
| import org.schabi.newpipe.local.subscription.SubscriptionManager; | import org.schabi.newpipe.local.subscription.SubscriptionManager; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | import org.schabi.newpipe.util.ThemeHelper; | ||||||
|  |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
| @@ -108,7 +105,7 @@ public class SelectChannelFragment extends DialogFragment { | |||||||
|         emptyView.setVisibility(View.GONE); |         emptyView.setVisibility(View.GONE); | ||||||
|  |  | ||||||
|  |  | ||||||
|         final SubscriptionManager subscriptionManager = new SubscriptionManager(getContext()); |         final SubscriptionManager subscriptionManager = new SubscriptionManager(requireContext()); | ||||||
|         subscriptionManager.subscriptions().toObservable() |         subscriptionManager.subscriptions().toObservable() | ||||||
|                 .subscribeOn(Schedulers.io()) |                 .subscribeOn(Schedulers.io()) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
| @@ -122,7 +119,7 @@ public class SelectChannelFragment extends DialogFragment { | |||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onCancel(final DialogInterface dialogInterface) { |     public void onCancel(@NonNull final DialogInterface dialogInterface) { | ||||||
|         super.onCancel(dialogInterface); |         super.onCancel(dialogInterface); | ||||||
|         if (onCancelListener != null) { |         if (onCancelListener != null) { | ||||||
|             onCancelListener.onCancel(); |             onCancelListener.onCancel(); | ||||||
| @@ -156,16 +153,17 @@ public class SelectChannelFragment extends DialogFragment { | |||||||
|     private Observer<List<SubscriptionEntity>> getSubscriptionObserver() { |     private Observer<List<SubscriptionEntity>> getSubscriptionObserver() { | ||||||
|         return new Observer<List<SubscriptionEntity>>() { |         return new Observer<List<SubscriptionEntity>>() { | ||||||
|             @Override |             @Override | ||||||
|             public void onSubscribe(final Disposable d) { } |             public void onSubscribe(@NonNull final Disposable disposable) { } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onNext(final List<SubscriptionEntity> newSubscriptions) { |             public void onNext(@NonNull final List<SubscriptionEntity> newSubscriptions) { | ||||||
|                 displayChannels(newSubscriptions); |                 displayChannels(newSubscriptions); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onError(final Throwable exception) { |             public void onError(@NonNull final Throwable exception) { | ||||||
|                 SelectChannelFragment.this.onError(exception); |                 ErrorActivity.reportUiErrorInSnackbar(SelectChannelFragment.this, | ||||||
|  |                         "Loading subscription", exception); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
| @@ -173,16 +171,6 @@ public class SelectChannelFragment extends DialogFragment { | |||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|     // Error |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|     protected void onError(final Throwable e) { |  | ||||||
|         final Activity activity = getActivity(); |  | ||||||
|         ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorInfo |  | ||||||
|                 .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Interfaces |     // Interfaces | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -197,6 +185,7 @@ public class SelectChannelFragment extends DialogFragment { | |||||||
|  |  | ||||||
|     private class SelectChannelAdapter |     private class SelectChannelAdapter | ||||||
|             extends RecyclerView.Adapter<SelectChannelAdapter.SelectChannelItemHolder> { |             extends RecyclerView.Adapter<SelectChannelAdapter.SelectChannelItemHolder> { | ||||||
|  |         @NonNull | ||||||
|         @Override |         @Override | ||||||
|         public SelectChannelItemHolder onCreateViewHolder(final ViewGroup parent, |         public SelectChannelItemHolder onCreateViewHolder(final ViewGroup parent, | ||||||
|                                                           final int viewType) { |                                                           final int viewType) { | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| package org.schabi.newpipe.settings; | package org.schabi.newpipe.settings; | ||||||
|  |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.content.DialogInterface; | import android.content.DialogInterface; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| @@ -16,11 +15,9 @@ import androidx.recyclerview.widget.LinearLayoutManager; | |||||||
| import androidx.recyclerview.widget.RecyclerView; | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.KioskTranslator; | import org.schabi.newpipe.util.KioskTranslator; | ||||||
| import org.schabi.newpipe.util.ServiceHelper; | import org.schabi.newpipe.util.ServiceHelper; | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | import org.schabi.newpipe.util.ThemeHelper; | ||||||
| @@ -83,7 +80,7 @@ public class SelectKioskFragment extends DialogFragment { | |||||||
|         try { |         try { | ||||||
|             selectKioskAdapter = new SelectKioskAdapter(); |             selectKioskAdapter = new SelectKioskAdapter(); | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             onError(e); |             ErrorActivity.reportUiErrorInSnackbar(this, "Selecting kiosk", e); | ||||||
|         } |         } | ||||||
|         recyclerView.setAdapter(selectKioskAdapter); |         recyclerView.setAdapter(selectKioskAdapter); | ||||||
|  |  | ||||||
| @@ -109,16 +106,6 @@ public class SelectKioskFragment extends DialogFragment { | |||||||
|         dismiss(); |         dismiss(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|     // Error |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|     protected void onError(final Throwable e) { |  | ||||||
|         final Activity activity = getActivity(); |  | ||||||
|         ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorInfo |  | ||||||
|                 .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Interfaces |     // Interfaces | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|   | |||||||
| @@ -24,11 +24,11 @@ import org.schabi.newpipe.database.LocalItem; | |||||||
| import org.schabi.newpipe.database.playlist.PlaylistLocalItem; | import org.schabi.newpipe.database.playlist.PlaylistLocalItem; | ||||||
| import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; | import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; | ||||||
| import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.local.playlist.LocalPlaylistManager; | import org.schabi.newpipe.local.playlist.LocalPlaylistManager; | ||||||
| import org.schabi.newpipe.local.playlist.RemotePlaylistManager; | import org.schabi.newpipe.local.playlist.RemotePlaylistManager; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
|  |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Vector; | import java.util.Vector; | ||||||
| @@ -115,8 +115,8 @@ public class SelectPlaylistFragment extends DialogFragment { | |||||||
|  |  | ||||||
|     protected void onError(final Throwable e) { |     protected void onError(final Throwable e) { | ||||||
|         final Activity activity = requireActivity(); |         final Activity activity = requireActivity(); | ||||||
|         ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorInfo |         ErrorActivity.reportErrorInSnackbar(activity, new ErrorInfo(e, | ||||||
|                 .make(UserAction.UI_ERROR, "none", "load_playlists", R.string.app_ui_crash)); |                 UserAction.UI_ERROR, "Loading playlists")); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|   | |||||||
| @@ -7,9 +7,9 @@ import android.util.Log; | |||||||
| import androidx.preference.PreferenceManager; | import androidx.preference.PreferenceManager; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; | import org.schabi.newpipe.error.ErrorActivity; | ||||||
| import org.schabi.newpipe.report.ErrorInfo; | import org.schabi.newpipe.error.ErrorInfo; | ||||||
| import org.schabi.newpipe.report.UserAction; | import org.schabi.newpipe.error.UserAction; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.MainActivity.DEBUG; | import static org.schabi.newpipe.MainActivity.DEBUG; | ||||||
|  |  | ||||||
| @@ -95,15 +95,13 @@ public final class SettingMigrations { | |||||||
|             } catch (final Exception e) { |             } catch (final Exception e) { | ||||||
|                 // save the version with the last successful migration and report the error |                 // save the version with the last successful migration and report the error | ||||||
|                 sp.edit().putInt(lastPrefVersionKey, currentVersion).apply(); |                 sp.edit().putInt(lastPrefVersionKey, currentVersion).apply(); | ||||||
|                 final ErrorInfo errorInfo = ErrorInfo.make( |                 ErrorActivity.reportError(context, new ErrorInfo( | ||||||
|  |                         e, | ||||||
|                         UserAction.PREFERENCES_MIGRATION, |                         UserAction.PREFERENCES_MIGRATION, | ||||||
|                         "none", |  | ||||||
|                         "Migrating preferences from version " + lastPrefVersion + " to " |                         "Migrating preferences from version " + lastPrefVersion + " to " | ||||||
|                                 + VERSION + ". " |                                 + VERSION + ". " | ||||||
|                                 + "Error at " + currentVersion  + " => " + ++currentVersion, |                                 + "Error at " + currentVersion  + " => " + ++currentVersion | ||||||
|                         0 |                 )); | ||||||
|                 ); |  | ||||||
|                 ErrorActivity.reportError(context, e, SettingMigrations.class, null, errorInfo); |  | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -27,10 +27,10 @@ import androidx.recyclerview.widget.RecyclerView; | |||||||
| import com.google.android.material.floatingactionbutton.FloatingActionButton; | import com.google.android.material.floatingactionbutton.FloatingActionButton; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.settings.SelectChannelFragment; | import org.schabi.newpipe.settings.SelectChannelFragment; | ||||||
| import org.schabi.newpipe.settings.SelectKioskFragment; | import org.schabi.newpipe.settings.SelectKioskFragment; | ||||||
| import org.schabi.newpipe.settings.SelectPlaylistFragment; | import org.schabi.newpipe.settings.SelectPlaylistFragment; | ||||||
| @@ -183,10 +183,9 @@ public class ChooseTabsFragment extends Fragment { | |||||||
|         final Tab.Type type = typeFrom(tabId); |         final Tab.Type type = typeFrom(tabId); | ||||||
|  |  | ||||||
|         if (type == null) { |         if (type == null) { | ||||||
|             ErrorActivity.reportError(requireContext(), |             ErrorActivity.reportErrorInSnackbar(this, | ||||||
|                     new IllegalStateException("Tab id not found: " + tabId), null, null, |                     new ErrorInfo(new IllegalStateException("Tab id not found: " + tabId), | ||||||
|                     ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", |                             UserAction.SOMETHING_ELSE, "Choosing tabs on settings")); | ||||||
|                             "Choosing tabs on settings", 0)); |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,6 +12,9 @@ import com.grack.nanojson.JsonSink; | |||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.database.LocalItem.LocalItemType; | import org.schabi.newpipe.database.LocalItem.LocalItemType; | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
| @@ -25,9 +28,6 @@ import org.schabi.newpipe.local.feed.FeedFragment; | |||||||
| import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; | import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; | ||||||
| import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; | import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; | ||||||
| import org.schabi.newpipe.local.subscription.SubscriptionFragment; | import org.schabi.newpipe.local.subscription.SubscriptionFragment; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.util.KioskTranslator; | import org.schabi.newpipe.util.KioskTranslator; | ||||||
| import org.schabi.newpipe.util.ServiceHelper; | import org.schabi.newpipe.util.ServiceHelper; | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | import org.schabi.newpipe.util.ThemeHelper; | ||||||
| @@ -483,9 +483,8 @@ public abstract class Tab { | |||||||
|                 final StreamingService service = NewPipe.getService(kioskServiceId); |                 final StreamingService service = NewPipe.getService(kioskServiceId); | ||||||
|                 kioskId = service.getKioskList().getDefaultKioskId(); |                 kioskId = service.getKioskList().getDefaultKioskId(); | ||||||
|             } catch (final ExtractionException e) { |             } catch (final ExtractionException e) { | ||||||
|                 ErrorActivity.reportError(context, e, null, null, |                 ErrorActivity.reportErrorInSnackbar(context, new ErrorInfo(e, | ||||||
|                         ErrorInfo.make(UserAction.REQUESTED_KIOSK, "none", |                         UserAction.REQUESTED_KIOSK, "Loading default kiosk for selected service")); | ||||||
|                                 "Loading default kiosk from selected service", 0)); |  | ||||||
|             } |             } | ||||||
|             return kioskId; |             return kioskId; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -20,12 +20,9 @@ | |||||||
| package org.schabi.newpipe.util; | package org.schabi.newpipe.util; | ||||||
|  |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Handler; |  | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.core.text.HtmlCompat; | import androidx.core.text.HtmlCompat; | ||||||
| @@ -33,7 +30,6 @@ import androidx.preference.PreferenceManager; | |||||||
|  |  | ||||||
| import org.schabi.newpipe.MainActivity; | import org.schabi.newpipe.MainActivity; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.ReCaptchaActivity; |  | ||||||
| import org.schabi.newpipe.extractor.Info; | import org.schabi.newpipe.extractor.Info; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; | import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; | ||||||
| @@ -44,29 +40,14 @@ import org.schabi.newpipe.extractor.Page; | |||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.channel.ChannelInfo; | import org.schabi.newpipe.extractor.channel.ChannelInfo; | ||||||
| import org.schabi.newpipe.extractor.comments.CommentsInfo; | import org.schabi.newpipe.extractor.comments.CommentsInfo; | ||||||
| import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.PaidContentException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.ParsingException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.PrivateContentException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; |  | ||||||
| import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; |  | ||||||
| import org.schabi.newpipe.extractor.feed.FeedExtractor; | import org.schabi.newpipe.extractor.feed.FeedExtractor; | ||||||
| import org.schabi.newpipe.extractor.feed.FeedInfo; | import org.schabi.newpipe.extractor.feed.FeedInfo; | ||||||
| import org.schabi.newpipe.extractor.kiosk.KioskInfo; | import org.schabi.newpipe.extractor.kiosk.KioskInfo; | ||||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | import org.schabi.newpipe.extractor.playlist.PlaylistInfo; | ||||||
| import org.schabi.newpipe.extractor.search.SearchInfo; | import org.schabi.newpipe.extractor.search.SearchInfo; | ||||||
| import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; |  | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||||
| import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; | import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; | ||||||
| import org.schabi.newpipe.ktx.ExceptionUtils; |  | ||||||
| import org.schabi.newpipe.report.ErrorActivity; |  | ||||||
| import org.schabi.newpipe.report.ErrorInfo; |  | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
|  |  | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| @@ -280,65 +261,6 @@ public final class ExtractorHelper { | |||||||
|         return null != loadFromCache(serviceId, url, infoType).blockingGet(); |         return null != loadFromCache(serviceId, url, infoType).blockingGet(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * A simple and general error handler that show a Toast for known exceptions, |  | ||||||
|      * and for others, opens the report error activity with the (optional) error message. |  | ||||||
|      * |  | ||||||
|      * @param context              Android app context |  | ||||||
|      * @param serviceId            the service the exception happened in |  | ||||||
|      * @param url                  the URL where the exception happened |  | ||||||
|      * @param exception            the exception to be handled |  | ||||||
|      * @param userAction           the action of the user that caused the exception |  | ||||||
|      * @param optionalErrorMessage the optional error message |  | ||||||
|      */ |  | ||||||
|     public static void handleGeneralException(final Context context, final int serviceId, |  | ||||||
|                                               final String url, final Throwable exception, |  | ||||||
|                                               final UserAction userAction, |  | ||||||
|                                               final String optionalErrorMessage) { |  | ||||||
|         final Handler handler = new Handler(context.getMainLooper()); |  | ||||||
|  |  | ||||||
|         handler.post(() -> { |  | ||||||
|             if (exception instanceof ReCaptchaException) { |  | ||||||
|                 Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); |  | ||||||
|                 // Starting ReCaptcha Challenge Activity |  | ||||||
|                 final Intent intent = new Intent(context, ReCaptchaActivity.class); |  | ||||||
|                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |  | ||||||
|                 context.startActivity(intent); |  | ||||||
|             } else if (ExceptionUtils.isNetworkRelated(exception)) { |  | ||||||
|                 Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); |  | ||||||
|             } else if (exception instanceof AgeRestrictedContentException) { |  | ||||||
|                 Toast.makeText(context, R.string.restricted_video_no_stream, |  | ||||||
|                         Toast.LENGTH_LONG).show(); |  | ||||||
|             } else if (exception instanceof GeographicRestrictionException) { |  | ||||||
|                 Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show(); |  | ||||||
|             } else if (exception instanceof PaidContentException) { |  | ||||||
|                 Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show(); |  | ||||||
|             } else if (exception instanceof PrivateContentException) { |  | ||||||
|                 Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show(); |  | ||||||
|             } else if (exception instanceof SoundCloudGoPlusContentException) { |  | ||||||
|                 Toast.makeText(context, R.string.soundcloud_go_plus_content, |  | ||||||
|                         Toast.LENGTH_LONG).show(); |  | ||||||
|             } else if (exception instanceof YoutubeMusicPremiumContentException) { |  | ||||||
|                 Toast.makeText(context, R.string.youtube_music_premium_content, |  | ||||||
|                         Toast.LENGTH_LONG).show(); |  | ||||||
|             } else if (exception instanceof ContentNotAvailableException) { |  | ||||||
|                 Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); |  | ||||||
|             } else if (exception instanceof ContentNotSupportedException) { |  | ||||||
|                 Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show(); |  | ||||||
|             } else { |  | ||||||
|                 final int errorId = exception instanceof YoutubeStreamExtractor.DeobfuscateException |  | ||||||
|                         ? R.string.youtube_signature_deobfuscation_error |  | ||||||
|                         : exception instanceof ParsingException |  | ||||||
|                         ? R.string.parsing_error : R.string.general_error; |  | ||||||
|                 ErrorActivity.reportError(handler, context, exception, MainActivity.class, null, |  | ||||||
|                         ErrorInfo.make(userAction, serviceId == -1 ? "none" |  | ||||||
|                                 : NewPipe.getNameOfService(serviceId), |  | ||||||
|                                 url + (optionalErrorMessage == null ? "" |  | ||||||
|                                         : optionalErrorMessage), errorId)); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Formats the text contained in the meta info list as HTML and puts it into the text view, |      * Formats the text contained in the meta info list as HTML and puts it into the text view, | ||||||
|      * while also making the separator visible. If the list is null or empty, or the user chose not |      * while also making the separator visible. If the list is null or empty, or the user chose not | ||||||
| @@ -352,10 +274,9 @@ public final class ExtractorHelper { | |||||||
|                                                     final TextView metaInfoTextView, |                                                     final TextView metaInfoTextView, | ||||||
|                                                     final View metaInfoSeparator) { |                                                     final View metaInfoSeparator) { | ||||||
|         final Context context = metaInfoTextView.getContext(); |         final Context context = metaInfoTextView.getContext(); | ||||||
|         final boolean showMetaInfo = PreferenceManager.getDefaultSharedPreferences(context) |         if (metaInfos == null || metaInfos.isEmpty() | ||||||
|                 .getBoolean(context.getString(R.string.show_meta_info_key), true); |                 || !PreferenceManager.getDefaultSharedPreferences(context).getBoolean( | ||||||
|  |                         context.getString(R.string.show_meta_info_key), true)) { | ||||||
|         if (!showMetaInfo || metaInfos == null || metaInfos.isEmpty()) { |  | ||||||
|             metaInfoTextView.setVisibility(View.GONE); |             metaInfoTextView.setVisibility(View.GONE); | ||||||
|             metaInfoSeparator.setVisibility(View.GONE); |             metaInfoSeparator.setVisibility(View.GONE); | ||||||
|             return Disposable.empty(); |             return Disposable.empty(); | ||||||
|   | |||||||
| @@ -41,9 +41,9 @@ import com.google.android.material.snackbar.Snackbar; | |||||||
| import org.schabi.newpipe.BuildConfig; | import org.schabi.newpipe.BuildConfig; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.extractor.NewPipe; | import org.schabi.newpipe.extractor.NewPipe; | ||||||
| import org.schabi.newpipe.report.ErrorActivity; | import org.schabi.newpipe.error.ErrorActivity; | ||||||
| import org.schabi.newpipe.report.ErrorInfo; | import org.schabi.newpipe.error.ErrorInfo; | ||||||
| import org.schabi.newpipe.report.UserAction; | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.ShareUtils; | import org.schabi.newpipe.util.ShareUtils; | ||||||
|  |  | ||||||
| @@ -583,16 +583,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb | |||||||
|         try { |         try { | ||||||
|             service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName(); |             service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName(); | ||||||
|         } catch (Exception e) { |         } catch (Exception e) { | ||||||
|             service = "-"; |             service = ErrorInfo.SERVICE_NONE; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         ErrorActivity.reportError( |         ErrorActivity.reportError(mContext, | ||||||
|                 mContext, |                 new ErrorInfo(ErrorInfo.Companion.throwableToStringList(mission.errObject), action, | ||||||
|                 mission.errObject, |                         service, request.toString(), reason, null)); | ||||||
|                 null, |  | ||||||
|                 null, |  | ||||||
|                 ErrorInfo.make(action, service, request.toString(), reason) |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void clearFinishedDownloads(boolean delete) { |     public void clearFinishedDownloads(boolean delete) { | ||||||
|   | |||||||
| @@ -219,7 +219,7 @@ | |||||||
|                     <!--ERROR PANEL--> |                     <!--ERROR PANEL--> | ||||||
|                     <include |                     <include | ||||||
|                         android:id="@+id/error_panel" |                         android:id="@+id/error_panel" | ||||||
|                         layout="@layout/error_retry" |                         layout="@layout/error_panel" | ||||||
|                         android:layout_width="match_parent" |                         android:layout_width="match_parent" | ||||||
|                         android:layout_height="wrap_content" |                         android:layout_height="wrap_content" | ||||||
|                         android:layout_below="@id/detail_title_root_layout" |                         android:layout_below="@id/detail_title_root_layout" | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
|     xmlns:tools="http://schemas.android.com/tools" |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="match_parent" |     android:layout_height="match_parent" | ||||||
|     tools:context=".report.ErrorActivity"> |     tools:context=".error.ErrorActivity"> | ||||||
|  |  | ||||||
|     <include |     <include | ||||||
|         layout="@layout/toolbar_layout" |         layout="@layout/toolbar_layout" | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:tools="http://schemas.android.com/tools" |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|     android:layout_width="match_parent" |     android:layout_width="wrap_content" | ||||||
|     android:layout_height="wrap_content" |     android:layout_height="wrap_content" | ||||||
|     android:gravity="center_horizontal" |     android:gravity="center_horizontal" | ||||||
|     android:orientation="vertical" |     android:orientation="vertical" | ||||||
| @@ -17,11 +17,24 @@ | |||||||
|         android:textStyle="bold" |         android:textStyle="bold" | ||||||
|         tools:text="Network error" /> |         tools:text="Network error" /> | ||||||
| 
 | 
 | ||||||
|  |     <Button | ||||||
|  |         android:id="@+id/error_button_action" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginTop="8dp" | ||||||
|  |         android:text="@string/error_snackbar_action" | ||||||
|  |         android:textAlignment="center" | ||||||
|  |         android:textAllCaps="true" | ||||||
|  |         android:textAppearance="@style/TextAppearance.AppCompat.Body1" | ||||||
|  |         android:textSize="16sp" | ||||||
|  |         android:theme="@style/ServiceColoredButton" /> | ||||||
|  | 
 | ||||||
|     <Button |     <Button | ||||||
|         android:id="@+id/error_button_retry" |         android:id="@+id/error_button_retry" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_margin="8dp" |         android:layout_marginTop="4dp" | ||||||
|  |         android:layout_marginBottom="8dp" | ||||||
|         android:text="@string/retry" |         android:text="@string/retry" | ||||||
|         android:textAlignment="center" |         android:textAlignment="center" | ||||||
|         android:textAllCaps="true" |         android:textAllCaps="true" | ||||||
| @@ -8,7 +8,7 @@ | |||||||
|  |  | ||||||
|     <include |     <include | ||||||
|         android:id="@+id/error_panel" |         android:id="@+id/error_panel" | ||||||
|         layout="@layout/error_retry" |         layout="@layout/error_panel" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_centerHorizontal="true" |         android:layout_centerHorizontal="true" | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ | |||||||
|     <!--ERROR PANEL--> |     <!--ERROR PANEL--> | ||||||
|     <include |     <include | ||||||
|         android:id="@+id/error_panel" |         android:id="@+id/error_panel" | ||||||
|         layout="@layout/error_retry" |         layout="@layout/error_panel" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_centerHorizontal="true" |         android:layout_centerHorizontal="true" | ||||||
|   | |||||||
| @@ -63,7 +63,7 @@ | |||||||
|     <!--ERROR PANEL--> |     <!--ERROR PANEL--> | ||||||
|     <include |     <include | ||||||
|         android:id="@+id/error_panel" |         android:id="@+id/error_panel" | ||||||
|         layout="@layout/error_retry" |         layout="@layout/error_panel" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_centerInParent="true" |         android:layout_centerInParent="true" | ||||||
|   | |||||||
| @@ -52,11 +52,11 @@ | |||||||
|     <!--ERROR PANEL--> |     <!--ERROR PANEL--> | ||||||
|     <include |     <include | ||||||
|         android:id="@+id/error_panel" |         android:id="@+id/error_panel" | ||||||
|         layout="@layout/error_retry" |         layout="@layout/error_panel" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_centerInParent="true" |         android:layout_centerHorizontal="true" | ||||||
|         android:layout_marginTop="50dp" |         android:layout_marginTop="16dp" | ||||||
|         android:visibility="gone" |         android:visibility="gone" | ||||||
|         tools:visibility="visible" /> |         tools:visibility="visible" /> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:tools="http://schemas.android.com/tools" |  | ||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="match_parent"> |     android:layout_height="match_parent"> | ||||||
|  |  | ||||||
| @@ -14,16 +13,6 @@ | |||||||
|             android:layout_width="wrap_content" |             android:layout_width="wrap_content" | ||||||
|             android:layout_height="wrap_content" |             android:layout_height="wrap_content" | ||||||
|             android:layout_gravity="center_horizontal" |             android:layout_gravity="center_horizontal" | ||||||
|             android:layout_marginTop="90dp" |             android:layout_marginTop="90dp" /> | ||||||
|             tools:visibility="visible" /> |  | ||||||
|  |  | ||||||
|     </androidx.core.widget.NestedScrollView> |     </androidx.core.widget.NestedScrollView> | ||||||
|  |  | ||||||
|     <View |  | ||||||
|         android:layout_width="match_parent" |  | ||||||
|         android:layout_height="4dp" |  | ||||||
|         android:layout_alignParentTop="true" |  | ||||||
|         android:background="?attr/toolbar_shadow" |  | ||||||
|         android:visibility="gone" /> |  | ||||||
|  |  | ||||||
| </RelativeLayout> | </RelativeLayout> | ||||||
|   | |||||||
| @@ -71,7 +71,7 @@ | |||||||
|     </RelativeLayout> |     </RelativeLayout> | ||||||
|  |  | ||||||
|     <androidx.swiperefreshlayout.widget.SwipeRefreshLayout |     <androidx.swiperefreshlayout.widget.SwipeRefreshLayout | ||||||
|         android:id="@+id/swiperefresh" |         android:id="@+id/swipeRefreshLayout" | ||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="match_parent" |         android:layout_height="match_parent" | ||||||
|         android:layout_below="@+id/refresh_root_view"> |         android:layout_below="@+id/refresh_root_view"> | ||||||
| @@ -122,7 +122,7 @@ | |||||||
|     <!--ERROR PANEL--> |     <!--ERROR PANEL--> | ||||||
|     <include |     <include | ||||||
|         android:id="@+id/error_panel" |         android:id="@+id/error_panel" | ||||||
|         layout="@layout/error_retry" |         layout="@layout/error_panel" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_centerInParent="true" |         android:layout_centerInParent="true" | ||||||
|   | |||||||
| @@ -53,7 +53,7 @@ | |||||||
|     <!--ERROR PANEL--> |     <!--ERROR PANEL--> | ||||||
|     <include |     <include | ||||||
|         android:id="@+id/error_panel" |         android:id="@+id/error_panel" | ||||||
|         layout="@layout/error_retry" |         layout="@layout/error_panel" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_centerInParent="true" |         android:layout_centerInParent="true" | ||||||
|   | |||||||
| @@ -52,7 +52,7 @@ | |||||||
|     <!--ERROR PANEL--> |     <!--ERROR PANEL--> | ||||||
|     <include |     <include | ||||||
|         android:id="@+id/error_panel" |         android:id="@+id/error_panel" | ||||||
|         layout="@layout/error_retry" |         layout="@layout/error_panel" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_centerInParent="true" |         android:layout_centerInParent="true" | ||||||
|   | |||||||
| @@ -52,7 +52,7 @@ | |||||||
|     <!--ERROR PANEL--> |     <!--ERROR PANEL--> | ||||||
|     <include |     <include | ||||||
|         android:id="@+id/error_panel" |         android:id="@+id/error_panel" | ||||||
|         layout="@layout/error_retry" |         layout="@layout/error_panel" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_centerInParent="true" |         android:layout_centerInParent="true" | ||||||
|   | |||||||
| @@ -104,11 +104,10 @@ | |||||||
|     <!--ERROR PANEL--> |     <!--ERROR PANEL--> | ||||||
|     <include |     <include | ||||||
|         android:id="@+id/error_panel" |         android:id="@+id/error_panel" | ||||||
|         layout="@layout/error_retry" |         layout="@layout/error_panel" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_centerHorizontal="true" |         android:layout_centerInParent="true" | ||||||
|         android:layout_marginTop="50dp" |  | ||||||
|         android:visibility="gone" |         android:visibility="gone" | ||||||
|         tools:visibility="visible" /> |         tools:visibility="visible" /> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ | |||||||
|     <!--ERROR PANEL--> |     <!--ERROR PANEL--> | ||||||
|     <include |     <include | ||||||
|         android:id="@+id/error_panel" |         android:id="@+id/error_panel" | ||||||
|         layout="@layout/error_retry" |         layout="@layout/error_panel" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_below="@id/items_list" |         android:layout_below="@id/items_list" | ||||||
|   | |||||||
| @@ -206,7 +206,7 @@ | |||||||
|                 <!--ERROR PANEL--> |                 <!--ERROR PANEL--> | ||||||
|                 <include |                 <include | ||||||
|                     android:id="@+id/error_panel" |                     android:id="@+id/error_panel" | ||||||
|                     layout="@layout/error_retry" |                     layout="@layout/error_panel" | ||||||
|                     android:layout_width="match_parent" |                     android:layout_width="match_parent" | ||||||
|                     android:layout_height="wrap_content" |                     android:layout_height="wrap_content" | ||||||
|                     android:layout_below="@id/detail_title_root_layout" |                     android:layout_below="@id/detail_title_root_layout" | ||||||
|   | |||||||
| @@ -372,6 +372,7 @@ | |||||||
|     <string name="title_activity_recaptcha">reCAPTCHA challenge</string> |     <string name="title_activity_recaptcha">reCAPTCHA challenge</string> | ||||||
|     <string name="subtitle_activity_recaptcha">Press \"Done\" when solved</string> |     <string name="subtitle_activity_recaptcha">Press \"Done\" when solved</string> | ||||||
|     <string name="recaptcha_request_toast">reCAPTCHA challenge requested</string> |     <string name="recaptcha_request_toast">reCAPTCHA challenge requested</string> | ||||||
|  |     <string name="recaptcha_solve">Solve</string> | ||||||
|     <string name="recaptcha_done_button">Done</string> |     <string name="recaptcha_done_button">Done</string> | ||||||
|     <!-- Downloads --> |     <!-- Downloads --> | ||||||
|     <string name="settings_category_downloads_title">Download</string> |     <string name="settings_category_downloads_title">Download</string> | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| package org.schabi.newpipe.report; | package org.schabi.newpipe.error; | ||||||
| 
 | 
 | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
| 
 | 
 | ||||||
| @@ -1,8 +1,7 @@ | |||||||
| package org.schabi.newpipe | package org.schabi.newpipe.error | ||||||
| 
 | 
 | ||||||
| import org.junit.Assert.assertEquals | import org.junit.Assert.assertEquals | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
| import org.schabi.newpipe.ReCaptchaActivity.YT_URL |  | ||||||
| 
 | 
 | ||||||
| class ReCaptchaActivityTest { | class ReCaptchaActivityTest { | ||||||
|     private fun assertSanitized(expected: String, actual: String?) { |     private fun assertSanitized(expected: String, actual: String?) { | ||||||
| @@ -10,9 +9,9 @@ class ReCaptchaActivityTest { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test fun `null, empty or blank url is sanitized correctly`() { |     @Test fun `null, empty or blank url is sanitized correctly`() { | ||||||
|         assertSanitized(YT_URL, null) |         assertSanitized(ReCaptchaActivity.YT_URL, null) | ||||||
|         assertSanitized(YT_URL, "") |         assertSanitized(ReCaptchaActivity.YT_URL, "") | ||||||
|         assertSanitized(YT_URL, "  \n \t ") |         assertSanitized(ReCaptchaActivity.YT_URL, "  \n \t ") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test fun `YouTube url containing pbj=1 is sanitized correctly`() { |     @Test fun `YouTube url containing pbj=1 is sanitized correctly`() { | ||||||
		Reference in New Issue
	
	Block a user
	 Tobi
					Tobi