mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	Merge branch 'refactor' into About-Compose
This commit is contained in:
		| @@ -125,6 +125,8 @@ ext { | |||||||
|  |  | ||||||
|     leakCanaryVersion = '2.12' |     leakCanaryVersion = '2.12' | ||||||
|     stethoVersion = '1.6.0' |     stethoVersion = '1.6.0' | ||||||
|  |  | ||||||
|  |     coilVersion = '3.0.3' | ||||||
| } | } | ||||||
|  |  | ||||||
| configurations { | configurations { | ||||||
| @@ -208,7 +210,7 @@ dependencies { | |||||||
|     // This works thanks to JitPack: https://jitpack.io/ |     // This works thanks to JitPack: https://jitpack.io/ | ||||||
|     implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' |     implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' | ||||||
|     // WORKAROUND: v0.24.2 can't be resolved by jitpack -> use git commit hash instead |     // WORKAROUND: v0.24.2 can't be resolved by jitpack -> use git commit hash instead | ||||||
|     implementation 'com.github.TeamNewPipe:NewPipeExtractor:176da72cb4c3ec4679211339b0e59f6b01bf2f52' |     implementation 'com.github.TeamNewPipe:NewPipeExtractor:d3d5f2b3f03a5f2b479b9f6fdf1c2555cbb9de0e' | ||||||
|     implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' |     implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' | ||||||
|  |  | ||||||
| /** Checkstyle **/ | /** Checkstyle **/ | ||||||
| @@ -270,7 +272,8 @@ dependencies { | |||||||
|     implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}" |     implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}" | ||||||
|  |  | ||||||
|     // Image loading |     // Image loading | ||||||
|     implementation 'io.coil-kt:coil-compose:2.7.0' |     implementation "io.coil-kt.coil3:coil-compose:${coilVersion}" | ||||||
|  |     implementation "io.coil-kt.coil3:coil-network-okhttp:${coilVersion}" | ||||||
|  |  | ||||||
|     // Markdown library for Android |     // Markdown library for Android | ||||||
|     implementation "io.noties.markwon:core:${markwonVersion}" |     implementation "io.noties.markwon:core:${markwonVersion}" | ||||||
|   | |||||||
| @@ -1,279 +0,0 @@ | |||||||
| package org.schabi.newpipe; |  | ||||||
|  |  | ||||||
| import android.app.ActivityManager; |  | ||||||
| import android.app.Application; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.SharedPreferences; |  | ||||||
| import android.util.Log; |  | ||||||
|  |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.core.app.NotificationChannelCompat; |  | ||||||
| import androidx.core.app.NotificationManagerCompat; |  | ||||||
| import androidx.core.content.ContextCompat; |  | ||||||
| import androidx.preference.PreferenceManager; |  | ||||||
|  |  | ||||||
| import com.jakewharton.processphoenix.ProcessPhoenix; |  | ||||||
|  |  | ||||||
| import org.acra.ACRA; |  | ||||||
| import org.acra.config.CoreConfigurationBuilder; |  | ||||||
| import org.schabi.newpipe.error.ReCaptchaActivity; |  | ||||||
| import org.schabi.newpipe.extractor.NewPipe; |  | ||||||
| import org.schabi.newpipe.extractor.downloader.Downloader; |  | ||||||
| import org.schabi.newpipe.ktx.ExceptionUtils; |  | ||||||
| import org.schabi.newpipe.settings.NewPipeSettings; |  | ||||||
| import org.schabi.newpipe.util.BridgeStateSaverInitializer; |  | ||||||
| import org.schabi.newpipe.util.Localization; |  | ||||||
| import org.schabi.newpipe.util.ServiceHelper; |  | ||||||
| import org.schabi.newpipe.util.StateSaver; |  | ||||||
| import org.schabi.newpipe.util.image.ImageStrategy; |  | ||||||
| import org.schabi.newpipe.util.image.PreferredImageQuality; |  | ||||||
|  |  | ||||||
| import java.io.IOException; |  | ||||||
| import java.io.InterruptedIOException; |  | ||||||
| import java.net.SocketException; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Objects; |  | ||||||
|  |  | ||||||
| import coil.ImageLoader; |  | ||||||
| import coil.ImageLoaderFactory; |  | ||||||
| import coil.util.DebugLogger; |  | ||||||
| import dagger.hilt.android.HiltAndroidApp; |  | ||||||
| import io.reactivex.rxjava3.exceptions.CompositeException; |  | ||||||
| import io.reactivex.rxjava3.exceptions.MissingBackpressureException; |  | ||||||
| import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; |  | ||||||
| import io.reactivex.rxjava3.exceptions.UndeliverableException; |  | ||||||
| import io.reactivex.rxjava3.functions.Consumer; |  | ||||||
| import io.reactivex.rxjava3.plugins.RxJavaPlugins; |  | ||||||
|  |  | ||||||
| /* |  | ||||||
|  * Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org> |  | ||||||
|  * App.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/>. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| @HiltAndroidApp |  | ||||||
| public class App extends Application implements ImageLoaderFactory { |  | ||||||
|     public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; |  | ||||||
|     private static final String TAG = App.class.toString(); |  | ||||||
|  |  | ||||||
|     private boolean isFirstRun = false; |  | ||||||
|     private static App app; |  | ||||||
|  |  | ||||||
|     @NonNull |  | ||||||
|     public static App getApp() { |  | ||||||
|         return app; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void attachBaseContext(final Context base) { |  | ||||||
|         super.attachBaseContext(base); |  | ||||||
|         initACRA(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreate() { |  | ||||||
|         super.onCreate(); |  | ||||||
|  |  | ||||||
|         app = this; |  | ||||||
|  |  | ||||||
|         if (ProcessPhoenix.isPhoenixProcess(this)) { |  | ||||||
|             Log.i(TAG, "This is a phoenix process! " |  | ||||||
|                     + "Aborting initialization of App[onCreate]"); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // check if the last used preference version is set |  | ||||||
|         // to determine whether this is the first app run |  | ||||||
|         final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this) |  | ||||||
|                 .getInt(getString(R.string.last_used_preferences_version), -1); |  | ||||||
|         isFirstRun = lastUsedPrefVersion == -1; |  | ||||||
|  |  | ||||||
|         // Initialize settings first because other initializations can use its values |  | ||||||
|         NewPipeSettings.initSettings(this); |  | ||||||
|  |  | ||||||
|         NewPipe.init(getDownloader(), |  | ||||||
|             Localization.getPreferredLocalization(this), |  | ||||||
|             Localization.getPreferredContentCountry(this)); |  | ||||||
|         Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext())); |  | ||||||
|  |  | ||||||
|         BridgeStateSaverInitializer.init(this); |  | ||||||
|         StateSaver.init(this); |  | ||||||
|         initNotificationChannels(); |  | ||||||
|  |  | ||||||
|         ServiceHelper.initServices(this); |  | ||||||
|  |  | ||||||
|         // Initialize image loader |  | ||||||
|         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); |  | ||||||
|         ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this, |  | ||||||
|                 prefs.getString(getString(R.string.image_quality_key), |  | ||||||
|                         getString(R.string.image_quality_default)))); |  | ||||||
|  |  | ||||||
|         configureRxJavaErrorHandler(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @NonNull |  | ||||||
|     @Override |  | ||||||
|     public ImageLoader newImageLoader() { |  | ||||||
|         return new ImageLoader.Builder(this) |  | ||||||
|                 .allowRgb565(ContextCompat.getSystemService(this, ActivityManager.class) |  | ||||||
|                         .isLowRamDevice()) |  | ||||||
|                 .logger(BuildConfig.DEBUG ? new DebugLogger() : null) |  | ||||||
|                 .crossfade(true) |  | ||||||
|                 .build(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected Downloader getDownloader() { |  | ||||||
|         final DownloaderImpl downloader = DownloaderImpl.init(null); |  | ||||||
|         setCookiesToDownloader(downloader); |  | ||||||
|         return downloader; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected void setCookiesToDownloader(final DownloaderImpl downloader) { |  | ||||||
|         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( |  | ||||||
|                 getApplicationContext()); |  | ||||||
|         final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key); |  | ||||||
|         downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null)); |  | ||||||
|         downloader.updateYoutubeRestrictedModeCookies(getApplicationContext()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void configureRxJavaErrorHandler() { |  | ||||||
|         // https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling |  | ||||||
|         RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() { |  | ||||||
|             @Override |  | ||||||
|             public void accept(@NonNull final Throwable throwable) { |  | ||||||
|                 Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : " |  | ||||||
|                         + "throwable = [" + throwable.getClass().getName() + "]"); |  | ||||||
|  |  | ||||||
|                 final Throwable actualThrowable; |  | ||||||
|                 if (throwable instanceof UndeliverableException) { |  | ||||||
|                     // As UndeliverableException is a wrapper, |  | ||||||
|                     // get the cause of it to get the "real" exception |  | ||||||
|                     actualThrowable = Objects.requireNonNull(throwable.getCause()); |  | ||||||
|                 } else { |  | ||||||
|                     actualThrowable = throwable; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 final List<Throwable> errors; |  | ||||||
|                 if (actualThrowable instanceof CompositeException) { |  | ||||||
|                     errors = ((CompositeException) actualThrowable).getExceptions(); |  | ||||||
|                 } else { |  | ||||||
|                     errors = List.of(actualThrowable); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 for (final Throwable error : errors) { |  | ||||||
|                     if (isThrowableIgnored(error)) { |  | ||||||
|                         return; |  | ||||||
|                     } |  | ||||||
|                     if (isThrowableCritical(error)) { |  | ||||||
|                         reportException(error); |  | ||||||
|                         return; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 // Out-of-lifecycle exceptions should only be reported if a debug user wishes so, |  | ||||||
|                 // When exception is not reported, log it |  | ||||||
|                 if (isDisposedRxExceptionsReported()) { |  | ||||||
|                     reportException(actualThrowable); |  | ||||||
|                 } else { |  | ||||||
|                     Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             private boolean isThrowableIgnored(@NonNull final Throwable throwable) { |  | ||||||
|                 // Don't crash the application over a simple network problem |  | ||||||
|                 return ExceptionUtils.hasAssignableCause(throwable, |  | ||||||
|                         // network api cancellation |  | ||||||
|                         IOException.class, SocketException.class, |  | ||||||
|                         // blocking code disposed |  | ||||||
|                         InterruptedException.class, InterruptedIOException.class); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             private boolean isThrowableCritical(@NonNull final Throwable throwable) { |  | ||||||
|                 // Though these exceptions cannot be ignored |  | ||||||
|                 return ExceptionUtils.hasAssignableCause(throwable, |  | ||||||
|                         NullPointerException.class, IllegalArgumentException.class, // bug in app |  | ||||||
|                         OnErrorNotImplementedException.class, MissingBackpressureException.class, |  | ||||||
|                         IllegalStateException.class); // bug in operator |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             private void reportException(@NonNull final Throwable throwable) { |  | ||||||
|                 // Throw uncaught exception that will trigger the report system |  | ||||||
|                 Thread.currentThread().getUncaughtExceptionHandler() |  | ||||||
|                         .uncaughtException(Thread.currentThread(), throwable); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Called in {@link #attachBaseContext(Context)} after calling the {@code super} method. |  | ||||||
|      * Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA. |  | ||||||
|      */ |  | ||||||
|     protected void initACRA() { |  | ||||||
|         if (ACRA.isACRASenderServiceProcess()) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder() |  | ||||||
|                 .withBuildConfigClass(BuildConfig.class); |  | ||||||
|         ACRA.init(this, acraConfig); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void initNotificationChannels() { |  | ||||||
|         // Keep the importance below DEFAULT to avoid making noise on every notification update for |  | ||||||
|         // the main and update channels |  | ||||||
|         final List<NotificationChannelCompat> notificationChannelCompats = List.of( |  | ||||||
|                 new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id), |  | ||||||
|                         NotificationManagerCompat.IMPORTANCE_LOW) |  | ||||||
|                         .setName(getString(R.string.notification_channel_name)) |  | ||||||
|                         .setDescription(getString(R.string.notification_channel_description)) |  | ||||||
|                         .build(), |  | ||||||
|                 new NotificationChannelCompat |  | ||||||
|                         .Builder(getString(R.string.app_update_notification_channel_id), |  | ||||||
|                         NotificationManagerCompat.IMPORTANCE_LOW) |  | ||||||
|                         .setName(getString(R.string.app_update_notification_channel_name)) |  | ||||||
|                         .setDescription( |  | ||||||
|                                 getString(R.string.app_update_notification_channel_description)) |  | ||||||
|                         .build(), |  | ||||||
|                 new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id), |  | ||||||
|                         NotificationManagerCompat.IMPORTANCE_HIGH) |  | ||||||
|                         .setName(getString(R.string.hash_channel_name)) |  | ||||||
|                         .setDescription(getString(R.string.hash_channel_description)) |  | ||||||
|                         .build(), |  | ||||||
|                 new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id), |  | ||||||
|                         NotificationManagerCompat.IMPORTANCE_LOW) |  | ||||||
|                         .setName(getString(R.string.error_report_channel_name)) |  | ||||||
|                         .setDescription(getString(R.string.error_report_channel_description)) |  | ||||||
|                         .build(), |  | ||||||
|                 new NotificationChannelCompat |  | ||||||
|                         .Builder(getString(R.string.streams_notification_channel_id), |  | ||||||
|                         NotificationManagerCompat.IMPORTANCE_DEFAULT) |  | ||||||
|                         .setName(getString(R.string.streams_notification_channel_name)) |  | ||||||
|                         .setDescription( |  | ||||||
|                                 getString(R.string.streams_notification_channel_description)) |  | ||||||
|                         .build() |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); |  | ||||||
|         notificationManager.createNotificationChannelsCompat(notificationChannelCompats); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected boolean isDisposedRxExceptionsReported() { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public boolean isFirstRun() { |  | ||||||
|         return isFirstRun; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										283
									
								
								app/src/main/java/org/schabi/newpipe/App.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								app/src/main/java/org/schabi/newpipe/App.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,283 @@ | |||||||
|  | package org.schabi.newpipe | ||||||
|  |  | ||||||
|  | import android.app.ActivityManager | ||||||
|  | import android.app.Application | ||||||
|  | import android.content.Context | ||||||
|  | import android.util.Log | ||||||
|  | import androidx.core.app.NotificationChannelCompat | ||||||
|  | import androidx.core.app.NotificationManagerCompat | ||||||
|  | import androidx.core.content.getSystemService | ||||||
|  | import androidx.preference.PreferenceManager | ||||||
|  | import coil3.ImageLoader | ||||||
|  | import coil3.SingletonImageLoader | ||||||
|  | import coil3.request.allowRgb565 | ||||||
|  | import coil3.request.crossfade | ||||||
|  | import coil3.util.DebugLogger | ||||||
|  | import com.jakewharton.processphoenix.ProcessPhoenix | ||||||
|  | import dagger.hilt.android.HiltAndroidApp | ||||||
|  | import io.reactivex.rxjava3.exceptions.CompositeException | ||||||
|  | import io.reactivex.rxjava3.exceptions.MissingBackpressureException | ||||||
|  | import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException | ||||||
|  | import io.reactivex.rxjava3.exceptions.UndeliverableException | ||||||
|  | import io.reactivex.rxjava3.functions.Consumer | ||||||
|  | import io.reactivex.rxjava3.plugins.RxJavaPlugins | ||||||
|  | import org.acra.ACRA.init | ||||||
|  | import org.acra.ACRA.isACRASenderServiceProcess | ||||||
|  | import org.acra.config.CoreConfigurationBuilder | ||||||
|  | import org.schabi.newpipe.error.ReCaptchaActivity | ||||||
|  | import org.schabi.newpipe.extractor.NewPipe | ||||||
|  | import org.schabi.newpipe.extractor.downloader.Downloader | ||||||
|  | import org.schabi.newpipe.ktx.hasAssignableCause | ||||||
|  | import org.schabi.newpipe.settings.NewPipeSettings | ||||||
|  | import org.schabi.newpipe.util.BridgeStateSaverInitializer | ||||||
|  | import org.schabi.newpipe.util.Localization | ||||||
|  | import org.schabi.newpipe.util.ServiceHelper | ||||||
|  | import org.schabi.newpipe.util.StateSaver | ||||||
|  | import org.schabi.newpipe.util.image.ImageStrategy | ||||||
|  | import org.schabi.newpipe.util.image.PreferredImageQuality | ||||||
|  | import java.io.IOException | ||||||
|  | import java.io.InterruptedIOException | ||||||
|  | import java.net.SocketException | ||||||
|  |  | ||||||
|  | /* | ||||||
|  |  * Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org> | ||||||
|  |  * App.kt 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/>. | ||||||
|  |  */ | ||||||
|  | @HiltAndroidApp | ||||||
|  | open class App : | ||||||
|  |     Application(), | ||||||
|  |     SingletonImageLoader.Factory { | ||||||
|  |     var isFirstRun = false | ||||||
|  |         private set | ||||||
|  |  | ||||||
|  |     override fun attachBaseContext(base: Context?) { | ||||||
|  |         super.attachBaseContext(base) | ||||||
|  |         initACRA() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreate() { | ||||||
|  |         super.onCreate() | ||||||
|  |  | ||||||
|  |         instance = this | ||||||
|  |  | ||||||
|  |         if (ProcessPhoenix.isPhoenixProcess(this)) { | ||||||
|  |             Log.i(TAG, "This is a phoenix process! Aborting initialization of App[onCreate]") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // check if the last used preference version is set | ||||||
|  |         // to determine whether this is the first app run | ||||||
|  |         val lastUsedPrefVersion = | ||||||
|  |             PreferenceManager | ||||||
|  |                 .getDefaultSharedPreferences(this) | ||||||
|  |                 .getInt(getString(R.string.last_used_preferences_version), -1) | ||||||
|  |         isFirstRun = lastUsedPrefVersion == -1 | ||||||
|  |  | ||||||
|  |         // Initialize settings first because other initializations can use its values | ||||||
|  |         NewPipeSettings.initSettings(this) | ||||||
|  |  | ||||||
|  |         NewPipe.init( | ||||||
|  |             getDownloader(), | ||||||
|  |             Localization.getPreferredLocalization(this), | ||||||
|  |             Localization.getPreferredContentCountry(this), | ||||||
|  |         ) | ||||||
|  |         Localization.initPrettyTime(Localization.resolvePrettyTime(this)) | ||||||
|  |  | ||||||
|  |         BridgeStateSaverInitializer.init(this) | ||||||
|  |         StateSaver.init(this) | ||||||
|  |         initNotificationChannels() | ||||||
|  |  | ||||||
|  |         ServiceHelper.initServices(this) | ||||||
|  |  | ||||||
|  |         // Initialize image loader | ||||||
|  |         val prefs = PreferenceManager.getDefaultSharedPreferences(this) | ||||||
|  |         ImageStrategy.setPreferredImageQuality( | ||||||
|  |             PreferredImageQuality.fromPreferenceKey( | ||||||
|  |                 this, | ||||||
|  |                 prefs.getString( | ||||||
|  |                     getString(R.string.image_quality_key), | ||||||
|  |                     getString(R.string.image_quality_default), | ||||||
|  |                 ), | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         configureRxJavaErrorHandler() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun newImageLoader(context: Context): ImageLoader = | ||||||
|  |         ImageLoader | ||||||
|  |             .Builder(this) | ||||||
|  |             .logger(if (BuildConfig.DEBUG) DebugLogger() else null) | ||||||
|  |             .allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice) | ||||||
|  |             .crossfade(true) | ||||||
|  |             .build() | ||||||
|  |  | ||||||
|  |     protected open fun getDownloader(): Downloader { | ||||||
|  |         val downloader = DownloaderImpl.init(null) | ||||||
|  |         setCookiesToDownloader(downloader) | ||||||
|  |         return downloader | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected fun setCookiesToDownloader(downloader: DownloaderImpl) { | ||||||
|  |         val prefs = PreferenceManager.getDefaultSharedPreferences(this) | ||||||
|  |         val key = getString(R.string.recaptcha_cookies_key) | ||||||
|  |         downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null)) | ||||||
|  |         downloader.updateYoutubeRestrictedModeCookies(this) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun configureRxJavaErrorHandler() { | ||||||
|  |         // https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling | ||||||
|  |         RxJavaPlugins.setErrorHandler( | ||||||
|  |             object : Consumer<Throwable> { | ||||||
|  |                 override fun accept(throwable: Throwable) { | ||||||
|  |                     Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [${throwable.javaClass.getName()}]") | ||||||
|  |  | ||||||
|  |                     // As UndeliverableException is a wrapper, | ||||||
|  |                     // get the cause of it to get the "real" exception | ||||||
|  |                     val actualThrowable = (throwable as? UndeliverableException)?.cause ?: throwable | ||||||
|  |  | ||||||
|  |                     val errors = (actualThrowable as? CompositeException)?.exceptions ?: listOf(actualThrowable) | ||||||
|  |  | ||||||
|  |                     for (error in errors) { | ||||||
|  |                         if (isThrowableIgnored(error)) { | ||||||
|  |                             return | ||||||
|  |                         } | ||||||
|  |                         if (isThrowableCritical(error)) { | ||||||
|  |                             reportException(error) | ||||||
|  |                             return | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     // Out-of-lifecycle exceptions should only be reported if a debug user wishes so, | ||||||
|  |                     // When exception is not reported, log it | ||||||
|  |                     if (isDisposedRxExceptionsReported()) { | ||||||
|  |                         reportException(actualThrowable) | ||||||
|  |                     } else { | ||||||
|  |                         Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 fun isThrowableIgnored(throwable: Throwable): Boolean { | ||||||
|  |                     // Don't crash the application over a simple network problem | ||||||
|  |                     return throwable // network api cancellation | ||||||
|  |                         .hasAssignableCause( | ||||||
|  |                             IOException::class.java, | ||||||
|  |                             SocketException::class.java, // blocking code disposed | ||||||
|  |                             InterruptedException::class.java, | ||||||
|  |                             InterruptedIOException::class.java, | ||||||
|  |                         ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 fun isThrowableCritical(throwable: Throwable): Boolean { | ||||||
|  |                     // Though these exceptions cannot be ignored | ||||||
|  |                     return throwable | ||||||
|  |                         .hasAssignableCause( | ||||||
|  |                             // bug in app | ||||||
|  |                             NullPointerException::class.java, | ||||||
|  |                             IllegalArgumentException::class.java, | ||||||
|  |                             OnErrorNotImplementedException::class.java, | ||||||
|  |                             MissingBackpressureException::class.java, | ||||||
|  |                             // bug in operator | ||||||
|  |                             IllegalStateException::class.java, | ||||||
|  |                         ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 fun reportException(throwable: Throwable) { | ||||||
|  |                     // Throw uncaught exception that will trigger the report system | ||||||
|  |                     Thread | ||||||
|  |                         .currentThread() | ||||||
|  |                         .uncaughtExceptionHandler | ||||||
|  |                         .uncaughtException(Thread.currentThread(), throwable) | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Called in [.attachBaseContext] after calling the `super` method. | ||||||
|  |      * Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA. | ||||||
|  |      */ | ||||||
|  |     protected fun initACRA() { | ||||||
|  |         if (isACRASenderServiceProcess()) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val acraConfig = | ||||||
|  |             CoreConfigurationBuilder() | ||||||
|  |                 .withBuildConfigClass(BuildConfig::class.java) | ||||||
|  |         init(this, acraConfig) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun initNotificationChannels() { | ||||||
|  |         // Keep the importance below DEFAULT to avoid making noise on every notification update for | ||||||
|  |         // the main and update channels | ||||||
|  |         val mainChannel = | ||||||
|  |             NotificationChannelCompat | ||||||
|  |                 .Builder( | ||||||
|  |                     getString(R.string.notification_channel_id), | ||||||
|  |                     NotificationManagerCompat.IMPORTANCE_LOW, | ||||||
|  |                 ).setName(getString(R.string.notification_channel_name)) | ||||||
|  |                 .setDescription(getString(R.string.notification_channel_description)) | ||||||
|  |                 .build() | ||||||
|  |         val appUpdateChannel = | ||||||
|  |             NotificationChannelCompat | ||||||
|  |                 .Builder( | ||||||
|  |                     getString(R.string.app_update_notification_channel_id), | ||||||
|  |                     NotificationManagerCompat.IMPORTANCE_LOW, | ||||||
|  |                 ).setName(getString(R.string.app_update_notification_channel_name)) | ||||||
|  |                 .setDescription(getString(R.string.app_update_notification_channel_description)) | ||||||
|  |                 .build() | ||||||
|  |         val hashChannel = | ||||||
|  |             NotificationChannelCompat | ||||||
|  |                 .Builder( | ||||||
|  |                     getString(R.string.hash_channel_id), | ||||||
|  |                     NotificationManagerCompat.IMPORTANCE_HIGH, | ||||||
|  |                 ).setName(getString(R.string.hash_channel_name)) | ||||||
|  |                 .setDescription(getString(R.string.hash_channel_description)) | ||||||
|  |                 .build() | ||||||
|  |         val errorReportChannel = | ||||||
|  |             NotificationChannelCompat | ||||||
|  |                 .Builder( | ||||||
|  |                     getString(R.string.error_report_channel_id), | ||||||
|  |                     NotificationManagerCompat.IMPORTANCE_LOW, | ||||||
|  |                 ).setName(getString(R.string.error_report_channel_name)) | ||||||
|  |                 .setDescription(getString(R.string.error_report_channel_description)) | ||||||
|  |                 .build() | ||||||
|  |         val newStreamChannel = | ||||||
|  |             NotificationChannelCompat | ||||||
|  |                 .Builder( | ||||||
|  |                     getString(R.string.streams_notification_channel_id), | ||||||
|  |                     NotificationManagerCompat.IMPORTANCE_DEFAULT, | ||||||
|  |                 ).setName(getString(R.string.streams_notification_channel_name)) | ||||||
|  |                 .setDescription(getString(R.string.streams_notification_channel_description)) | ||||||
|  |                 .build() | ||||||
|  |  | ||||||
|  |         val channels = listOf(mainChannel, appUpdateChannel, hashChannel, errorReportChannel, newStreamChannel) | ||||||
|  |  | ||||||
|  |         NotificationManagerCompat.from(this).createNotificationChannelsCompat(channels) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected open fun isDisposedRxExceptionsReported(): Boolean = false | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         const val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID | ||||||
|  |         private val TAG = App::class.java.toString() | ||||||
|  |  | ||||||
|  |         @JvmStatic | ||||||
|  |         lateinit var instance: App | ||||||
|  |             private set | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -166,7 +166,7 @@ public class MainActivity extends AppCompatActivity { | |||||||
|             NotificationWorker.initialize(this); |             NotificationWorker.initialize(this); | ||||||
|         } |         } | ||||||
|         if (!UpdateSettingsFragment.wasUserAskedForConsent(this) |         if (!UpdateSettingsFragment.wasUserAskedForConsent(this) | ||||||
|                 && !App.getApp().isFirstRun() |                 && !App.getInstance().isFirstRun() | ||||||
|                 && ReleaseVersionUtil.INSTANCE.isReleaseApk()) { |                 && ReleaseVersionUtil.INSTANCE.isReleaseApk()) { | ||||||
|             UpdateSettingsFragment.askForConsentToUpdateChecks(this); |             UpdateSettingsFragment.askForConsentToUpdateChecks(this); | ||||||
|         } |         } | ||||||
| @@ -176,7 +176,7 @@ public class MainActivity extends AppCompatActivity { | |||||||
|     protected void onPostCreate(final Bundle savedInstanceState) { |     protected void onPostCreate(final Bundle savedInstanceState) { | ||||||
|         super.onPostCreate(savedInstanceState); |         super.onPostCreate(savedInstanceState); | ||||||
|  |  | ||||||
|         final App app = App.getApp(); |         final App app = App.getInstance(); | ||||||
|         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); |         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); | ||||||
|  |  | ||||||
|         if (prefs.getBoolean(app.getString(R.string.update_app_key), false) |         if (prefs.getBoolean(app.getString(R.string.update_app_key), false) | ||||||
|   | |||||||
| @@ -127,7 +127,7 @@ import java.util.Optional; | |||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
| import java.util.function.Consumer; | import java.util.function.Consumer; | ||||||
|  |  | ||||||
| import coil.util.CoilUtils; | import coil3.util.CoilUtils; | ||||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | ||||||
| import io.reactivex.rxjava3.disposables.CompositeDisposable; | import io.reactivex.rxjava3.disposables.CompositeDisposable; | ||||||
| import io.reactivex.rxjava3.disposables.Disposable; | import io.reactivex.rxjava3.disposables.Disposable; | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ import java.util.List; | |||||||
| import java.util.Queue; | import java.util.Queue; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
| import coil.util.CoilUtils; | import coil3.util.CoilUtils; | ||||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | ||||||
| import io.reactivex.rxjava3.core.Observable; | import io.reactivex.rxjava3.core.Observable; | ||||||
| import io.reactivex.rxjava3.disposables.CompositeDisposable; | import io.reactivex.rxjava3.disposables.CompositeDisposable; | ||||||
|   | |||||||
| @@ -62,7 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean; | |||||||
| import java.util.function.Supplier; | import java.util.function.Supplier; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
|  |  | ||||||
| import coil.util.CoilUtils; | import coil3.util.CoilUtils; | ||||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | ||||||
| import io.reactivex.rxjava3.core.Flowable; | import io.reactivex.rxjava3.core.Flowable; | ||||||
| import io.reactivex.rxjava3.core.Single; | import io.reactivex.rxjava3.core.Single; | ||||||
|   | |||||||
| @@ -346,7 +346,7 @@ public final class InfoItemDialog { | |||||||
|  |  | ||||||
|         public static void reportErrorDuringInitialization(final Throwable throwable, |         public static void reportErrorDuringInitialization(final Throwable throwable, | ||||||
|                                                            final InfoItem item) { |                                                            final InfoItem item) { | ||||||
|             ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo( |             ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo( | ||||||
|                     throwable, |                     throwable, | ||||||
|                     UserAction.OPEN_INFO_ITEM_DIALOG, |                     UserAction.OPEN_INFO_ITEM_DIALOG, | ||||||
|                     "none", |                     "none", | ||||||
|   | |||||||
| @@ -165,7 +165,7 @@ class FeedViewModel( | |||||||
|         fun getFactory(context: Context, groupId: Long) = viewModelFactory { |         fun getFactory(context: Context, groupId: Long) = viewModelFactory { | ||||||
|             initializer { |             initializer { | ||||||
|                 FeedViewModel( |                 FeedViewModel( | ||||||
|                     App.getApp(), |                     App.instance, | ||||||
|                     groupId, |                     groupId, | ||||||
|                     // Read initial value from preferences |                     // Read initial value from preferences | ||||||
|                     getShowPlayedItemsFromPreferences(context.applicationContext), |                     getShowPlayedItemsFromPreferences(context.applicationContext), | ||||||
|   | |||||||
| @@ -46,6 +46,7 @@ import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; | |||||||
| import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; | import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; | ||||||
| import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; | import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; | ||||||
| import static java.util.concurrent.TimeUnit.MILLISECONDS; | import static java.util.concurrent.TimeUnit.MILLISECONDS; | ||||||
|  | import static coil3.Image_androidKt.toBitmap; | ||||||
|  |  | ||||||
| import android.content.BroadcastReceiver; | import android.content.BroadcastReceiver; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| @@ -53,14 +54,12 @@ import android.content.Intent; | |||||||
| import android.content.IntentFilter; | import android.content.IntentFilter; | ||||||
| import android.content.SharedPreferences; | import android.content.SharedPreferences; | ||||||
| import android.graphics.Bitmap; | import android.graphics.Bitmap; | ||||||
| import android.graphics.drawable.Drawable; |  | ||||||
| import android.media.AudioManager; | import android.media.AudioManager; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.core.graphics.drawable.DrawableKt; |  | ||||||
| import androidx.core.math.MathUtils; | import androidx.core.math.MathUtils; | ||||||
| import androidx.preference.PreferenceManager; | import androidx.preference.PreferenceManager; | ||||||
|  |  | ||||||
| @@ -125,7 +124,7 @@ import java.util.List; | |||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
| import java.util.stream.IntStream; | import java.util.stream.IntStream; | ||||||
|  |  | ||||||
| import coil.target.Target; | import coil3.target.Target; | ||||||
| import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; | ||||||
| import io.reactivex.rxjava3.core.Observable; | import io.reactivex.rxjava3.core.Observable; | ||||||
| import io.reactivex.rxjava3.disposables.CompositeDisposable; | import io.reactivex.rxjava3.disposables.CompositeDisposable; | ||||||
| @@ -193,7 +192,7 @@ public final class Player implements PlaybackListener, Listener { | |||||||
|     @Nullable |     @Nullable | ||||||
|     private Bitmap currentThumbnail; |     private Bitmap currentThumbnail; | ||||||
|     @Nullable |     @Nullable | ||||||
|     private coil.request.Disposable thumbnailDisposable; |     private coil3.request.Disposable thumbnailDisposable; | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Player |     // Player | ||||||
| @@ -789,27 +788,26 @@ public final class Player implements PlaybackListener, Listener { | |||||||
|         // scale down the notification thumbnail for performance |         // scale down the notification thumbnail for performance | ||||||
|         final var thumbnailTarget = new Target() { |         final var thumbnailTarget = new Target() { | ||||||
|             @Override |             @Override | ||||||
|             public void onError(@Nullable final Drawable error) { |             public void onError(@Nullable final coil3.Image error) { | ||||||
|                 Log.e(TAG, "Thumbnail - onError() called"); |                 Log.e(TAG, "Thumbnail - onError() called"); | ||||||
|                 // there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too. |                 // there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too. | ||||||
|                 onThumbnailLoaded(null); |                 onThumbnailLoaded(null); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onStart(@Nullable final Drawable placeholder) { |             public void onStart(@Nullable final coil3.Image placeholder) { | ||||||
|                 if (DEBUG) { |                 if (DEBUG) { | ||||||
|                     Log.d(TAG, "Thumbnail - onStart() called"); |                     Log.d(TAG, "Thumbnail - onStart() called"); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onSuccess(@NonNull final Drawable result) { |             public void onSuccess(@NonNull final coil3.Image result) { | ||||||
|                 if (DEBUG) { |                 if (DEBUG) { | ||||||
|                     Log.d(TAG, "Thumbnail - onSuccess() called with: drawable = [" + result + "]"); |                     Log.d(TAG, "Thumbnail - onSuccess() called with: drawable = [" + result + "]"); | ||||||
|                 } |                 } | ||||||
|                 // there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too. |                 // there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too. | ||||||
|                 onThumbnailLoaded(DrawableKt.toBitmapOrNull(result, result.getIntrinsicWidth(), |                 onThumbnailLoaded(toBitmap(result)); | ||||||
|                         result.getIntrinsicHeight(), null)); |  | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|         thumbnailDisposable = CoilHelper.INSTANCE |         thumbnailDisposable = CoilHelper.INSTANCE | ||||||
|   | |||||||
| @@ -16,8 +16,8 @@ import com.google.android.exoplayer2.PlaybackParameters; | |||||||
| import org.schabi.newpipe.App; | import org.schabi.newpipe.App; | ||||||
| import org.schabi.newpipe.MainActivity; | import org.schabi.newpipe.MainActivity; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
| import org.schabi.newpipe.player.PlayerService; |  | ||||||
| import org.schabi.newpipe.player.Player; | import org.schabi.newpipe.player.Player; | ||||||
|  | import org.schabi.newpipe.player.PlayerService; | ||||||
| import org.schabi.newpipe.player.PlayerType; | import org.schabi.newpipe.player.PlayerType; | ||||||
| import org.schabi.newpipe.player.event.PlayerServiceEventListener; | import org.schabi.newpipe.player.event.PlayerServiceEventListener; | ||||||
| import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; | import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; | ||||||
| @@ -116,7 +116,7 @@ public final class PlayerHolder { | |||||||
|     // helper to handle context in common place as using the same |     // helper to handle context in common place as using the same | ||||||
|     // context to bind/unbind a service is crucial |     // context to bind/unbind a service is crucial | ||||||
|     private Context getCommonContext() { |     private Context getCommonContext() { | ||||||
|         return App.getApp(); |         return App.getInstance(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void startService(final boolean playAfterConnect, |     public void startService(final boolean playAfterConnect, | ||||||
|   | |||||||
| @@ -179,7 +179,7 @@ public class SeekbarPreviewThumbnailHolder { | |||||||
|  |  | ||||||
|             // Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient |             // Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient | ||||||
|             // Ensure that you are not running on the main thread, otherwise this will hang |             // Ensure that you are not running on the main thread, otherwise this will hang | ||||||
|             final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getApp(), url); |             final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getInstance(), url); | ||||||
|  |  | ||||||
|             if (sw != null) { |             if (sw != null) { | ||||||
|                 Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took " |                 Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took " | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.localization.Localization; | |||||||
| import org.schabi.newpipe.util.image.ImageStrategy; | import org.schabi.newpipe.util.image.ImageStrategy; | ||||||
| import org.schabi.newpipe.util.image.PreferredImageQuality; | import org.schabi.newpipe.util.image.PreferredImageQuality; | ||||||
|  |  | ||||||
| import coil.Coil; | import coil3.SingletonImageLoader; | ||||||
|  |  | ||||||
| public class ContentSettingsFragment extends BasePreferenceFragment { | public class ContentSettingsFragment extends BasePreferenceFragment { | ||||||
|     private String youtubeRestrictedModeEnabledKey; |     private String youtubeRestrictedModeEnabledKey; | ||||||
| @@ -41,7 +41,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { | |||||||
|                 (preference, newValue) -> { |                 (preference, newValue) -> { | ||||||
|                     ImageStrategy.setPreferredImageQuality(PreferredImageQuality |                     ImageStrategy.setPreferredImageQuality(PreferredImageQuality | ||||||
|                             .fromPreferenceKey(requireContext(), (String) newValue)); |                             .fromPreferenceKey(requireContext(), (String) newValue)); | ||||||
|                     final var loader = Coil.imageLoader(preference.getContext()); |                     final var loader = SingletonImageLoader.get(preference.getContext()); | ||||||
|                     loader.getMemoryCache().clear(); |                     loader.getMemoryCache().clear(); | ||||||
|                     loader.getDiskCache().clear(); |                     loader.getDiskCache().clear(); | ||||||
|                     Toast.makeText(preference.getContext(), |                     Toast.makeText(preference.getContext(), | ||||||
|   | |||||||
| @@ -156,7 +156,7 @@ public final class NewPipeSettings { | |||||||
|                 prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0 |                 prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0 | ||||||
|                         && !prefs.getBoolean(disabledTunnelingKey, false); |                         && !prefs.getBoolean(disabledTunnelingKey, false); | ||||||
|  |  | ||||||
|         if (App.getApp().isFirstRun() |         if (App.getInstance().isFirstRun() | ||||||
|                 || (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) { |                 || (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) { | ||||||
|             setMediaTunneling(context); |             setMediaTunneling(context); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| package org.schabi.newpipe.settings; | package org.schabi.newpipe.settings; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.MainActivity.DEBUG; | ||||||
|  |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.SharedPreferences; | import android.content.SharedPreferences; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| @@ -18,8 +20,6 @@ import java.util.Collections; | |||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.MainActivity.DEBUG; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * In order to add a migration, follow these steps, given P is the previous version:<br> |  * In order to add a migration, follow these steps, given P is the previous version:<br> | ||||||
|  * - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in |  * - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in | ||||||
| @@ -171,7 +171,7 @@ public final class SettingMigrations { | |||||||
|         final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0); |         final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0); | ||||||
|  |  | ||||||
|         // no migration to run, already up to date |         // no migration to run, already up to date | ||||||
|         if (App.getApp().isFirstRun()) { |         if (App.getInstance().isFirstRun()) { | ||||||
|             sp.edit().putInt(lastPrefVersionKey, VERSION).apply(); |             sp.edit().putInt(lastPrefVersionKey, VERSION).apply(); | ||||||
|             return; |             return; | ||||||
|         } else if (lastPrefVersion == VERSION) { |         } else if (lastPrefVersion == VERSION) { | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ import androidx.compose.ui.layout.ContentScale | |||||||
| import androidx.compose.ui.platform.LocalContext | import androidx.compose.ui.platform.LocalContext | ||||||
| import androidx.compose.ui.res.painterResource | import androidx.compose.ui.res.painterResource | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import coil.compose.AsyncImage | import coil3.compose.AsyncImage | ||||||
| import org.schabi.newpipe.R | import org.schabi.newpipe.R | ||||||
| import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem | import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem | ||||||
| import org.schabi.newpipe.util.Localization | import org.schabi.newpipe.util.Localization | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ import androidx.compose.ui.res.painterResource | |||||||
| import androidx.compose.ui.res.stringResource | import androidx.compose.ui.res.stringResource | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import androidx.lifecycle.viewmodel.compose.viewModel | import androidx.lifecycle.viewmodel.compose.viewModel | ||||||
| import coil.compose.AsyncImage | import coil3.compose.AsyncImage | ||||||
| import org.schabi.newpipe.R | import org.schabi.newpipe.R | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem | import org.schabi.newpipe.extractor.stream.StreamInfoItem | ||||||
| import org.schabi.newpipe.util.Localization | import org.schabi.newpipe.util.Localization | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview | |||||||
| import androidx.compose.ui.tooling.preview.PreviewParameter | import androidx.compose.ui.tooling.preview.PreviewParameter | ||||||
| import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider | import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import coil.compose.AsyncImage | import coil3.compose.AsyncImage | ||||||
| import org.schabi.newpipe.R | import org.schabi.newpipe.R | ||||||
| import org.schabi.newpipe.extractor.Page | import org.schabi.newpipe.extractor.Page | ||||||
| import org.schabi.newpipe.extractor.comments.CommentsInfoItem | import org.schabi.newpipe.extractor.comments.CommentsInfoItem | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow | |||||||
| import androidx.compose.ui.tooling.preview.Preview | import androidx.compose.ui.tooling.preview.Preview | ||||||
| import androidx.compose.ui.tooling.preview.datasource.LoremIpsum | import androidx.compose.ui.tooling.preview.datasource.LoremIpsum | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import coil.compose.AsyncImage | import coil3.compose.AsyncImage | ||||||
| import org.schabi.newpipe.R | import org.schabi.newpipe.R | ||||||
| import org.schabi.newpipe.extractor.comments.CommentsInfoItem | import org.schabi.newpipe.extractor.comments.CommentsInfoItem | ||||||
| import org.schabi.newpipe.extractor.stream.Description | import org.schabi.newpipe.extractor.stream.Description | ||||||
|   | |||||||
| @@ -130,7 +130,7 @@ public final class DeviceUtils { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         isFireTV = |         isFireTV = | ||||||
|                 App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV); |                 App.getInstance().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV); | ||||||
|         return isFireTV; |         return isFireTV; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -139,7 +139,7 @@ public final class DeviceUtils { | |||||||
|             return isTV; |             return isTV; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         final PackageManager pm = App.getApp().getPackageManager(); |         final PackageManager pm = App.getInstance().getPackageManager(); | ||||||
|  |  | ||||||
|         // from doc: https://developer.android.com/training/tv/start/hardware.html#runtime-check |         // from doc: https://developer.android.com/training/tv/start/hardware.html#runtime-check | ||||||
|         boolean isTv = ContextCompat.getSystemService(context, UiModeManager.class) |         boolean isTv = ContextCompat.getSystemService(context, UiModeManager.class) | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ object ReleaseVersionUtil { | |||||||
|         val certificates = mapOf( |         val certificates = mapOf( | ||||||
|             RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256 |             RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256 | ||||||
|         ) |         ) | ||||||
|         val app = App.getApp() |         val app = App.instance | ||||||
|         try { |         try { | ||||||
|             PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false) |             PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false) | ||||||
|         } catch (e: PackageManager.NameNotFoundException) { |         } catch (e: PackageManager.NameNotFoundException) { | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| package org.schabi.newpipe.util.external_communication; | package org.schabi.newpipe.util.external_communication; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.MainActivity.DEBUG; | import static org.schabi.newpipe.MainActivity.DEBUG; | ||||||
|  | import static coil3.Image_androidKt.toBitmap; | ||||||
|  |  | ||||||
| import android.content.ActivityNotFoundException; | import android.content.ActivityNotFoundException; | ||||||
| import android.content.ClipData; | import android.content.ClipData; | ||||||
| @@ -31,9 +32,9 @@ import java.nio.file.Files; | |||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
| import coil.Coil; | import coil3.SingletonImageLoader; | ||||||
| import coil.disk.DiskCache; | import coil3.disk.DiskCache; | ||||||
| import coil.memory.MemoryCache; | import coil3.memory.MemoryCache; | ||||||
|  |  | ||||||
| public final class ShareUtils { | public final class ShareUtils { | ||||||
|     private static final String TAG = ShareUtils.class.getSimpleName(); |     private static final String TAG = ShareUtils.class.getSimpleName(); | ||||||
| @@ -377,13 +378,13 @@ public final class ShareUtils { | |||||||
|             // Save the image in memory to the application's cache because we need a URI to the |             // Save the image in memory to the application's cache because we need a URI to the | ||||||
|             // image to generate a ClipData which will show the share sheet, and so an image file |             // image to generate a ClipData which will show the share sheet, and so an image file | ||||||
|             final Context applicationContext = context.getApplicationContext(); |             final Context applicationContext = context.getApplicationContext(); | ||||||
|             final var loader = Coil.imageLoader(context); |             final var loader = SingletonImageLoader.get(context); | ||||||
|             final var value = loader.getMemoryCache() |             final var value = loader.getMemoryCache() | ||||||
|                     .get(new MemoryCache.Key(thumbnailUrl, Collections.emptyMap())); |                     .get(new MemoryCache.Key(thumbnailUrl, Collections.emptyMap())); | ||||||
|  |  | ||||||
|             final Bitmap cachedBitmap; |             final Bitmap cachedBitmap; | ||||||
|             if (value != null) { |             if (value != null) { | ||||||
|                 cachedBitmap = value.getBitmap(); |                 cachedBitmap = toBitmap(value.getImage()); | ||||||
|             } else { |             } else { | ||||||
|                 try (var snapshot = loader.getDiskCache().openSnapshot(thumbnailUrl)) { |                 try (var snapshot = loader.getDiskCache().openSnapshot(thumbnailUrl)) { | ||||||
|                     if (snapshot != null) { |                     if (snapshot != null) { | ||||||
|   | |||||||
| @@ -5,14 +5,18 @@ import android.graphics.Bitmap | |||||||
| import android.util.Log | import android.util.Log | ||||||
| import android.widget.ImageView | import android.widget.ImageView | ||||||
| import androidx.annotation.DrawableRes | import androidx.annotation.DrawableRes | ||||||
| import androidx.core.graphics.drawable.toBitmapOrNull | import coil3.executeBlocking | ||||||
| import coil.executeBlocking | import coil3.imageLoader | ||||||
| import coil.imageLoader | import coil3.request.Disposable | ||||||
| import coil.request.Disposable | import coil3.request.ImageRequest | ||||||
| import coil.request.ImageRequest | import coil3.request.error | ||||||
| import coil.size.Size | import coil3.request.placeholder | ||||||
| import coil.target.Target | import coil3.request.target | ||||||
| import coil.transform.Transformation | import coil3.request.transformations | ||||||
|  | import coil3.size.Size | ||||||
|  | import coil3.target.Target | ||||||
|  | import coil3.toBitmap | ||||||
|  | import coil3.transform.Transformation | ||||||
| import org.schabi.newpipe.MainActivity | import org.schabi.newpipe.MainActivity | ||||||
| import org.schabi.newpipe.R | import org.schabi.newpipe.R | ||||||
| import org.schabi.newpipe.extractor.Image | import org.schabi.newpipe.extractor.Image | ||||||
| @@ -26,84 +30,119 @@ object CoilHelper { | |||||||
|     fun loadBitmapBlocking( |     fun loadBitmapBlocking( | ||||||
|         context: Context, |         context: Context, | ||||||
|         url: String?, |         url: String?, | ||||||
|         @DrawableRes placeholderResId: Int = 0 |         @DrawableRes placeholderResId: Int = 0, | ||||||
|     ): Bitmap? { |     ): Bitmap? = | ||||||
|         val request = getImageRequest(context, url, placeholderResId).build() |         context.imageLoader | ||||||
|         return context.imageLoader.executeBlocking(request).drawable?.toBitmapOrNull() |             .executeBlocking(getImageRequest(context, url, placeholderResId).build()) | ||||||
|     } |             .image | ||||||
|  |             ?.toBitmap() | ||||||
|  |  | ||||||
|     fun loadAvatar(target: ImageView, images: List<Image>) { |     fun loadAvatar( | ||||||
|  |         target: ImageView, | ||||||
|  |         images: List<Image>, | ||||||
|  |     ) { | ||||||
|         loadImageDefault(target, images, R.drawable.placeholder_person) |         loadImageDefault(target, images, R.drawable.placeholder_person) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun loadAvatar(target: ImageView, url: String?) { |     fun loadAvatar( | ||||||
|  |         target: ImageView, | ||||||
|  |         url: String?, | ||||||
|  |     ) { | ||||||
|         loadImageDefault(target, url, R.drawable.placeholder_person) |         loadImageDefault(target, url, R.drawable.placeholder_person) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun loadThumbnail(target: ImageView, images: List<Image>) { |     fun loadThumbnail( | ||||||
|  |         target: ImageView, | ||||||
|  |         images: List<Image>, | ||||||
|  |     ) { | ||||||
|         loadImageDefault(target, images, R.drawable.placeholder_thumbnail_video) |         loadImageDefault(target, images, R.drawable.placeholder_thumbnail_video) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun loadThumbnail(target: ImageView, url: String?) { |     fun loadThumbnail( | ||||||
|  |         target: ImageView, | ||||||
|  |         url: String?, | ||||||
|  |     ) { | ||||||
|         loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video) |         loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun loadScaledDownThumbnail(context: Context, images: List<Image>, target: Target): Disposable { |     fun loadScaledDownThumbnail( | ||||||
|  |         context: Context, | ||||||
|  |         images: List<Image>, | ||||||
|  |         target: Target, | ||||||
|  |     ): Disposable { | ||||||
|         val url = ImageStrategy.choosePreferredImage(images) |         val url = ImageStrategy.choosePreferredImage(images) | ||||||
|         val request = getImageRequest(context, url, R.drawable.placeholder_thumbnail_video) |         val request = | ||||||
|             .target(target) |             getImageRequest(context, url, R.drawable.placeholder_thumbnail_video) | ||||||
|             .transformations(object : Transformation { |                 .target(target) | ||||||
|                 override val cacheKey = "COIL_PLAYER_THUMBNAIL_TRANSFORMATION_KEY" |                 .transformations( | ||||||
|  |                     object : Transformation() { | ||||||
|  |                         override val cacheKey = "COIL_PLAYER_THUMBNAIL_TRANSFORMATION_KEY" | ||||||
|  |  | ||||||
|                 override suspend fun transform(input: Bitmap, size: Size): Bitmap { |                         override suspend fun transform( | ||||||
|                     if (MainActivity.DEBUG) { |                             input: Bitmap, | ||||||
|                         Log.d(TAG, "Thumbnail - transform() called") |                             size: Size, | ||||||
|                     } |                         ): Bitmap { | ||||||
|  |                             if (MainActivity.DEBUG) { | ||||||
|  |                                 Log.d(TAG, "Thumbnail - transform() called") | ||||||
|  |                             } | ||||||
|  |  | ||||||
|                     val notificationThumbnailWidth = min( |                             val notificationThumbnailWidth = | ||||||
|                         context.resources.getDimension(R.dimen.player_notification_thumbnail_width), |                                 min( | ||||||
|                         input.width.toFloat() |                                     context.resources.getDimension(R.dimen.player_notification_thumbnail_width), | ||||||
|                     ).toInt() |                                     input.width.toFloat(), | ||||||
|  |                                 ).toInt() | ||||||
|  |  | ||||||
|                     var newHeight = input.height / (input.width / notificationThumbnailWidth) |                             var newHeight = input.height / (input.width / notificationThumbnailWidth) | ||||||
|                     val result = input.scale(notificationThumbnailWidth, newHeight) |                             val result = input.scale(notificationThumbnailWidth, newHeight) | ||||||
|  |  | ||||||
|                     return if (result == input || !result.isMutable) { |                             return if (result == input || !result.isMutable) { | ||||||
|                         // create a new mutable bitmap to prevent strange crashes on some |                                 // create a new mutable bitmap to prevent strange crashes on some | ||||||
|                         // devices (see #4638) |                                 // devices (see #4638) | ||||||
|                         newHeight = input.height / (input.width / (notificationThumbnailWidth - 1)) |                                 newHeight = input.height / (input.width / (notificationThumbnailWidth - 1)) | ||||||
|                         input.scale(notificationThumbnailWidth, newHeight) |                                 input.scale(notificationThumbnailWidth, newHeight) | ||||||
|                     } else { |                             } else { | ||||||
|                         result |                                 result | ||||||
|                     } |                             } | ||||||
|                 } |                         } | ||||||
|             }) |                     }, | ||||||
|             .build() |                 ).build() | ||||||
|  |  | ||||||
|         return context.imageLoader.enqueue(request) |         return context.imageLoader.enqueue(request) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun loadDetailsThumbnail(target: ImageView, images: List<Image>) { |     fun loadDetailsThumbnail( | ||||||
|  |         target: ImageView, | ||||||
|  |         images: List<Image>, | ||||||
|  |     ) { | ||||||
|         val url = ImageStrategy.choosePreferredImage(images) |         val url = ImageStrategy.choosePreferredImage(images) | ||||||
|         loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video, false) |         loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video, false) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun loadBanner(target: ImageView, images: List<Image>) { |     fun loadBanner( | ||||||
|  |         target: ImageView, | ||||||
|  |         images: List<Image>, | ||||||
|  |     ) { | ||||||
|         loadImageDefault(target, images, R.drawable.placeholder_channel_banner) |         loadImageDefault(target, images, R.drawable.placeholder_channel_banner) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun loadPlaylistThumbnail(target: ImageView, images: List<Image>) { |     fun loadPlaylistThumbnail( | ||||||
|  |         target: ImageView, | ||||||
|  |         images: List<Image>, | ||||||
|  |     ) { | ||||||
|         loadImageDefault(target, images, R.drawable.placeholder_thumbnail_playlist) |         loadImageDefault(target, images, R.drawable.placeholder_thumbnail_playlist) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun loadPlaylistThumbnail(target: ImageView, url: String?) { |     fun loadPlaylistThumbnail( | ||||||
|  |         target: ImageView, | ||||||
|  |         url: String?, | ||||||
|  |     ) { | ||||||
|         loadImageDefault(target, url, R.drawable.placeholder_thumbnail_playlist) |         loadImageDefault(target, url, R.drawable.placeholder_thumbnail_playlist) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private fun loadImageDefault( |     private fun loadImageDefault( | ||||||
|         target: ImageView, |         target: ImageView, | ||||||
|         images: List<Image>, |         images: List<Image>, | ||||||
|         @DrawableRes placeholderResId: Int |         @DrawableRes placeholderResId: Int, | ||||||
|     ) { |     ) { | ||||||
|         loadImageDefault(target, ImageStrategy.choosePreferredImage(images), placeholderResId) |         loadImageDefault(target, ImageStrategy.choosePreferredImage(images), placeholderResId) | ||||||
|     } |     } | ||||||
| @@ -112,11 +151,12 @@ object CoilHelper { | |||||||
|         target: ImageView, |         target: ImageView, | ||||||
|         url: String?, |         url: String?, | ||||||
|         @DrawableRes placeholderResId: Int, |         @DrawableRes placeholderResId: Int, | ||||||
|         showPlaceholder: Boolean = true |         showPlaceholder: Boolean = true, | ||||||
|     ) { |     ) { | ||||||
|         val request = getImageRequest(target.context, url, placeholderResId, showPlaceholder) |         val request = | ||||||
|             .target(target) |             getImageRequest(target.context, url, placeholderResId, showPlaceholder) | ||||||
|             .build() |                 .target(target) | ||||||
|  |                 .build() | ||||||
|         target.context.imageLoader.enqueue(request) |         target.context.imageLoader.enqueue(request) | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -124,14 +164,15 @@ object CoilHelper { | |||||||
|         context: Context, |         context: Context, | ||||||
|         url: String?, |         url: String?, | ||||||
|         @DrawableRes placeholderResId: Int, |         @DrawableRes placeholderResId: Int, | ||||||
|         showPlaceholderWhileLoading: Boolean = true |         showPlaceholderWhileLoading: Boolean = true, | ||||||
|     ): ImageRequest.Builder { |     ): ImageRequest.Builder { | ||||||
|         // if the URL was chosen with `choosePreferredImage` it will be null, but check again |         // if the URL was chosen with `choosePreferredImage` it will be null, but check again | ||||||
|         // `shouldLoadImages` in case the URL was chosen with `imageListToDbUrl` (which is the case |         // `shouldLoadImages` in case the URL was chosen with `imageListToDbUrl` (which is the case | ||||||
|         // for URLs stored in the database) |         // for URLs stored in the database) | ||||||
|         val takenUrl = url?.takeIf { it.isNotEmpty() && ImageStrategy.shouldLoadImages() } |         val takenUrl = url?.takeIf { it.isNotEmpty() && ImageStrategy.shouldLoadImages() } | ||||||
|  |  | ||||||
|         return ImageRequest.Builder(context) |         return ImageRequest | ||||||
|  |             .Builder(context) | ||||||
|             .data(takenUrl) |             .data(takenUrl) | ||||||
|             .error(placeholderResId) |             .error(placeholderResId) | ||||||
|             .memoryCacheKey(takenUrl) |             .memoryCacheKey(takenUrl) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Isira Seneviratne
					Isira Seneviratne