1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-12-23 08:30:44 +00:00

Migrate to Coil 3

This commit is contained in:
Isira Seneviratne 2024-11-20 08:44:18 +05:30
parent 6a98b1dac7
commit ca855cbca0
22 changed files with 334 additions and 297 deletions

View File

@ -124,6 +124,8 @@ ext {
leakCanaryVersion = '2.12' leakCanaryVersion = '2.12'
stethoVersion = '1.6.0' stethoVersion = '1.6.0'
coilVersion = '3.0.3'
} }
configurations { configurations {
@ -207,7 +209,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 **/
@ -272,7 +274,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}"

View File

@ -1,49 +1,43 @@
package org.schabi.newpipe; package org.schabi.newpipe
import android.app.ActivityManager; import android.app.ActivityManager
import android.app.Application; import android.app.Application
import android.content.Context; import android.content.Context
import android.content.SharedPreferences; import android.util.Log
import android.util.Log; import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat
import androidx.annotation.NonNull; import androidx.core.content.getSystemService
import androidx.core.app.NotificationChannelCompat; import androidx.preference.PreferenceManager
import androidx.core.app.NotificationManagerCompat; import coil3.ImageLoader
import androidx.core.content.ContextCompat; import coil3.SingletonImageLoader
import androidx.preference.PreferenceManager; import coil3.request.allowRgb565
import coil3.request.crossfade
import com.jakewharton.processphoenix.ProcessPhoenix; import coil3.util.DebugLogger
import com.jakewharton.processphoenix.ProcessPhoenix
import org.acra.ACRA; import dagger.hilt.android.HiltAndroidApp
import org.acra.config.CoreConfigurationBuilder; import io.reactivex.rxjava3.exceptions.CompositeException
import org.schabi.newpipe.error.ReCaptchaActivity; import io.reactivex.rxjava3.exceptions.MissingBackpressureException
import org.schabi.newpipe.extractor.NewPipe; import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException
import org.schabi.newpipe.extractor.downloader.Downloader; import io.reactivex.rxjava3.exceptions.UndeliverableException
import org.schabi.newpipe.ktx.ExceptionUtils; import io.reactivex.rxjava3.functions.Consumer
import org.schabi.newpipe.settings.NewPipeSettings; import io.reactivex.rxjava3.plugins.RxJavaPlugins
import org.schabi.newpipe.util.BridgeStateSaverInitializer; import org.acra.ACRA.init
import org.schabi.newpipe.util.Localization; import org.acra.ACRA.isACRASenderServiceProcess
import org.schabi.newpipe.util.ServiceHelper; import org.acra.config.CoreConfigurationBuilder
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.error.ReCaptchaActivity
import org.schabi.newpipe.util.image.ImageStrategy; import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.util.image.PreferredImageQuality; import org.schabi.newpipe.extractor.downloader.Downloader
import org.schabi.newpipe.ktx.hasAssignableCause
import java.io.IOException; import org.schabi.newpipe.settings.NewPipeSettings
import java.io.InterruptedIOException; import org.schabi.newpipe.util.BridgeStateSaverInitializer
import java.net.SocketException; import org.schabi.newpipe.util.Localization
import java.util.List; import org.schabi.newpipe.util.ServiceHelper
import java.util.Objects; import org.schabi.newpipe.util.StateSaver
import org.schabi.newpipe.util.image.ImageStrategy
import coil.ImageLoader; import org.schabi.newpipe.util.image.PreferredImageQuality
import coil.ImageLoaderFactory; import java.io.IOException
import coil.util.DebugLogger; import java.io.InterruptedIOException
import dagger.hilt.android.HiltAndroidApp; import java.net.SocketException
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> * Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
@ -62,218 +56,218 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>. * along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/ */
@HiltAndroidApp @HiltAndroidApp
public class App extends Application implements ImageLoaderFactory { open class App :
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; Application(),
private static final String TAG = App.class.toString(); SingletonImageLoader.Factory {
var isFirstRun = false
private set
private boolean isFirstRun = false; override fun attachBaseContext(base: Context?) {
private static App app; super.attachBaseContext(base)
initACRA()
@NonNull
public static App getApp() {
return app;
} }
@Override override fun onCreate() {
protected void attachBaseContext(final Context base) { super.onCreate()
super.attachBaseContext(base);
initACRA();
}
@Override instance = this
public void onCreate() {
super.onCreate();
app = this;
if (ProcessPhoenix.isPhoenixProcess(this)) { if (ProcessPhoenix.isPhoenixProcess(this)) {
Log.i(TAG, "This is a phoenix process! " Log.i(TAG, "This is a phoenix process! Aborting initialization of App[onCreate]")
+ "Aborting initialization of App[onCreate]"); return
return;
} }
// check if the last used preference version is set // check if the last used preference version is set
// to determine whether this is the first app run // to determine whether this is the first app run
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this) val lastUsedPrefVersion =
.getInt(getString(R.string.last_used_preferences_version), -1); PreferenceManager
isFirstRun = lastUsedPrefVersion == -1; .getDefaultSharedPreferences(this)
.getInt(getString(R.string.last_used_preferences_version), -1)
isFirstRun = lastUsedPrefVersion == -1
// Initialize settings first because other initializations can use its values // Initialize settings first because other initializations can use its values
NewPipeSettings.initSettings(this); NewPipeSettings.initSettings(this)
NewPipe.init(getDownloader(), NewPipe.init(
getDownloader(),
Localization.getPreferredLocalization(this), Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this)); Localization.getPreferredContentCountry(this),
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext())); )
Localization.initPrettyTime(Localization.resolvePrettyTime(this))
BridgeStateSaverInitializer.init(this); BridgeStateSaverInitializer.init(this)
StateSaver.init(this); StateSaver.init(this)
initNotificationChannels(); initNotificationChannels()
ServiceHelper.initServices(this); ServiceHelper.initServices(this)
// Initialize image loader // Initialize image loader
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); val prefs = PreferenceManager.getDefaultSharedPreferences(this)
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this, ImageStrategy.setPreferredImageQuality(
prefs.getString(getString(R.string.image_quality_key), PreferredImageQuality.fromPreferenceKey(
getString(R.string.image_quality_default)))); this,
prefs.getString(
getString(R.string.image_quality_key),
getString(R.string.image_quality_default),
),
),
)
configureRxJavaErrorHandler(); configureRxJavaErrorHandler()
} }
@NonNull override fun newImageLoader(context: Context): ImageLoader =
@Override ImageLoader
public ImageLoader newImageLoader() { .Builder(this)
return new ImageLoader.Builder(this) .logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(ContextCompat.getSystemService(this, ActivityManager.class) .allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
.isLowRamDevice()) .crossfade(true)
.logger(BuildConfig.DEBUG ? new DebugLogger() : null) .build()
.crossfade(true)
.build(); protected open fun getDownloader(): Downloader {
val downloader = DownloaderImpl.init(null)
setCookiesToDownloader(downloader)
return downloader
} }
protected Downloader getDownloader() { protected fun setCookiesToDownloader(downloader: DownloaderImpl) {
final DownloaderImpl downloader = DownloaderImpl.init(null); val prefs = PreferenceManager.getDefaultSharedPreferences(this)
setCookiesToDownloader(downloader); val key = getString(R.string.recaptcha_cookies_key)
return downloader; downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null))
downloader.updateYoutubeRestrictedModeCookies(this)
} }
protected void setCookiesToDownloader(final DownloaderImpl downloader) { private fun configureRxJavaErrorHandler() {
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 // https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() { RxJavaPlugins.setErrorHandler(
@Override object : Consumer<Throwable> {
public void accept(@NonNull final Throwable throwable) { override fun accept(throwable: Throwable) {
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : " Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [${throwable.javaClass.getName()}]")
+ "throwable = [" + throwable.getClass().getName() + "]");
final Throwable actualThrowable;
if (throwable instanceof UndeliverableException) {
// As UndeliverableException is a wrapper, // As UndeliverableException is a wrapper,
// get the cause of it to get the "real" exception // get the cause of it to get the "real" exception
actualThrowable = Objects.requireNonNull(throwable.getCause()); val actualThrowable = (throwable as? UndeliverableException)?.cause ?: throwable
} else {
actualThrowable = throwable;
}
final List<Throwable> errors; val errors = (actualThrowable as? CompositeException)?.exceptions ?: listOf(actualThrowable)
if (actualThrowable instanceof CompositeException) {
errors = ((CompositeException) actualThrowable).getExceptions();
} else {
errors = List.of(actualThrowable);
}
for (final Throwable error : errors) { for (error in errors) {
if (isThrowableIgnored(error)) { if (isThrowableIgnored(error)) {
return; return
}
if (isThrowableCritical(error)) {
reportException(error)
return
}
} }
if (isThrowableCritical(error)) {
reportException(error); // Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
return; // When exception is not reported, log it
if (isDisposedRxExceptionsReported()) {
reportException(actualThrowable)
} else {
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable)
} }
} }
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so, fun isThrowableIgnored(throwable: Throwable): Boolean {
// When exception is not reported, log it // Don't crash the application over a simple network problem
if (isDisposedRxExceptionsReported()) { return throwable // network api cancellation
reportException(actualThrowable); .hasAssignableCause(
} else { IOException::class.java,
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable); SocketException::class.java, // blocking code disposed
InterruptedException::class.java,
InterruptedIOException::class.java,
)
} }
}
private boolean isThrowableIgnored(@NonNull final Throwable throwable) { fun isThrowableCritical(throwable: Throwable): Boolean {
// Don't crash the application over a simple network problem // Though these exceptions cannot be ignored
return ExceptionUtils.hasAssignableCause(throwable, return throwable
// network api cancellation .hasAssignableCause(
IOException.class, SocketException.class, NullPointerException::class.java,
// blocking code disposed IllegalArgumentException::class.java, // bug in app
InterruptedException.class, InterruptedIOException.class); OnErrorNotImplementedException::class.java,
} MissingBackpressureException::class.java,
IllegalStateException::class.java,
) // bug in operator
}
private boolean isThrowableCritical(@NonNull final Throwable throwable) { fun reportException(throwable: Throwable) {
// Though these exceptions cannot be ignored // Throw uncaught exception that will trigger the report system
return ExceptionUtils.hasAssignableCause(throwable, Thread.currentThread().uncaughtExceptionHandler
NullPointerException.class, IllegalArgumentException.class, // bug in app .uncaughtException(Thread.currentThread(), throwable)
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. * Called in [.attachBaseContext] after calling the `super` method.
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA. * Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
*/ */
protected void initACRA() { protected fun initACRA() {
if (ACRA.isACRASenderServiceProcess()) { if (isACRASenderServiceProcess()) {
return; return
} }
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder() val acraConfig =
.withBuildConfigClass(BuildConfig.class); CoreConfigurationBuilder()
ACRA.init(this, acraConfig); .withBuildConfigClass(BuildConfig::class.java)
init(this, acraConfig)
} }
private void initNotificationChannels() { private fun initNotificationChannels() {
// Keep the importance below DEFAULT to avoid making noise on every notification update for // Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels // the main and update channels
final List<NotificationChannelCompat> notificationChannelCompats = List.of( val mainChannel = NotificationChannelCompat.Builder(
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id), getString(R.string.notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW) NotificationManagerCompat.IMPORTANCE_LOW,
.setName(getString(R.string.notification_channel_name)) )
.setDescription(getString(R.string.notification_channel_description)) .setName(getString(R.string.notification_channel_name))
.build(), .setDescription(getString(R.string.notification_channel_description))
new NotificationChannelCompat .build()
.Builder(getString(R.string.app_update_notification_channel_id), val appUpdateChannel = NotificationChannelCompat.Builder(
NotificationManagerCompat.IMPORTANCE_LOW) getString(R.string.app_update_notification_channel_id),
.setName(getString(R.string.app_update_notification_channel_name)) NotificationManagerCompat.IMPORTANCE_LOW,
.setDescription( )
getString(R.string.app_update_notification_channel_description)) .setName(getString(R.string.app_update_notification_channel_name))
.build(), .setDescription(getString(R.string.app_update_notification_channel_description))
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id), .build()
NotificationManagerCompat.IMPORTANCE_HIGH) val hashChannel = NotificationChannelCompat.Builder(
.setName(getString(R.string.hash_channel_name)) getString(R.string.hash_channel_id),
.setDescription(getString(R.string.hash_channel_description)) NotificationManagerCompat.IMPORTANCE_HIGH,
.build(), )
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id), .setName(getString(R.string.hash_channel_name))
NotificationManagerCompat.IMPORTANCE_LOW) .setDescription(getString(R.string.hash_channel_description))
.setName(getString(R.string.error_report_channel_name)) .build()
.setDescription(getString(R.string.error_report_channel_description)) val errorReportChannel = NotificationChannelCompat.Builder(
.build(), getString(R.string.error_report_channel_id),
new NotificationChannelCompat NotificationManagerCompat.IMPORTANCE_LOW,
.Builder(getString(R.string.streams_notification_channel_id), )
NotificationManagerCompat.IMPORTANCE_DEFAULT) .setName(getString(R.string.error_report_channel_name))
.setName(getString(R.string.streams_notification_channel_name)) .setDescription(getString(R.string.error_report_channel_description))
.setDescription( .build()
getString(R.string.streams_notification_channel_description)) val newStreamChannel = NotificationChannelCompat.Builder(
.build() 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); val channels = listOf(mainChannel, appUpdateChannel, hashChannel, errorReportChannel, newStreamChannel)
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
NotificationManagerCompat.from(this).createNotificationChannelsCompat(channels)
} }
protected boolean isDisposedRxExceptionsReported() { protected open fun isDisposedRxExceptionsReported(): Boolean = false
return false;
}
public boolean isFirstRun() { companion object {
return isFirstRun; const val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID
private val TAG = App::class.java.toString()
@JvmStatic
lateinit var instance: App
} }
} }

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -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",

View File

@ -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),

View File

@ -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

View File

@ -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,

View File

@ -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 "

View File

@ -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(),

View File

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

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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) {

View File

@ -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) {

View File

@ -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)