diff --git a/app/build.gradle b/app/build.gradle index 24e3d32cf..98270fa70 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -124,6 +124,8 @@ ext { leakCanaryVersion = '2.12' stethoVersion = '1.6.0' + + coilVersion = '3.0.3' } configurations { @@ -207,7 +209,7 @@ dependencies { // This works thanks to JitPack: https://jitpack.io/ implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' // 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' /** Checkstyle **/ @@ -272,7 +274,8 @@ dependencies { implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}" // 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 implementation "io.noties.markwon:core:${markwonVersion}" diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java deleted file mode 100644 index d271dba0d..000000000 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ /dev/null @@ -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 - * 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 . - */ - -@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() { - @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 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 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; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/App.kt b/app/src/main/java/org/schabi/newpipe/App.kt new file mode 100644 index 000000000..0c065f491 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/App.kt @@ -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 + * 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 . + */ +@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()!!.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 { + 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 + } +} diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 41abd10c1..39431537d 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -166,7 +166,7 @@ public class MainActivity extends AppCompatActivity { NotificationWorker.initialize(this); } if (!UpdateSettingsFragment.wasUserAskedForConsent(this) - && !App.getApp().isFirstRun() + && !App.getInstance().isFirstRun() && ReleaseVersionUtil.INSTANCE.isReleaseApk()) { UpdateSettingsFragment.askForConsentToUpdateChecks(this); } @@ -176,7 +176,7 @@ public class MainActivity extends AppCompatActivity { protected void onPostCreate(final Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); - final App app = App.getApp(); + final App app = App.getInstance(); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); if (prefs.getBoolean(app.getString(R.string.update_app_key), false) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 4f5bd9e94..63077e92d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -127,7 +127,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import coil.util.CoilUtils; +import coil3.util.CoilUtils; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 56d8a9315..55e3ae52a 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -60,7 +60,7 @@ import java.util.List; import java.util.Queue; import java.util.concurrent.TimeUnit; -import coil.util.CoilUtils; +import coil3.util.CoilUtils; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.disposables.CompositeDisposable; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index d4607a9ff..be4f076dd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -62,7 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import java.util.stream.Collectors; -import coil.util.CoilUtils; +import coil3.util.CoilUtils; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Single; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java index 0c69557bf..dcf01e190 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java @@ -346,7 +346,7 @@ public final class InfoItemDialog { public static void reportErrorDuringInitialization(final Throwable throwable, final InfoItem item) { - ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo( + ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo( throwable, UserAction.OPEN_INFO_ITEM_DIALOG, "none", diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt index 728570b17..462e8ef21 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt @@ -165,7 +165,7 @@ class FeedViewModel( fun getFactory(context: Context, groupId: Long) = viewModelFactory { initializer { FeedViewModel( - App.getApp(), + App.instance, groupId, // Read initial value from preferences getShowPlayedItemsFromPreferences(context.applicationContext), diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 74d35cf31..ab5274996 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -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.Localization.assureCorrectAppLanguage; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static coil3.Image_androidKt.toBitmap; import android.content.BroadcastReceiver; import android.content.Context; @@ -53,14 +54,12 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.util.Log; import android.view.LayoutInflater; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.graphics.drawable.DrawableKt; import androidx.core.math.MathUtils; import androidx.preference.PreferenceManager; @@ -125,7 +124,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.IntStream; -import coil.target.Target; +import coil3.target.Target; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.disposables.CompositeDisposable; @@ -193,7 +192,7 @@ public final class Player implements PlaybackListener, Listener { @Nullable private Bitmap currentThumbnail; @Nullable - private coil.request.Disposable thumbnailDisposable; + private coil3.request.Disposable thumbnailDisposable; /*////////////////////////////////////////////////////////////////////////// // Player @@ -789,27 +788,26 @@ public final class Player implements PlaybackListener, Listener { // scale down the notification thumbnail for performance final var thumbnailTarget = new Target() { @Override - public void onError(@Nullable final Drawable error) { + public void onError(@Nullable final coil3.Image error) { Log.e(TAG, "Thumbnail - onError() called"); // there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too. onThumbnailLoaded(null); } @Override - public void onStart(@Nullable final Drawable placeholder) { + public void onStart(@Nullable final coil3.Image placeholder) { if (DEBUG) { Log.d(TAG, "Thumbnail - onStart() called"); } } @Override - public void onSuccess(@NonNull final Drawable result) { + public void onSuccess(@NonNull final coil3.Image result) { if (DEBUG) { 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. - onThumbnailLoaded(DrawableKt.toBitmapOrNull(result, result.getIntrinsicWidth(), - result.getIntrinsicHeight(), null)); + onThumbnailLoaded(toBitmap(result)); } }; thumbnailDisposable = CoilHelper.INSTANCE diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java index b55a6547a..24939c1d8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -16,8 +16,8 @@ import com.google.android.exoplayer2.PlaybackParameters; import org.schabi.newpipe.App; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.event.PlayerServiceEventListener; 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 // context to bind/unbind a service is crucial private Context getCommonContext() { - return App.getApp(); + return App.getInstance(); } public void startService(final boolean playAfterConnect, diff --git a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java b/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java index d09664aeb..863e2fb8a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java @@ -179,7 +179,7 @@ public class SeekbarPreviewThumbnailHolder { // 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 - final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getApp(), url); + final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getInstance(), url); if (sw != null) { Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took " diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index c47abb930..fca8c7162 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.util.image.ImageStrategy; import org.schabi.newpipe.util.image.PreferredImageQuality; -import coil.Coil; +import coil3.SingletonImageLoader; public class ContentSettingsFragment extends BasePreferenceFragment { private String youtubeRestrictedModeEnabledKey; @@ -41,7 +41,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { (preference, newValue) -> { ImageStrategy.setPreferredImageQuality(PreferredImageQuality .fromPreferenceKey(requireContext(), (String) newValue)); - final var loader = Coil.imageLoader(preference.getContext()); + final var loader = SingletonImageLoader.get(preference.getContext()); loader.getMemoryCache().clear(); loader.getDiskCache().clear(); Toast.makeText(preference.getContext(), diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 421440ea7..9fe5240cc 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -156,7 +156,7 @@ public final class NewPipeSettings { prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0 && !prefs.getBoolean(disabledTunnelingKey, false); - if (App.getApp().isFirstRun() + if (App.getInstance().isFirstRun() || (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) { setMediaTunneling(context); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java index d731f2f5e..a77e1c514 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.settings; +import static org.schabi.newpipe.MainActivity.DEBUG; + import android.content.Context; import android.content.SharedPreferences; import android.util.Log; @@ -18,8 +20,6 @@ import java.util.Collections; import java.util.HashSet; 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:
* - 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); // no migration to run, already up to date - if (App.getApp().isFirstRun()) { + if (App.getInstance().isFirstRun()) { sp.edit().putInt(lastPrefVersionKey, VERSION).apply(); return; } else if (lastPrefVersion == VERSION) { diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt index b1d4d200c..36711105b 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage +import coil3.compose.AsyncImage import org.schabi.newpipe.R import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem import org.schabi.newpipe.util.Localization diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt index bcccd3217..f5515a24a 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import coil.compose.AsyncImage +import coil3.compose.AsyncImage import org.schabi.newpipe.R import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.util.Localization diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/Comment.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/Comment.kt index 7f29269d1..efa87b581 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/Comment.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/Comment.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage +import coil3.compose.AsyncImage import org.schabi.newpipe.R import org.schabi.newpipe.extractor.Page import org.schabi.newpipe.extractor.comments.CommentsInfoItem diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesHeader.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesHeader.kt index 95542092e..e6627f7f0 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesHeader.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesHeader.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage +import coil3.compose.AsyncImage import org.schabi.newpipe.R import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.stream.Description diff --git a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java index e9678c2b0..7a357a0c1 100644 --- a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java @@ -130,7 +130,7 @@ public final class DeviceUtils { } isFireTV = - App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV); + App.getInstance().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV); return isFireTV; } @@ -139,7 +139,7 @@ public final class DeviceUtils { 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 boolean isTv = ContextCompat.getSystemService(context, UiModeManager.class) diff --git a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt index 3ea19fa4f..080f5bace 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt +++ b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt @@ -21,7 +21,7 @@ object ReleaseVersionUtil { val certificates = mapOf( RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256 ) - val app = App.getApp() + val app = App.instance try { PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false) } catch (e: PackageManager.NameNotFoundException) { diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java index 9008a213d..4be5445bc 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.util.external_communication; import static org.schabi.newpipe.MainActivity.DEBUG; +import static coil3.Image_androidKt.toBitmap; import android.content.ActivityNotFoundException; import android.content.ClipData; @@ -31,9 +32,9 @@ import java.nio.file.Files; import java.util.Collections; import java.util.List; -import coil.Coil; -import coil.disk.DiskCache; -import coil.memory.MemoryCache; +import coil3.SingletonImageLoader; +import coil3.disk.DiskCache; +import coil3.memory.MemoryCache; public final class ShareUtils { 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 // image to generate a ClipData which will show the share sheet, and so an image file final Context applicationContext = context.getApplicationContext(); - final var loader = Coil.imageLoader(context); + final var loader = SingletonImageLoader.get(context); final var value = loader.getMemoryCache() .get(new MemoryCache.Key(thumbnailUrl, Collections.emptyMap())); final Bitmap cachedBitmap; if (value != null) { - cachedBitmap = value.getBitmap(); + cachedBitmap = toBitmap(value.getImage()); } else { try (var snapshot = loader.getDiskCache().openSnapshot(thumbnailUrl)) { if (snapshot != null) { diff --git a/app/src/main/java/org/schabi/newpipe/util/image/CoilHelper.kt b/app/src/main/java/org/schabi/newpipe/util/image/CoilHelper.kt index 2608090dc..5b393658c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/image/CoilHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/util/image/CoilHelper.kt @@ -5,14 +5,18 @@ import android.graphics.Bitmap import android.util.Log import android.widget.ImageView import androidx.annotation.DrawableRes -import androidx.core.graphics.drawable.toBitmapOrNull -import coil.executeBlocking -import coil.imageLoader -import coil.request.Disposable -import coil.request.ImageRequest -import coil.size.Size -import coil.target.Target -import coil.transform.Transformation +import coil3.executeBlocking +import coil3.imageLoader +import coil3.request.Disposable +import coil3.request.ImageRequest +import coil3.request.error +import coil3.request.placeholder +import coil3.request.target +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.R import org.schabi.newpipe.extractor.Image @@ -26,84 +30,119 @@ object CoilHelper { fun loadBitmapBlocking( context: Context, url: String?, - @DrawableRes placeholderResId: Int = 0 - ): Bitmap? { - val request = getImageRequest(context, url, placeholderResId).build() - return context.imageLoader.executeBlocking(request).drawable?.toBitmapOrNull() - } + @DrawableRes placeholderResId: Int = 0, + ): Bitmap? = + context.imageLoader + .executeBlocking(getImageRequest(context, url, placeholderResId).build()) + .image + ?.toBitmap() - fun loadAvatar(target: ImageView, images: List) { + fun loadAvatar( + target: ImageView, + images: List, + ) { 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) } - fun loadThumbnail(target: ImageView, images: List) { + fun loadThumbnail( + target: ImageView, + images: List, + ) { 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) } - fun loadScaledDownThumbnail(context: Context, images: List, target: Target): Disposable { + fun loadScaledDownThumbnail( + context: Context, + images: List, + target: Target, + ): Disposable { val url = ImageStrategy.choosePreferredImage(images) - val request = getImageRequest(context, url, R.drawable.placeholder_thumbnail_video) - .target(target) - .transformations(object : Transformation { - override val cacheKey = "COIL_PLAYER_THUMBNAIL_TRANSFORMATION_KEY" + val request = + getImageRequest(context, url, R.drawable.placeholder_thumbnail_video) + .target(target) + .transformations( + object : Transformation() { + override val cacheKey = "COIL_PLAYER_THUMBNAIL_TRANSFORMATION_KEY" - override suspend fun transform(input: Bitmap, size: Size): Bitmap { - if (MainActivity.DEBUG) { - Log.d(TAG, "Thumbnail - transform() called") - } + override suspend fun transform( + input: Bitmap, + size: Size, + ): Bitmap { + if (MainActivity.DEBUG) { + Log.d(TAG, "Thumbnail - transform() called") + } - val notificationThumbnailWidth = min( - context.resources.getDimension(R.dimen.player_notification_thumbnail_width), - input.width.toFloat() - ).toInt() + val notificationThumbnailWidth = + min( + context.resources.getDimension(R.dimen.player_notification_thumbnail_width), + input.width.toFloat(), + ).toInt() - var newHeight = input.height / (input.width / notificationThumbnailWidth) - val result = input.scale(notificationThumbnailWidth, newHeight) + var newHeight = input.height / (input.width / notificationThumbnailWidth) + val result = input.scale(notificationThumbnailWidth, newHeight) - return if (result == input || !result.isMutable) { - // create a new mutable bitmap to prevent strange crashes on some - // devices (see #4638) - newHeight = input.height / (input.width / (notificationThumbnailWidth - 1)) - input.scale(notificationThumbnailWidth, newHeight) - } else { - result - } - } - }) - .build() + return if (result == input || !result.isMutable) { + // create a new mutable bitmap to prevent strange crashes on some + // devices (see #4638) + newHeight = input.height / (input.width / (notificationThumbnailWidth - 1)) + input.scale(notificationThumbnailWidth, newHeight) + } else { + result + } + } + }, + ).build() return context.imageLoader.enqueue(request) } - fun loadDetailsThumbnail(target: ImageView, images: List) { + fun loadDetailsThumbnail( + target: ImageView, + images: List, + ) { val url = ImageStrategy.choosePreferredImage(images) loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video, false) } - fun loadBanner(target: ImageView, images: List) { + fun loadBanner( + target: ImageView, + images: List, + ) { loadImageDefault(target, images, R.drawable.placeholder_channel_banner) } - fun loadPlaylistThumbnail(target: ImageView, images: List) { + fun loadPlaylistThumbnail( + target: ImageView, + images: List, + ) { 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) } private fun loadImageDefault( target: ImageView, images: List, - @DrawableRes placeholderResId: Int + @DrawableRes placeholderResId: Int, ) { loadImageDefault(target, ImageStrategy.choosePreferredImage(images), placeholderResId) } @@ -112,11 +151,12 @@ object CoilHelper { target: ImageView, url: String?, @DrawableRes placeholderResId: Int, - showPlaceholder: Boolean = true + showPlaceholder: Boolean = true, ) { - val request = getImageRequest(target.context, url, placeholderResId, showPlaceholder) - .target(target) - .build() + val request = + getImageRequest(target.context, url, placeholderResId, showPlaceholder) + .target(target) + .build() target.context.imageLoader.enqueue(request) } @@ -124,14 +164,15 @@ object CoilHelper { context: Context, url: String?, @DrawableRes placeholderResId: Int, - showPlaceholderWhileLoading: Boolean = true + showPlaceholderWhileLoading: Boolean = true, ): ImageRequest.Builder { // 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 // for URLs stored in the database) val takenUrl = url?.takeIf { it.isNotEmpty() && ImageStrategy.shouldLoadImages() } - return ImageRequest.Builder(context) + return ImageRequest + .Builder(context) .data(takenUrl) .error(placeholderResId) .memoryCacheKey(takenUrl)