Compare commits

..

No commits in common. "dev" and "revert-12781-feat/similar-youtube-client-screen-rotation" have entirely different histories.

366 changed files with 3955 additions and 4293 deletions

View File

@ -22,7 +22,7 @@ jobs:
github.event.comment.author_association == 'MEMBER'
)
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Get backport metadata
# the target branch is the first argument after `/backport`
env:

View File

@ -38,7 +38,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: gradle/actions/wrapper-validation@v5
- uses: gradle/actions/wrapper-validation@v4
- name: create and checkout branch
# push events already checked out the branch

View File

@ -3,8 +3,6 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import com.android.build.api.dsl.ApplicationExtension
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
@ -34,7 +32,7 @@ kotlin {
}
}
configure<ApplicationExtension> {
android {
compileSdk = 36
namespace = "org.schabi.newpipe"
@ -44,9 +42,9 @@ configure<ApplicationExtension> {
minSdk = 21
targetSdk = 35
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1008
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1006
versionName = "0.28.3"
versionName = "0.28.1"
System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it }
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -79,18 +77,19 @@ configure<ApplicationExtension> {
resValue("string", "app_name", "NewPipe $suffix")
}
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
isShrinkResources = false // disabled to fix F-Droid"s reproducible build
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
lint {
lintConfig = file("lint.xml")
// Continue the debug build even when errors are found
checkReleaseBuilds = false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
abortOnError = false
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
// 5.0, avoid using them in switch case statements"), which affects only library projects
disable += "NonConstantResourceId"
}
compileOptions {
@ -101,7 +100,7 @@ configure<ApplicationExtension> {
sourceSets {
getByName("androidTest") {
assets.directories += "$projectDir/schemas"
assets.srcDir("$projectDir/schemas")
}
}
@ -112,7 +111,6 @@ configure<ApplicationExtension> {
buildFeatures {
viewBinding = true
buildConfig = true
resValues = true
}
packaging {
@ -272,8 +270,7 @@ dependencies {
implementation(libs.lisawray.groupie.viewbinding)
// Image loading
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
implementation(libs.squareup.picasso)
// Markdown library for Android
implementation(libs.noties.markwon.core)

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<lint>
<issue id="MissingTranslation" severity="ignore" />
<issue id="MissingQuantity" severity="ignore" />
<issue id="ImpliedQuantity" severity="ignore" />
</lint>

View File

@ -39,8 +39,3 @@
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
-keep class org.schabi.newpipe.settings.notifications.** { *; }
# Prevent R8 from stripping or renaming Protobuf internal fields
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
<fields>;
}

View File

@ -0,0 +1,285 @@
package org.schabi.newpipe;
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.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.extractor.services.youtube.extractors.YoutubeStreamExtractor;
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.PicassoHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.List;
import java.util.Objects;
import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* App.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class App extends Application {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString();
private boolean isFirstRun = false;
private boolean notificationsRequested = false;
private static App app;
@NonNull
public static App getApp() {
return app;
}
public boolean getNotificationsRequested() {
return notificationsRequested;
}
public void setNotificationsRequested() {
notificationsRequested = true;
}
@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());
BridgeStateSaverInitializer.init(this);
StateSaver.init(this);
initNotificationChannels();
ServiceHelper.initServices(this);
// Initialize image loader
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
PicassoHelper.init(this);
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
prefs.getString(getString(R.string.image_quality_key),
getString(R.string.image_quality_default))));
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
configureRxJavaErrorHandler();
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl.INSTANCE);
}
@Override
public void onTerminate() {
super.onTerminate();
PicassoHelper.terminate();
}
protected Downloader getDownloader() {
final DownloaderImpl downloader = DownloaderImpl.init(null);
setCookiesToDownloader(downloader);
return downloader;
}
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
}
private void configureRxJavaErrorHandler() {
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
@Override
public void accept(@NonNull final Throwable throwable) {
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
+ "throwable = [" + throwable.getClass().getName() + "]");
final Throwable actualThrowable;
if (throwable instanceof UndeliverableException) {
// As UndeliverableException is a wrapper,
// get the cause of it to get the "real" exception
actualThrowable = Objects.requireNonNull(throwable.getCause());
} else {
actualThrowable = throwable;
}
final List<Throwable> errors;
if (actualThrowable instanceof CompositeException) {
errors = ((CompositeException) actualThrowable).getExceptions();
} else {
errors = List.of(actualThrowable);
}
for (final Throwable error : errors) {
if (isThrowableIgnored(error)) {
return;
}
if (isThrowableCritical(error)) {
reportException(error);
return;
}
}
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
// When exception is not reported, log it
if (isDisposedRxExceptionsReported()) {
reportException(actualThrowable);
} else {
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
}
}
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
// Don't crash the application over a simple network problem
return ExceptionUtils.hasAssignableCause(throwable,
// network api cancellation
IOException.class, SocketException.class,
// blocking code disposed
InterruptedException.class, InterruptedIOException.class);
}
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
// Though these exceptions cannot be ignored
return ExceptionUtils.hasAssignableCause(throwable,
NullPointerException.class, IllegalArgumentException.class, // bug in app
OnErrorNotImplementedException.class, MissingBackpressureException.class,
IllegalStateException.class); // bug in operator
}
private void reportException(@NonNull final Throwable throwable) {
// Throw uncaught exception that will trigger the report system
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), throwable);
}
});
}
/**
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
*/
protected void initACRA() {
if (ACRA.isACRASenderServiceProcess()) {
return;
}
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
.withBuildConfigClass(BuildConfig.class);
ACRA.init(this, acraConfig);
}
private void initNotificationChannels() {
// Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.notification_channel_name))
.setDescription(getString(R.string.notification_channel_description))
.build(),
new NotificationChannelCompat
.Builder(getString(R.string.app_update_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.app_update_notification_channel_name))
.setDescription(
getString(R.string.app_update_notification_channel_description))
.build(),
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
NotificationManagerCompat.IMPORTANCE_HIGH)
.setName(getString(R.string.hash_channel_name))
.setDescription(getString(R.string.hash_channel_description))
.build(),
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.error_report_channel_name))
.setDescription(getString(R.string.error_report_channel_description))
.build(),
new NotificationChannelCompat
.Builder(getString(R.string.streams_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(getString(R.string.streams_notification_channel_name))
.setDescription(
getString(R.string.streams_notification_channel_description))
.build()
);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
}
protected boolean isDisposedRxExceptionsReported() {
return false;
}
public boolean isFirstRun() {
return isFirstRun;
}
}

View File

@ -1,293 +0,0 @@
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.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.util.DebugLogger
import com.jakewharton.processphoenix.ProcessPhoenix
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 java.io.IOException
import java.io.InterruptedIOException
import java.net.SocketException
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.extractor.services.youtube.extractors.YoutubeStreamExtractor
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 org.schabi.newpipe.util.potoken.PoTokenProviderImpl
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* App.kt is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
open class App :
Application(),
SingletonImageLoader.Factory {
var isFirstRun = false
private set
var notificationsRequested = false
private set
fun setNotificationsRequested() {
notificationsRequested = true
}
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())
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()
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
}
override fun newImageLoader(context: Context): ImageLoader = ImageLoader
.Builder(this)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
.crossfade(true)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
}.build()
protected open fun getDownloader(): Downloader {
val downloader = DownloaderImpl.init(null)
setCookiesToDownloader(downloader)
return downloader
}
protected fun setCookiesToDownloader(downloader: DownloaderImpl) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val key = getString(R.string.recaptcha_cookies_key)
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null))
downloader.updateYoutubeRestrictedModeCookies(this)
}
private fun configureRxJavaErrorHandler() {
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
RxJavaPlugins.setErrorHandler(
object : Consumer<Throwable> {
override fun accept(throwable: Throwable) {
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [${throwable.javaClass.getName()}]")
// As UndeliverableException is a wrapper,
// get the cause of it to get the "real" exception
val actualThrowable = (throwable as? UndeliverableException)?.cause ?: throwable
val errors = (actualThrowable as? CompositeException)?.exceptions ?: listOf(actualThrowable)
for (error in errors) {
if (isThrowableIgnored(error)) {
return
}
if (isThrowableCritical(error)) {
reportException(error)
return
}
}
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
// When exception is not reported, log it
if (isDisposedRxExceptionsReported()) {
reportException(actualThrowable)
} else {
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable)
}
}
fun isThrowableIgnored(throwable: Throwable): Boolean {
// Don't crash the application over a simple network problem
return throwable // network api cancellation
.hasAssignableCause(
IOException::class.java,
SocketException::class.java, // blocking code disposed
InterruptedException::class.java,
InterruptedIOException::class.java
)
}
fun isThrowableCritical(throwable: Throwable): Boolean {
// Though these exceptions cannot be ignored
return throwable
.hasAssignableCause(
// bug in app
NullPointerException::class.java,
IllegalArgumentException::class.java,
OnErrorNotImplementedException::class.java,
MissingBackpressureException::class.java,
// bug in operator
IllegalStateException::class.java
)
}
fun reportException(throwable: Throwable) {
// Throw uncaught exception that will trigger the report system
Thread
.currentThread()
.uncaughtExceptionHandler
.uncaughtException(Thread.currentThread(), throwable)
}
}
)
}
/**
* Called in [.attachBaseContext] after calling the `super` method.
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
*/
protected fun initACRA() {
if (isACRASenderServiceProcess()) {
return
}
val acraConfig =
CoreConfigurationBuilder()
.withBuildConfigClass(BuildConfig::class.java)
init(this, acraConfig)
}
private fun initNotificationChannels() {
// Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels
val mainChannel =
NotificationChannelCompat
.Builder(
getString(R.string.notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW
).setName(getString(R.string.notification_channel_name))
.setDescription(getString(R.string.notification_channel_description))
.build()
val appUpdateChannel =
NotificationChannelCompat
.Builder(
getString(R.string.app_update_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW
).setName(getString(R.string.app_update_notification_channel_name))
.setDescription(getString(R.string.app_update_notification_channel_description))
.build()
val hashChannel =
NotificationChannelCompat
.Builder(
getString(R.string.hash_channel_id),
NotificationManagerCompat.IMPORTANCE_HIGH
).setName(getString(R.string.hash_channel_name))
.setDescription(getString(R.string.hash_channel_description))
.build()
val errorReportChannel =
NotificationChannelCompat
.Builder(
getString(R.string.error_report_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW
).setName(getString(R.string.error_report_channel_name))
.setDescription(getString(R.string.error_report_channel_description))
.build()
val newStreamChannel =
NotificationChannelCompat
.Builder(
getString(R.string.streams_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_DEFAULT
).setName(getString(R.string.streams_notification_channel_name))
.setDescription(getString(R.string.streams_notification_channel_description))
.build()
val channels = listOf(mainChannel, appUpdateChannel, hashChannel, errorReportChannel, newStreamChannel)
NotificationManagerCompat.from(this).createNotificationChannelsCompat(channels)
}
protected open fun isDisposedRxExceptionsReported(): Boolean = false
companion object {
const val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID
private val TAG = App::class.java.toString()
@JvmStatic
lateinit var instance: App
private set
}
}

View File

@ -48,11 +48,6 @@ public final class DownloaderImpl extends Downloader {
this.mCookies = new HashMap<>();
}
@NonNull
public OkHttpClient getClient() {
return client;
}
/**
* It's recommended to call exactly once in the entire lifetime of the application.
*

View File

@ -0,0 +1,50 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import org.schabi.newpipe.util.NavigationHelper;
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* ExitActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ExitActivity extends Activity {
public static void exitAndRemoveFromRecentApps(final Activity activity) {
final Intent intent = new Intent(activity, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
activity.startActivity(intent);
}
@SuppressLint("NewApi")
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finishAndRemoveTask();
NavigationHelper.restartApp(this);
}
}

View File

@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import org.schabi.newpipe.util.NavigationHelper
class ExitActivity : Activity() {
@SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finishAndRemoveTask()
NavigationHelper.restartApp(this)
}
companion object {
@JvmStatic
fun exitAndRemoveFromRecentApps(activity: Activity) {
val intent = Intent(activity, ExitActivity::class.java)
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
or Intent.FLAG_ACTIVITY_CLEAR_TASK
or Intent.FLAG_ACTIVITY_NO_ANIMATION
)
activity.startActivity(intent)
}
}
}

View File

@ -191,7 +191,7 @@ public class MainActivity extends AppCompatActivity {
NotificationWorker.initialize(this);
}
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
&& !App.getInstance().isFirstRun()
&& !App.getApp().isFirstRun()
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
}
@ -203,7 +203,7 @@ public class MainActivity extends AppCompatActivity {
protected void onPostCreate(final Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
final App app = App.getInstance();
final App app = App.getApp();
if (sharedPreferences.getBoolean(app.getString(R.string.update_app_key), false)
&& sharedPreferences
@ -309,21 +309,25 @@ public class MainActivity extends AppCompatActivity {
}
private boolean drawerItemSelected(final MenuItem item) {
final int groupId = item.getGroupId();
if (groupId == R.id.menu_services_group) {
changeService(item);
} else if (groupId == R.id.menu_tabs_group) {
tabSelected(item);
} else if (groupId == R.id.menu_kiosks_group) {
try {
kioskSelected(item);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
}
} else if (groupId == R.id.menu_options_about_group) {
optionsAboutSelected(item);
} else {
return false;
switch (item.getGroupId()) {
case R.id.menu_services_group:
changeService(item);
break;
case R.id.menu_tabs_group:
tabSelected(item);
break;
case R.id.menu_kiosks_group:
try {
kioskSelected(item);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
}
break;
case R.id.menu_options_about_group:
optionsAboutSelected(item);
break;
default:
return false;
}
mainBinding.getRoot().closeDrawers();

View File

@ -82,9 +82,7 @@ class NewVersionWorker(
)
val notificationManager = NotificationManagerCompat.from(applicationContext)
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(2000, notificationBuilder.build())
}
notificationManager.notify(2000, notificationBuilder.build())
}
@Throws(IOException::class, ReCaptchaException::class)

View File

@ -41,50 +41,50 @@ public final class QueueItemMenuUtil {
}
popupMenu.setOnMenuItemClickListener(menuItem -> {
final int itemId = menuItem.getItemId();
if (itemId == R.id.menu_item_remove) {
final int index = playQueue.indexOf(item);
playQueue.remove(index);
return true;
} else if (itemId == R.id.menu_item_details) {
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(context, item.getServiceId(),
item.getUrl(), item.getTitle(), null,
false);
return true;
} else if (itemId == R.id.menu_item_append_playlist) {
PlaylistDialog.createCorrespondingDialog(
context,
List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragmentManager,
"QueueItemMenuUtil@append_playlist"
)
);
switch (menuItem.getItemId()) {
case R.id.menu_item_remove:
final int index = playQueue.indexOf(item);
playQueue.remove(index);
return true;
case R.id.menu_item_details:
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(context, item.getServiceId(),
item.getUrl(), item.getTitle(), null,
false);
return true;
case R.id.menu_item_append_playlist:
PlaylistDialog.createCorrespondingDialog(
context,
List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragmentManager,
"QueueItemMenuUtil@append_playlist"
)
);
return true;
} else if (itemId == R.id.menu_item_channel_details) {
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
item.getUrl(), item.getUploaderUrl(),
// An intent must be used here.
// Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments.
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
context, item.getServiceId(), uploaderUrl, item.getUploader()
));
return true;
} else if (itemId == R.id.menu_item_share) {
shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnails());
return true;
} else if (itemId == R.id.menu_item_download) {
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
info -> {
final DownloadDialog downloadDialog = new DownloadDialog(context,
info);
downloadDialog.show(fragmentManager, "downloadDialog");
});
return true;
return true;
case R.id.menu_item_channel_details:
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
item.getUrl(), item.getUploaderUrl(),
// An intent must be used here.
// Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments.
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
context, item.getServiceId(), uploaderUrl, item.getUploader()
));
return true;
case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnails());
return true;
case R.id.menu_item_download:
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
info -> {
final DownloadDialog downloadDialog = new DownloadDialog(context,
info);
downloadDialog.show(fragmentManager, "downloadDialog");
});
return true;
}
return false;
});

View File

@ -343,7 +343,8 @@ public class RouterActivity extends AppCompatActivity {
return;
}
final var capabilities = currentService.getServiceInfo().getMediaCapabilities();
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
currentService.getServiceInfo().getMediaCapabilities();
// Check if the service supports the choice
if ((isVideoPlayerSelected && capabilities.contains(VIDEO))
@ -527,7 +528,8 @@ public class RouterActivity extends AppCompatActivity {
final List<AdapterChoiceItem> returnedItems = new ArrayList<>();
returnedItems.add(showInfo); // Always present
final var capabilities = service.getServiceInfo().getMediaCapabilities();
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
service.getServiceInfo().getMediaCapabilities();
if (linkType == LinkType.STREAM || linkType == LinkType.PLAYLIST) {
if (capabilities.contains(VIDEO)) {

View File

@ -92,7 +92,7 @@ class AboutActivity : AppCompatActivity() {
return when (position) {
posAbout -> AboutFragment()
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
else -> error("Unknown position for ViewPager2")
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
}
}
@ -105,7 +105,7 @@ class AboutActivity : AppCompatActivity() {
return when (position) {
posAbout -> R.string.tab_about
posLicense -> R.string.tab_licenses
else -> error("Unknown position for ViewPager2")
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
}
}
}
@ -207,10 +207,10 @@ class AboutActivity : AppCompatActivity() {
StandardLicenses.APACHE2
),
SoftwareComponent(
"Coil",
"2023",
"Coil Contributors",
"https://coil-kt.github.io/coil/",
"Picasso",
"2013",
"Square, Inc.",
"https://square.github.io/picasso/",
StandardLicenses.APACHE2
),
SoftwareComponent(

View File

@ -62,7 +62,11 @@ data class PlaylistRemoteEntity(
orderingName = playlistInfo.name,
url = playlistInfo.url,
thumbnailUrl = ImageStrategy.imageListToDbUrl(
playlistInfo.thumbnails.ifEmpty { playlistInfo.uploaderAvatars }
if (playlistInfo.thumbnails.isEmpty()) {
playlistInfo.uploaderAvatars
} else {
playlistInfo.thumbnails
}
),
uploader = playlistInfo.uploaderName,
streamCount = playlistInfo.streamCount

View File

@ -87,7 +87,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
private fun compareAndUpdateStream(newerStream: StreamEntity) {
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
?: error("Stream cannot be null just after insertion.")
?: throw IllegalStateException("Stream cannot be null just after insertion.")
newerStream.uid = existentMinimalStream.uid
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {

View File

@ -100,7 +100,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
entity.uid = uidFromInsert
} else {
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!)
?: error("Subscription cannot be null just after insertion.")
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
entity.uid = subscriptionIdFromDb
update(entity)

View File

@ -16,7 +16,6 @@ import android.os.IBinder;
import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
@ -32,6 +31,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.menu.ActionMenuItemView;
import androidx.appcompat.widget.Toolbar;
import androidx.collection.SparseArrayCompat;
import androidx.documentfile.provider.DocumentFile;
@ -113,7 +113,7 @@ public class DownloadDialog extends DialogFragment
private StoredDirectoryHelper mainStorageAudio = null;
private StoredDirectoryHelper mainStorageVideo = null;
private DownloadManager downloadManager = null;
private MenuItem okButton = null;
private ActionMenuItemView okButton = null;
private Context context = null;
private boolean askForSavePath;
@ -558,13 +558,17 @@ public class DownloadDialog extends DialogFragment
}
boolean flag = true;
if (checkedId == R.id.audio_button) {
setupAudioSpinner();
} else if (checkedId == R.id.video_button) {
setupVideoSpinner();
} else if (checkedId == R.id.subtitle_button) {
setupSubtitleSpinner();
flag = false;
switch (checkedId) {
case R.id.audio_button:
setupAudioSpinner();
break;
case R.id.video_button:
setupVideoSpinner();
break;
case R.id.subtitle_button:
setupSubtitleSpinner();
flag = false;
break;
}
dialogBinding.threads.setEnabled(flag);
@ -581,26 +585,29 @@ public class DownloadDialog extends DialogFragment
+ "position = [" + position + "], id = [" + id + "]");
}
final int parentId = parent.getId();
if (parentId == R.id.quality_spinner) {
final int checkedRadioButtonId = dialogBinding.videoAudioGroup
.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.video_button) {
selectedVideoIndex = position;
onVideoStreamSelected();
} else if (checkedRadioButtonId == R.id.subtitle_button) {
selectedSubtitleIndex = position;
}
onItemSelectedSetFileName();
} else if (parentId == R.id.audio_track_spinner) {
final boolean trackChanged = selectedAudioTrackIndex != position;
selectedAudioTrackIndex = position;
if (trackChanged) {
updateSecondaryStreams();
fetchStreamsSize();
}
} else if (parentId == R.id.audio_stream_spinner) {
selectedAudioIndex = position;
switch (parent.getId()) {
case R.id.quality_spinner:
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.video_button:
selectedVideoIndex = position;
onVideoStreamSelected();
break;
case R.id.subtitle_button:
selectedSubtitleIndex = position;
break;
}
onItemSelectedSetFileName();
break;
case R.id.audio_track_spinner:
final boolean trackChanged = selectedAudioTrackIndex != position;
selectedAudioTrackIndex = position;
if (trackChanged) {
updateSecondaryStreams();
fetchStreamsSize();
}
break;
case R.id.audio_stream_spinner:
selectedAudioIndex = position;
}
}
@ -615,20 +622,23 @@ public class DownloadDialog extends DialogFragment
|| prevFileName.startsWith(getString(R.string.caption_file_name, fileName, ""))) {
// only update the file name field if it was not edited by the user
final int radioButtonId = dialogBinding.videoAudioGroup
.getCheckedRadioButtonId();
if (radioButtonId == R.id.audio_button || radioButtonId == R.id.video_button) {
if (!prevFileName.equals(fileName)) {
// since the user might have switched between audio and video, the correct
// text might already be in place, so avoid resetting the cursor position
dialogBinding.fileName.setText(fileName);
}
} else if (radioButtonId == R.id.subtitle_button) {
final String setSubtitleLanguageCode = subtitleStreamsAdapter
.getItem(selectedSubtitleIndex).getLanguageTag();
// this will reset the cursor position, which is bad UX, but it can't be avoided
dialogBinding.fileName.setText(getString(
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
case R.id.video_button:
if (!prevFileName.equals(fileName)) {
// since the user might have switched between audio and video, the correct
// text might already be in place, so avoid resetting the cursor position
dialogBinding.fileName.setText(fileName);
}
break;
case R.id.subtitle_button:
final String setSubtitleLanguageCode = subtitleStreamsAdapter
.getItem(selectedSubtitleIndex).getLanguageTag();
// this will reset the cursor position, which is bad UX, but it can't be avoided
dialogBinding.fileName.setText(getString(
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
break;
}
}
}
@ -760,44 +770,47 @@ public class DownloadDialog extends DialogFragment
filenameTmp = getNameEditText().concat(".");
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.audio_button) {
selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
} else if (checkedRadioButtonId == R.id.video_button) {
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
} else if (checkedRadioButtonId == R.id.subtitle_button) {
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
if (format != null) {
mimeTmp = format.mimeType;
}
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
break;
case R.id.video_button:
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
break;
case R.id.subtitle_button:
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
if (format != null) {
mimeTmp = format.mimeType;
}
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.getSuffix();
} else if (format != null) {
filenameTmp += format.getSuffix();
}
} else {
throw new RuntimeException("No stream selected");
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.getSuffix();
} else if (format != null) {
filenameTmp += format.getSuffix();
}
break;
default:
throw new RuntimeException("No stream selected");
}
if (!askForSavePath && (mainStorage == null
@ -1044,56 +1057,59 @@ public class DownloadDialog extends DialogFragment
long nearLength = 0;
// more download logic: select muxer, subtitle converter, etc.
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.audio_button) {
kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
}
} else if (checkedRadioButtonId == R.id.video_button) {
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
if (secondary != null) {
secondaryStream = secondary.getStream();
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
} else {
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
}
break;
case R.id.video_button:
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream);
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondary.getSizeInBytes() + videoSize;
if (secondary != null) {
secondaryStream = secondary.getStream();
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
} else {
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
}
final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondary.getSizeInBytes() + videoSize;
}
}
}
} else if (checkedRadioButtonId == R.id.subtitle_button) {
threads = 1; // use unique thread for subtitles due small file size
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
break;
case R.id.subtitle_button:
threads = 1; // use unique thread for subtitles due small file size
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames
};
}
} else {
return;
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[] {
selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames
};
}
break;
default:
return;
}
if (secondaryStream == null) {

View File

@ -0,0 +1,324 @@
package org.schabi.newpipe.error;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.IntentCompat;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityErrorBinding;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.stream.Collectors;
/*
* Created by Christian Schabesberger on 24.10.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ErrorActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* This activity is used to show error details and allow reporting them in various ways. Use {@link
* ErrorUtil#openActivity(Context, ErrorInfo)} to correctly open this activity.
*/
public class ErrorActivity extends AppCompatActivity {
// LOG TAGS
public static final String TAG = ErrorActivity.class.toString();
// BUNDLE TAGS
public static final String ERROR_INFO = "error_info";
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
public static final String ERROR_GITHUB_ISSUE_URL =
"https://github.com/TeamNewPipe/NewPipe/issues";
private ErrorInfo errorInfo;
private String currentTimeStamp;
private ActivityErrorBinding activityErrorBinding;
////////////////////////////////////////////////////////////////////////
// Activity lifecycle
////////////////////////////////////////////////////////////////////////
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.setDayNightMode(this);
ThemeHelper.setTheme(this);
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
setContentView(activityErrorBinding.getRoot());
final Intent intent = getIntent();
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.error_report_title);
actionBar.setDisplayShowTitleEnabled(true);
}
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
// important add guru meditation
addGuruMeditation();
// print current time, as zoned ISO8601 timestamp
final ZonedDateTime now = ZonedDateTime.now();
currentTimeStamp = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "EMAIL"));
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
ShareUtils.copyToClipboard(this, buildMarkdown()));
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "GITHUB"));
// normal bugreport
buildInfo(errorInfo);
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage(this));
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
// print stack trace once again for debugging:
for (final String e : errorInfo.getStackTraces()) {
Log.e(TAG, e);
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.error_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.menu_item_share_error:
ShareUtils.shareText(getApplicationContext(),
getString(R.string.error_report_title), buildJson());
return true;
default:
return false;
}
}
private void openPrivacyPolicyDialog(final Context context, final String action) {
new AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.privacy_policy_title)
.setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
ShareUtils.openUrlInApp(context,
context.getString(R.string.privacy_policy_url)))
.setPositiveButton(R.string.accept, (dialog, which) -> {
if (action.equals("EMAIL")) { // send on email
final Intent i = new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse("mailto:")) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS})
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
+ getString(R.string.app_name) + " "
+ BuildConfig.VERSION_NAME)
.putExtra(Intent.EXTRA_TEXT, buildJson());
ShareUtils.openIntentInApp(context, i);
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL);
}
})
.setNegativeButton(R.string.decline, null)
.show();
}
private String formErrorText(final String[] el) {
final String separator = "-------------------------------------";
return Arrays.stream(el)
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
}
private void buildInfo(final ErrorInfo info) {
String text = "";
activityErrorBinding.errorInfoLabelsView.setText(getString(R.string.info_labels)
.replace("\\n", "\n"));
text += getUserActionString(info.getUserAction()) + "\n"
+ info.getRequest() + "\n"
+ getContentLanguageString() + "\n"
+ getContentCountryString() + "\n"
+ getAppLanguage() + "\n"
+ info.getServiceName() + "\n"
+ currentTimeStamp + "\n"
+ getPackageName() + "\n"
+ BuildConfig.VERSION_NAME + "\n"
+ getOsString();
activityErrorBinding.errorInfosView.setText(text);
}
private String buildJson() {
try {
return JsonWriter.string()
.object()
.value("user_action", getUserActionString(errorInfo.getUserAction()))
.value("request", errorInfo.getRequest())
.value("content_language", getContentLanguageString())
.value("content_country", getContentCountryString())
.value("app_language", getAppLanguage())
.value("service", errorInfo.getServiceName())
.value("package", getPackageName())
.value("version", BuildConfig.VERSION_NAME)
.value("os", getOsString())
.value("time", currentTimeStamp)
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
.toString())
.end()
.done();
} catch (final Throwable e) {
Log.e(TAG, "Error while erroring: Could not build json");
e.printStackTrace();
}
return "";
}
private String buildMarkdown() {
try {
final StringBuilder htmlErrorReport = new StringBuilder();
final String userComment = activityErrorBinding.errorCommentBox.getText().toString();
if (!userComment.isEmpty()) {
htmlErrorReport.append(userComment).append("\n");
}
// basic error info
htmlErrorReport
.append("## Exception")
.append("\n* __User Action:__ ")
.append(getUserActionString(errorInfo.getUserAction()))
.append("\n* __Request:__ ").append(errorInfo.getRequest())
.append("\n* __Content Country:__ ").append(getContentCountryString())
.append("\n* __Content Language:__ ").append(getContentLanguageString())
.append("\n* __App Language:__ ").append(getAppLanguage())
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
.append("\n* __Timestamp:__ ").append(currentTimeStamp)
.append("\n* __Package:__ ").append(getPackageName())
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
.append("\n* __OS:__ ").append(getOsString()).append("\n");
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport
.append("<details><summary><b>Exceptions (")
.append(errorInfo.getStackTraces().length)
.append(")</b></summary><p>\n");
}
// add the logs
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
htmlErrorReport.append("<details><summary><b>Crash log ");
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append(i + 1);
}
htmlErrorReport.append("</b>")
.append("</summary><p>\n")
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
.append("</details>\n");
}
// make sure to close everything
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append("</p></details>\n");
}
htmlErrorReport.append("<hr>\n");
return htmlErrorReport.toString();
} catch (final Throwable e) {
Log.e(TAG, "Error while erroring: Could not build markdown");
e.printStackTrace();
return "";
}
}
private String getUserActionString(final UserAction userAction) {
if (userAction == null) {
return "Your description is in another castle.";
} else {
return userAction.getMessage();
}
}
private String getContentCountryString() {
return Localization.getPreferredContentCountry(this).getCountryCode();
}
private String getContentLanguageString() {
return Localization.getPreferredLocalization(this).getLocalizationCode();
}
private String getAppLanguage() {
return Localization.getAppLocale().toString();
}
private String getOsString() {
final String osBase = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
? Build.VERSION.BASE_OS : "Android";
return System.getProperty("os.name")
+ " " + (osBase.isEmpty() ? "Android" : osBase)
+ " " + Build.VERSION.RELEASE
+ " - " + Build.VERSION.SDK_INT;
}
private void addGuruMeditation() {
//just an easter egg
String text = activityErrorBinding.errorSorryView.getText().toString();
text += "\n" + getString(R.string.guru_meditation);
activityErrorBinding.errorSorryView.setText(text);
}
}

View File

@ -1,281 +0,0 @@
/*
* SPDX-FileCopyrightText: 2015-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.error
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.IntentCompat
import androidx.core.net.toUri
import com.grack.nanojson.JsonWriter
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityErrorBinding
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
/**
* This activity is used to show error details and allow reporting them in various ways.
* Use [ErrorUtil.openActivity] to correctly open this activity.
*/
class ErrorActivity : AppCompatActivity() {
private lateinit var errorInfo: ErrorInfo
private lateinit var currentTimeStamp: String
private lateinit var binding: ActivityErrorBinding
private val contentCountryString: String
get() = Localization.getPreferredContentCountry(this).countryCode
private val contentLanguageString: String
get() = Localization.getPreferredLocalization(this).localizationCode
private val appLanguage: String
get() = Localization.getAppLocale().toString()
private val osString: String
get() {
val name = System.getProperty("os.name")!!
val osBase = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Build.VERSION.BASE_OS.ifEmpty { "Android" }
} else {
"Android"
}
return "$name $osBase ${Build.VERSION.RELEASE} - ${Build.VERSION.SDK_INT}"
}
private val errorEmailSubject: String
get() = "$ERROR_EMAIL_SUBJECT ${getString(R.string.app_name)} ${BuildConfig.VERSION_NAME}"
// /////////////////////////////////////////////////////////////////////
// Activity lifecycle
// /////////////////////////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeHelper.setDayNightMode(this)
ThemeHelper.setTheme(this)
binding = ActivityErrorBinding.inflate(layoutInflater)
setContentView(binding.getRoot())
setSupportActionBar(binding.toolbarLayout.toolbar)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setTitle(R.string.error_report_title)
setDisplayShowTitleEnabled(true)
}
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo::class.java)!!
// important add guru meditation
addGuruMeditation()
// print current time, as zoned ISO8601 timestamp
currentTimeStamp = ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
binding.errorReportEmailButton.setOnClickListener { _ ->
openPrivacyPolicyDialog(this, "EMAIL")
}
binding.errorReportCopyButton.setOnClickListener { _ ->
ShareUtils.copyToClipboard(this, buildMarkdown())
}
binding.errorReportGitHubButton.setOnClickListener { _ ->
openPrivacyPolicyDialog(this, "GITHUB")
}
// normal bugreport
buildInfo(errorInfo)
binding.errorMessageView.text = errorInfo.getMessage(this)
binding.errorView.text = formErrorText(errorInfo.stackTraces)
// print stack trace once again for debugging:
errorInfo.stackTraces.forEach { Log.e(TAG, it) }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.error_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
R.id.menu_item_share_error -> {
ShareUtils.shareText(
applicationContext,
getString(R.string.error_report_title),
buildJson()
)
true
}
else -> false
}
}
private fun openPrivacyPolicyDialog(context: Context, action: String) {
AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.privacy_policy_title)
.setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy) { _, _ ->
ShareUtils.openUrlInApp(context, context.getString(R.string.privacy_policy_url))
}
.setPositiveButton(R.string.accept) { _, _ ->
if (action == "EMAIL") { // send on email
val intent = Intent(Intent.ACTION_SENDTO)
.setData("mailto:".toUri()) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, arrayOf(ERROR_EMAIL_ADDRESS))
.putExtra(Intent.EXTRA_SUBJECT, errorEmailSubject)
.putExtra(Intent.EXTRA_TEXT, buildJson())
ShareUtils.openIntentInApp(context, intent)
} else if (action == "GITHUB") { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL)
}
}
.setNegativeButton(R.string.decline, null)
.show()
}
private fun formErrorText(stacktrace: Array<String>): String {
val separator = "-------------------------------------"
return stacktrace.joinToString(separator + "\n", separator + "\n", separator)
}
private fun buildInfo(info: ErrorInfo) {
binding.errorInfoLabelsView.text = getString(R.string.info_labels)
val text = info.userAction.message + "\n" +
info.request + "\n" +
contentLanguageString + "\n" +
contentCountryString + "\n" +
appLanguage + "\n" +
info.getServiceName() + "\n" +
currentTimeStamp + "\n" +
packageName + "\n" +
BuildConfig.VERSION_NAME + "\n" +
osString
binding.errorInfosView.text = text
}
private fun buildJson(): String {
try {
return JsonWriter.string()
.`object`()
.value("user_action", errorInfo.userAction.message)
.value("request", errorInfo.request)
.value("content_language", contentLanguageString)
.value("content_country", contentCountryString)
.value("app_language", appLanguage)
.value("service", errorInfo.getServiceName())
.value("package", packageName)
.value("version", BuildConfig.VERSION_NAME)
.value("os", osString)
.value("time", currentTimeStamp)
.array("exceptions", errorInfo.stackTraces.toList())
.value("user_comment", binding.errorCommentBox.getText().toString())
.end()
.done()
} catch (exception: Exception) {
Log.e(TAG, "Error while erroring: Could not build json", exception)
}
return ""
}
private fun buildMarkdown(): String {
try {
return buildString(1024) {
val userComment = binding.errorCommentBox.text.toString()
if (userComment.isNotEmpty()) {
appendLine(userComment)
}
// basic error info
appendLine("## Exception")
appendLine("* __User Action:__ ${errorInfo.userAction.message}")
appendLine("* __Request:__ ${errorInfo.request}")
appendLine("* __Content Country:__ $contentCountryString")
appendLine("* __Content Language:__ $contentLanguageString")
appendLine("* __App Language:__ $appLanguage")
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
appendLine("* __Timestamp:__ $currentTimeStamp")
appendLine("* __Package:__ $packageName")
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
appendLine("* __Version:__ ${BuildConfig.VERSION_NAME}")
appendLine("* __OS:__ $osString")
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorInfo.stackTraces.size > 1) {
append("<details><summary><b>Exceptions (")
append(errorInfo.stackTraces.size)
append(")</b></summary><p>\n")
}
// add the logs
errorInfo.stackTraces.forEachIndexed { index, stacktrace ->
append("<details><summary><b>Crash log ")
if (errorInfo.stackTraces.size > 1) {
append(index + 1)
}
append("</b>")
append("</summary><p>\n")
append("\n```\n${stacktrace}\n```\n")
append("</details>\n")
}
// make sure to close everything
if (errorInfo.stackTraces.size > 1) {
append("</p></details>\n")
}
append("<hr>\n")
}
} catch (exception: Exception) {
Log.e(TAG, "Error while erroring: Could not build markdown", exception)
return ""
}
}
private fun addGuruMeditation() {
// just an easter egg
var text = binding.errorSorryView.text.toString()
text += "\n" + getString(R.string.guru_meditation)
binding.errorSorryView.text = text
}
companion object {
// LOG TAGS
private val TAG = ErrorActivity::class.java.toString()
// BUNDLE TAGS
const val ERROR_INFO = "error_info"
private const val ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"
private const val ERROR_EMAIL_SUBJECT = "Exception in "
private const val ERROR_GITHUB_ISSUE_URL = "https://github.com/TeamNewPipe/NewPipe/issues"
}
}

View File

@ -299,9 +299,9 @@ class ErrorInfo private constructor(
// indicates that it's important and is thus reportable
null -> true
// if the service explicitly said that content is not available (e.g. age
// restrictions, video deleted, etc.), there is no use in letting users report it
is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable)
// the service explicitly said that content is not available (e.g. age restrictions,
// video deleted, etc.), there is no use in letting users report it
is ContentNotAvailableException -> false
// we know the content is not supported, no need to let the user report it
is ContentNotSupportedException -> false
@ -318,8 +318,8 @@ class ErrorInfo private constructor(
fun isRetryable(throwable: Throwable?): Boolean {
return when (throwable) {
// if we know the content is surely not available, retrying won't help
is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable)
// we know the content is not available, retrying won't help
is ContentNotAvailableException -> false
// we know the content is not supported, retrying won't help
is ContentNotSupportedException -> false
@ -329,28 +329,5 @@ class ErrorInfo private constructor(
else -> true
}
}
/**
* Unfortunately sometimes [ContentNotAvailableException] may not indicate that the content
* is blocked/deleted/paid, but may just indicate that we could not extract it. This is an
* inconsistency in the exceptions thrown by the extractor, but until it is fixed, this
* function will distinguish between the two types.
* @return `true` if the content is not available because of a limitation imposed by the
* service or the owner, `false` if the extractor could not extract info about it
*/
fun isContentSurelyNotAvailable(e: ContentNotAvailableException): Boolean {
return when (e) {
is AccountTerminatedException,
is AgeRestrictedContentException,
is GeographicRestrictionException,
is PaidContentException,
is PrivateContentException,
is SoundCloudGoPlusContentException,
is UnsupportedContentInCountryException,
is YoutubeMusicPremiumContentException -> true
else -> false
}
}
}
}

View File

@ -134,11 +134,8 @@ class ErrorUtil {
)
)
val notificationManager = NotificationManagerCompat.from(context)
if (notificationManager.areNotificationsEnabled()) {
notificationManager
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
}
NotificationManagerCompat.from(context)
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
ContextCompat.getMainExecutor(context).execute {
// since the notification is silent, also show a toast, otherwise the user is confused

View File

@ -126,7 +126,6 @@ public class ReCaptchaActivity extends AppCompatActivity {
}
@Override
@SuppressLint("MissingSuperCall") // saveCookiesAndFinish method handles back navigation
public void onBackPressed() {
saveCookiesAndFinish();
}

View File

@ -118,7 +118,7 @@ import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.ArrayList;
import java.util.Iterator;
@ -129,7 +129,6 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
@ -161,6 +160,8 @@ public final class VideoDetailFragment
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
// tabs
private boolean showComments;
private boolean showRelatedItems;
@ -645,12 +646,6 @@ public final class VideoDetailFragment
protected void initListeners() {
super.initListeners();
// Workaround for #5600
// Forcefully catch click events uncaught by children because otherwise
// they will be caught by underlying view and "click through" will happen
binding.getRoot().setOnClickListener(v -> { });
binding.getRoot().setOnLongClickListener(v -> true);
setOnClickListeners();
setOnLongClickListeners();
@ -1427,10 +1422,8 @@ public final class VideoDetailFragment
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
// Rebound to the service if it was closed via notification or mini player
if (!playerHolder.isBound()) {
playerHolder.startService(
false, VideoDetailFragment.this);
}
playerHolder.setListener(VideoDetailFragment.this);
playerHolder.tryBindIfNeeded(context);
break;
}
}
@ -1498,10 +1491,7 @@ public final class VideoDetailFragment
}
}
CoilUtils.dispose(binding.detailThumbnailImageView);
CoilUtils.dispose(binding.detailSubChannelThumbnailView);
CoilUtils.dispose(binding.overlayThumbnail);
CoilUtils.dispose(binding.detailUploaderThumbnailView);
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
binding.detailThumbnailImageView.setImageBitmap(null);
binding.detailSubChannelThumbnailView.setImageBitmap(null);
}
@ -1592,8 +1582,8 @@ public final class VideoDetailFragment
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
checkUpdateProgressInfo(info);
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.detailThumbnailImageView,
info.getThumbnails());
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView);
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables);
@ -1643,8 +1633,8 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE);
}
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
info.getUploaderAvatars());
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
}
@ -1675,11 +1665,11 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE);
}
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
info.getSubChannelAvatars());
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
CoilHelper.INSTANCE.loadAvatar(binding.detailUploaderThumbnailView,
info.getUploaderAvatars());
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailUploaderThumbnailView);
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
}
@ -1907,11 +1897,7 @@ public final class VideoDetailFragment
}
if (binding.relatedItemsLayout != null) {
if (showRelatedItems) {
binding.relatedItemsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
} else {
binding.relatedItemsLayout.setVisibility(View.GONE);
}
binding.relatedItemsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
}
scrollToTop();
@ -2441,7 +2427,8 @@ public final class VideoDetailFragment
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageDrawable(null);
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails);
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail);
}
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {

View File

@ -53,14 +53,13 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -74,6 +73,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
implements StateSaver.WriteRead {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@ -160,29 +160,34 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.menu_item_notify) {
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
} else if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(requireContext());
} else if (itemId == R.id.menu_item_rss) {
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
} else if (itemId == R.id.menu_item_openInBrowser) {
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(),
currentInfo.getOriginalUrl());
}
} else if (itemId == R.id.menu_item_share) {
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name,
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
}
} else {
return false;
switch (item.getItemId()) {
case R.id.menu_item_notify:
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
break;
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_rss:
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
break;
case R.id.menu_item_openInBrowser:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(),
currentInfo.getOriginalUrl());
}
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name,
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
}
break;
default:
return false;
}
return true;
}
@ -578,9 +583,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override
public void showLoading() {
super.showLoading();
CoilUtils.dispose(binding.channelAvatarView);
CoilUtils.dispose(binding.channelBannerImage);
CoilUtils.dispose(binding.subChannelAvatarView);
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
animate(binding.channelSubscribeButton, false, 100);
}
@ -591,15 +594,17 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
CoilHelper.INSTANCE.loadBanner(binding.channelBannerImage, result.getBanners());
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelBannerImage);
} else {
// do not waste space for the banner, if the user disabled images or there is not one
binding.channelBannerImage.setImageDrawable(null);
}
CoilHelper.INSTANCE.loadAvatar(binding.channelAvatarView, result.getAvatars());
CoilHelper.INSTANCE.loadAvatar(binding.subChannelAvatarView,
result.getParentChannelAvatars());
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelAvatarView);
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.subChannelAvatarView);
binding.channelTitleView.setText(result.getName());
binding.channelSubscriberView.setVisibility(View.VISIBLE);

View File

@ -25,8 +25,8 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextLinkifier;
import org.schabi.newpipe.util.text.LongPressLinkMovementMethod;
@ -84,7 +84,7 @@ public final class CommentRepliesFragment
final CommentsInfoItem item = commentsInfoItem;
// load the author avatar
CoilHelper.INSTANCE.loadAvatar(binding.authorAvatar, item.getUploaderAvatars());
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
? View.VISIBLE : View.GONE);

View File

@ -53,7 +53,7 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
import java.util.ArrayList;
@ -62,7 +62,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
@ -72,6 +71,8 @@ import io.reactivex.rxjava3.disposables.Disposable;
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
implements PlaylistControlViewHolder {
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
private CompositeDisposable disposables;
private Subscription bookmarkReactor;
private AtomicBoolean isBookmarkButtonReady;
@ -231,30 +232,35 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(requireContext());
} else if (itemId == R.id.menu_item_openInBrowser) {
ShareUtils.openUrlInBrowser(requireContext(), url);
} else if (itemId == R.id.menu_item_share) {
ShareUtils.shareText(requireContext(), name, url,
currentInfo == null ? List.of() : currentInfo.getThumbnails());
} else if (itemId == R.id.menu_item_bookmark) {
onBookmarkClicked();
} else if (itemId == R.id.menu_item_append_playlist) {
if (currentInfo != null) {
disposables.add(PlaylistDialog.createCorrespondingDialog(
getContext(),
getPlayQueue()
.getStreams()
.stream()
.map(StreamEntity::new)
.collect(Collectors.toList()),
dialog -> dialog.show(getFM(), TAG)
));
}
} else {
return super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_openInBrowser:
ShareUtils.openUrlInBrowser(requireContext(), url);
break;
case R.id.menu_item_share:
ShareUtils.shareText(requireContext(), name, url,
currentInfo == null ? List.of() : currentInfo.getThumbnails());
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();
break;
case R.id.menu_item_append_playlist:
if (currentInfo != null) {
disposables.add(PlaylistDialog.createCorrespondingDialog(
getContext(),
getPlayQueue()
.getStreams()
.stream()
.map(StreamEntity::new)
.collect(Collectors.toList()),
dialog -> dialog.show(getFM(), TAG)
));
}
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
@ -270,7 +276,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
animate(headerBinding.getRoot(), false, 200);
animateHideRecyclerViewAllowingScrolling(itemsList);
CoilUtils.dispose(headerBinding.uploaderAvatarView);
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
animate(headerBinding.uploaderLayout, false, 200);
}
@ -321,8 +327,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
R.drawable.ic_radio)
);
} else {
CoilHelper.INSTANCE.loadAvatar(headerBinding.uploaderAvatarView,
result.getUploaderAvatars());
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
.into(headerBinding.uploaderAvatarView);
}
streamCount = result.getStreamCount();

View File

@ -1009,11 +1009,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
}
suggestionListAdapter.submitList(suggestions,
() -> {
if (searchBinding != null) {
searchBinding.suggestionsList.scrollToPosition(0);
}
});
() -> searchBinding.suggestionsList.scrollToPosition(0));
if (suggestionsPanelVisible && isErrorPanelVisible()) {
hideLoading();

View File

@ -0,0 +1,131 @@
package org.schabi.newpipe.info_list;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.OnClickGesture;
/*
* Created by Christian Schabesberger on 26.09.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoItemBuilder.java is part of NewPipe.
* </p>
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
* </p>
*/
public class InfoItemBuilder {
private final Context context;
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
public InfoItemBuilder(final Context context) {
this.context = context;
}
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
return buildView(parent, infoItem, historyRecordManager, false);
}
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager,
final boolean useMiniVariant) {
final InfoItemHolder holder =
holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
holder.updateFromItem(infoItem, historyRecordManager);
return holder.itemView;
}
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
@NonNull final InfoItem.InfoType infoType,
final boolean useMiniVariant) {
switch (infoType) {
case STREAM:
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
: new StreamInfoItemHolder(this, parent);
case CHANNEL:
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
: new ChannelInfoItemHolder(this, parent);
case PLAYLIST:
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
: new PlaylistInfoItemHolder(this, parent);
case COMMENT:
return new CommentInfoItemHolder(this, parent);
default:
throw new RuntimeException("InfoType not expected = " + infoType.name());
}
}
public Context getContext() {
return context;
}
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
return onStreamSelectedListener;
}
public void setOnStreamSelectedListener(final OnClickGesture<StreamInfoItem> listener) {
this.onStreamSelectedListener = listener;
}
public OnClickGesture<ChannelInfoItem> getOnChannelSelectedListener() {
return onChannelSelectedListener;
}
public void setOnChannelSelectedListener(final OnClickGesture<ChannelInfoItem> listener) {
this.onChannelSelectedListener = listener;
}
public OnClickGesture<PlaylistInfoItem> getOnPlaylistSelectedListener() {
return onPlaylistSelectedListener;
}
public void setOnPlaylistSelectedListener(final OnClickGesture<PlaylistInfoItem> listener) {
this.onPlaylistSelectedListener = listener;
}
public OnClickGesture<CommentsInfoItem> getOnCommentsSelectedListener() {
return onCommentsSelectedListener;
}
public void setOnCommentsSelectedListener(
final OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
this.onCommentsSelectedListener = onCommentsSelectedListener;
}
}

View File

@ -1,20 +0,0 @@
/*
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.info_list
import android.content.Context
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.OnClickGesture
class InfoItemBuilder(val context: Context) {
var onStreamSelectedListener: OnClickGesture<StreamInfoItem>? = null
var onChannelSelectedListener: OnClickGesture<ChannelInfoItem>? = null
var onPlaylistSelectedListener: OnClickGesture<PlaylistInfoItem>? = null
var onCommentsSelectedListener: OnClickGesture<CommentsInfoItem>? = null
}

View File

@ -1,18 +1,19 @@
package org.schabi.newpipe.info_list
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
import android.widget.ImageView
import android.widget.TextView
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ItemStreamSegmentBinding
import org.schabi.newpipe.extractor.stream.StreamSegment
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.image.CoilHelper
import org.schabi.newpipe.util.image.PicassoHelper
class StreamSegmentItem(
private val item: StreamSegment,
private val onClick: StreamSegmentAdapter.StreamSegmentListener
) : BindableItem<ItemStreamSegmentBinding>() {
) : Item<GroupieViewHolder>() {
companion object {
const val PAYLOAD_SELECT = 1
@ -20,35 +21,34 @@ class StreamSegmentItem(
var isSelected = false
override fun bind(viewBinding: ItemStreamSegmentBinding, position: Int) {
CoilHelper.loadThumbnail(viewBinding.previewImage, item.previewUrl)
viewBinding.textViewTitle.text = item.title
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
item.previewUrl?.let {
PicassoHelper.loadThumbnail(it)
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
}
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
if (item.channelName == null) {
viewBinding.textViewChannel.visibility = View.GONE
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.GONE
// When the channel name is displayed there is less space
// and thus the segment title needs to be only one line height.
// But when there is no channel name displayed, the title can be two lines long.
// The default maxLines value is set to 1 to display all elements in the AS preview,
viewBinding.textViewTitle.maxLines = 2
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).maxLines = 2
} else {
viewBinding.textViewChannel.text = item.channelName
viewBinding.textViewChannel.visibility = View.VISIBLE
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).text = item.channelName
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.VISIBLE
}
viewBinding.textViewStartSeconds.text =
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
Localization.getDurationString(item.startTimeSeconds.toLong())
viewBinding.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewBinding.root.setOnLongClickListener {
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewHolder.root.setOnLongClickListener {
onClick.onItemLongClick(this, item.startTimeSeconds)
true
}
viewBinding.root.isSelected = isSelected
viewHolder.root.isSelected = isSelected
}
override fun bind(
viewHolder: GroupieViewHolder<ItemStreamSegmentBinding>,
position: Int,
payloads: MutableList<Any>
) {
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(PAYLOAD_SELECT)) {
viewHolder.root.isSelected = isSelected
return
@ -57,6 +57,4 @@ class StreamSegmentItem(
}
override fun getLayout() = R.layout.item_stream_segment
override fun initializeViewBinding(view: View) = ItemStreamSegmentBinding.bind(view)
}

View File

@ -346,7 +346,7 @@ public final class InfoItemDialog {
public static void reportErrorDuringInitialization(final Throwable throwable,
final InfoItem item) {
ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo(
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
throwable,
UserAction.OPEN_INFO_ITEM_DIALOG,
"none",

View File

@ -13,8 +13,8 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
private final ImageView itemThumbnailView;
@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemAdditionalDetailView.setText(getDetailLine(item));
}
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getThumbnails());
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) {

View File

@ -27,8 +27,8 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
public class CommentInfoItemHolder extends InfoItemHolder {
@ -82,12 +82,14 @@ public class CommentInfoItemHolder extends InfoItemHolder {
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem item)) {
if (!(infoItem instanceof CommentsInfoItem)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
// load the author avatar
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getUploaderAvatars());
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
if (ImageStrategy.shouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,

View File

@ -9,8 +9,8 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setText(item.getUploaderName());
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnails());
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) {

View File

@ -16,8 +16,8 @@ import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.util.concurrent.TimeUnit;
@ -87,7 +87,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView, item.getThumbnails());
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {

View File

@ -1,13 +0,0 @@
package org.schabi.newpipe.ktx
import android.graphics.Bitmap
import android.graphics.Rect
import androidx.core.graphics.BitmapCompat
@Suppress("NOTHING_TO_INLINE")
inline fun Bitmap.scale(
width: Int,
height: Int,
srcRect: Rect? = null,
scaleInLinearSpace: Boolean = true
) = BitmapCompat.createScaledBitmap(this, width, height, srcRect, scaleInLinearSpace)

View File

@ -255,7 +255,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
viewModel.getShowFutureItemsFromPreferences()
)
AlertDialog.Builder(requireContext())
AlertDialog.Builder(context!!)
.setTitle(R.string.feed_hide_streams_title)
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
checkedDialogItems[which] = isChecked

View File

@ -129,7 +129,8 @@ class FeedViewModel(
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
this.showPlayedItems.onNext(showPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
this.putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
this.apply()
}
}
@ -138,7 +139,8 @@ class FeedViewModel(
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
this.putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
this.apply()
}
}
@ -147,7 +149,8 @@ class FeedViewModel(
fun setSaveShowFutureItems(showFutureItems: Boolean) {
this.showFutureItems.onNext(showFutureItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
this.apply()
}
}
@ -166,7 +169,7 @@ class FeedViewModel(
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
initializer {
FeedViewModel(
App.instance,
App.getApp(),
groupId,
// Read initial value from preferences
getShowPlayedItemsFromPreferences(context.applicationContext),

View File

@ -21,7 +21,7 @@ import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.StreamTypeUtil
import org.schabi.newpipe.util.image.CoilHelper
import org.schabi.newpipe.util.image.PicassoHelper
data class StreamItem(
val streamWithState: StreamWithState,
@ -101,7 +101,7 @@ data class StreamItem(
viewBinding.itemProgressView.visibility = View.GONE
}
CoilHelper.loadThumbnail(viewBinding.itemThumbnailView, stream.thumbnailUrl)
PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
if (itemVersion != ItemVersion.MINI) {
viewBinding.itemAdditionalDetails.text =

View File

@ -6,6 +6,7 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.provider.Settings
@ -16,17 +17,20 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.image.CoilHelper
import org.schabi.newpipe.util.image.PicassoHelper
/**
* Helper for everything related to show notifications about new streams to the user.
*/
class NotificationHelper(val context: Context) {
private val manager = NotificationManagerCompat.from(context)
private val iconLoadingTargets = ArrayList<Target>()
/**
* Show notifications for new streams from a single channel. The individual notifications are
@ -67,42 +71,69 @@ class NotificationHelper(val context: Context) {
summaryBuilder.setStyle(style)
// open the channel page when clicking on the summary notification
val intent = NavigationHelper
.getChannelIntent(context, data.serviceId, data.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
summaryBuilder.setContentIntent(
PendingIntentCompat.getActivity(context, data.pseudoId, intent, 0, false)
PendingIntentCompat.getActivity(
context,
data.pseudoId,
NavigationHelper
.getChannelIntent(context, data.serviceId, data.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
0,
false
)
)
val avatarIcon =
CoilHelper.loadBitmapBlocking(context, data.avatarUrl, R.drawable.ic_newpipe_triangle_white)
summaryBuilder.setLargeIcon(avatarIcon)
// a Target is like a listener for image loading events
val target = object : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
// set channel icon only if there is actually one (for Android versions < 7.0)
summaryBuilder.setLargeIcon(bitmap)
// Show individual stream notifications, set channel icon only if there is actually one
showStreamNotifications(newStreams, data.serviceId, avatarIcon)
// Show summary notification
if (manager.areNotificationsEnabled()) {
manager.notify(data.pseudoId, summaryBuilder.build())
// Show individual stream notifications, set channel icon only if there is actually
// one
showStreamNotifications(newStreams, data.serviceId, data.url, bitmap)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
// Show individual stream notifications
showStreamNotifications(newStreams, data.serviceId, data.url, null)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
// Nothing to do
}
}
// add the target to the list to hold a strong reference and prevent it from being garbage
// collected, since Picasso only holds weak references to targets
iconLoadingTargets.add(target)
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
}
private fun showStreamNotifications(
newStreams: List<StreamInfoItem>,
serviceId: Int,
channelUrl: String,
channelIcon: Bitmap?
) {
if (manager.areNotificationsEnabled()) {
newStreams.forEach { stream ->
val notification =
createStreamNotification(stream, serviceId, channelIcon)
manager.notify(stream.url.hashCode(), notification)
}
for (stream in newStreams) {
val notification = createStreamNotification(stream, serviceId, channelUrl, channelIcon)
manager.notify(stream.url.hashCode(), notification)
}
}
private fun createStreamNotification(
item: StreamInfoItem,
serviceId: Int,
channelUrl: String,
channelIcon: Bitmap?
): Notification {
return NotificationCompat.Builder(
@ -113,7 +144,7 @@ class NotificationHelper(val context: Context) {
.setLargeIcon(channelIcon)
.setContentTitle(item.name)
.setContentText(item.uploaderName)
.setGroup(item.uploaderUrl)
.setGroup(channelUrl)
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
.setColorized(true)
.setAutoCancel(true)

View File

@ -111,8 +111,7 @@ class FeedLoadManager(private val context: Context) {
broadcastProgress()
}
.observeOn(Schedulers.io())
// Randomize user subscription ordering to attempt to resist fingerprinting
.flatMap { Flowable.fromIterable(it.shuffled()) }
.flatMap { Flowable.fromIterable(it) }
.takeWhile { !cancelSignal.get() }
.doOnNext { subscriptionEntity ->
// throttle YouTube extractions once every BATCH_SIZE to avoid being rate limited

View File

@ -185,9 +185,7 @@ class FeedLoadService : Service() {
}
}
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
}
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
}
// /////////////////////////////////////////////////////////////////////////

View File

@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
import java.time.format.DateTimeFormatter;
@ -30,16 +30,17 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistMetadataEntry item)) {
if (!(localItem instanceof PlaylistMetadataEntry)) {
return;
}
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
itemTitleView.setText(item.getOrderingName());
itemStreamCountView.setText(Localization.localizeStreamCountMini(
itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setVisibility(View.INVISIBLE);
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
if (item instanceof PlaylistDuplicatesEntry
&& ((PlaylistDuplicatesEntry) item).getTimesStreamIsContained() > 0) {

View File

@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter;
@ -83,8 +83,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
item.getStreamEntity().getThumbnailUrl());
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
.into(itemThumbnailView);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {

View File

@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter;
@ -117,8 +117,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
item.getStreamEntity().getThumbnailUrl());
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
.into(itemThumbnailView);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {

View File

@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.time.format.DateTimeFormatter;
@ -29,9 +29,10 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistRemoteEntity item)) {
if (!(localItem instanceof PlaylistRemoteEntity)) {
return;
}
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
itemTitleView.setText(item.getOrderingName());
itemStreamCountView.setText(Localization.localizeStreamCountMini(
@ -44,7 +45,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
}
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
}

View File

@ -327,7 +327,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
groupIcon = feedGroupEntity?.icon
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
val feedGroupIcon = selectedIcon ?: icon
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
@ -506,7 +506,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun hideKeyboardSearch() {
inputMethodManager.hideSoftInputFromWindow(
searchLayoutBinding.toolbarSearchEditText.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS
InputMethodManager.RESULT_UNCHANGED_SHOWN
)
searchLayoutBinding.toolbarSearchEditText.clearFocus()
}
@ -523,7 +523,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun hideKeyboard() {
inputMethodManager.hideSoftInputFromWindow(
feedGroupCreateBinding.groupNameInput.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS
InputMethodManager.RESULT_UNCHANGED_SHOWN
)
feedGroupCreateBinding.groupNameInput.clearFocus()
}

View File

@ -9,7 +9,7 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.image.CoilHelper
import org.schabi.newpipe.util.image.PicassoHelper
class ChannelItem(
private val infoItem: ChannelInfoItem,
@ -39,7 +39,7 @@ class ChannelItem(
itemChannelDescriptionView.text = infoItem.description
}
CoilHelper.loadAvatar(itemThumbnailView, infoItem.thumbnails)
PicassoHelper.loadAvatar(infoItem.thumbnails).into(itemThumbnailView)
gesturesListener?.run {
viewHolder.root.setOnClickListener { selected(infoItem) }

View File

@ -10,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.util.image.CoilHelper
import org.schabi.newpipe.util.image.PicassoHelper
data class PickerSubscriptionItem(
val subscriptionEntity: SubscriptionEntity,
@ -21,7 +21,7 @@ data class PickerSubscriptionItem(
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
CoilHelper.loadAvatar(viewBinding.thumbnailView, subscriptionEntity.avatarUrl)
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
viewBinding.titleView.text = subscriptionEntity.name
viewBinding.selectedHighlight.isVisible = isSelected
}

View File

@ -144,9 +144,7 @@ public abstract class BaseImportExportService extends Service {
notificationBuilder.setContentText(text);
}
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(getNotificationId(), notificationBuilder.build());
}
notificationManager.notify(getNotificationId(), notificationBuilder.build());
}
protected void stopService() {
@ -176,10 +174,7 @@ public abstract class BaseImportExportService extends Service {
.setContentTitle(title)
.setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty))
.setContentText(textOrEmpty);
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(getNotificationId(), notificationBuilder.build());
}
notificationManager.notify(getNotificationId(), notificationBuilder.build());
}
protected NotificationCompat.Builder createNotification() {

View File

@ -127,39 +127,39 @@ public final class PlayQueueActivity extends AppCompatActivity
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
return true;
} else if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(this);
return true;
} else if (itemId == R.id.action_append_playlist) {
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
return true;
} else if (itemId == R.id.action_playback_speed) {
openPlaybackParameterDialog();
return true;
} else if (itemId == R.id.action_mute) {
player.toggleMute();
return true;
} else if (itemId == R.id.action_system_audio) {
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
return true;
} else if (itemId == R.id.action_switch_main) {
this.player.setRecovery();
NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true);
return true;
} else if (itemId == R.id.action_switch_popup) {
if (PermissionHelper.isPopupEnabledElseAsk(this)) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
return true;
case R.id.action_append_playlist:
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
return true;
case R.id.action_playback_speed:
openPlaybackParameterDialog();
return true;
case R.id.action_mute:
player.toggleMute();
return true;
case R.id.action_system_audio:
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
return true;
case R.id.action_switch_main:
this.player.setRecovery();
NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true);
}
return true;
} else if (itemId == R.id.action_switch_background) {
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
return true;
NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true);
return true;
case R.id.action_switch_popup:
if (PermissionHelper.isPopupEnabledElseAsk(this)) {
this.player.setRecovery();
NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true);
}
return true;
case R.id.action_switch_background:
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
return true;
}
if (item.getGroupId() == MENU_ID_AUDIO_TRACK) {

View File

@ -46,14 +46,13 @@ import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static coil3.Image_androidKt.toBitmap;
import android.content.BroadcastReceiver;
import android.content.Context;
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.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
@ -81,6 +80,8 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.video.VideoSize;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
@ -113,7 +114,6 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
import org.schabi.newpipe.player.ui.BackgroundPlayerUi;
import org.schabi.newpipe.player.ui.MainPlayerUi;
import org.schabi.newpipe.player.ui.PlayerUi;
import org.schabi.newpipe.player.ui.PlayerUiList;
@ -125,14 +125,13 @@ import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.IntStream;
import coil3.target.Target;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
@ -181,6 +180,7 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/
public static final int RENDERER_UNAVAILABLE = -1;
private static final String PICASSO_PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
/*//////////////////////////////////////////////////////////////////////////
// Playback
@ -199,8 +199,6 @@ public final class Player implements PlaybackListener, Listener {
private MediaItemTag currentMetadata;
@Nullable
private Bitmap currentThumbnail;
@Nullable
private coil3.request.Disposable thumbnailDisposable;
/*//////////////////////////////////////////////////////////////////////////
// Player
@ -256,6 +254,12 @@ public final class Player implements PlaybackListener, Listener {
@NonNull
private final CompositeDisposable streamItemDisposable = new CompositeDisposable();
// This is the only listener we need for thumbnail loading, since there is always at most only
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
// which would otherwise be garbage collected since Picasso holds weak references to targets.
@NonNull
private final Target currentThumbnailTarget;
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@ -267,7 +271,6 @@ public final class Player implements PlaybackListener, Listener {
@NonNull
private final HistoryRecordManager recordManager;
private boolean screenOn = true;
/*//////////////////////////////////////////////////////////////////////////
// Constructor
@ -309,6 +312,8 @@ public final class Player implements PlaybackListener, Listener {
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
audioResolver = new AudioPlaybackResolver(context, dataSource);
currentThumbnailTarget = getCurrentThumbnailTarget();
// The UIs added here should always be present. They will be initialized when the player
// reaches the initialization step. Make sure the media session ui is before the
// notification ui in the UIs list, since the notification depends on the media session in
@ -560,12 +565,15 @@ public final class Player implements PlaybackListener, Listener {
if (queueCache == null) {
return null;
}
return SerializedCache.getInstance().take(queueCache, PlayQueue.class);
final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class);
if (newQueue == null) {
return null;
}
return newQueue;
}
private void initUIsForCurrentPlayerType() {
if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
|| (UIs.get(BackgroundPlayerUi.class).isPresent() && playerType == PlayerType.AUDIO)
|| (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
// correct UI already in place
return;
@ -584,17 +592,14 @@ public final class Player implements PlaybackListener, Listener {
switch (playerType) {
case MAIN:
UIs.destroyAll(PopupPlayerUi.class);
UIs.destroyAll(BackgroundPlayerUi.class);
UIs.addAndPrepare(new MainPlayerUi(this, binding));
break;
case POPUP:
UIs.destroyAll(MainPlayerUi.class);
UIs.destroyAll(BackgroundPlayerUi.class);
UIs.addAndPrepare(new PopupPlayerUi(this, binding));
break;
case AUDIO:
UIs.destroyAll(VideoPlayerUi.class); // destroys both MainPlayerUi and PopupPlayerUi
UIs.addAndPrepare(new BackgroundPlayerUi(this));
UIs.destroyAll(VideoPlayerUi.class);
break;
}
}
@ -697,6 +702,7 @@ public final class Player implements PlaybackListener, Listener {
databaseUpdateDisposable.clear();
progressUpdateDisposable.set(null);
streamItemDisposable.clear();
cancelLoadingCurrentThumbnail();
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
}
@ -836,12 +842,6 @@ public final class Player implements PlaybackListener, Listener {
case ACTION_SHUFFLE:
toggleShuffleModeEnabled();
break;
case Intent.ACTION_SCREEN_OFF:
screenOn = false;
break;
case Intent.ACTION_SCREEN_ON:
screenOn = true;
break;
case Intent.ACTION_CONFIGURATION_CHANGED:
if (DEBUG) {
Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received");
@ -876,58 +876,67 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/
//region Thumbnail loading
private Target getCurrentThumbnailTarget() {
// a Picasso target is just a listener for thumbnail loading events
return new Target() {
@Override
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onBitmapLoaded() called with: bitmap = [" + bitmap
+ " -> " + bitmap.getWidth() + "x" + bitmap.getHeight() + "], from = ["
+ from + "]");
}
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
onThumbnailLoaded(bitmap);
}
@Override
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
Log.e(TAG, "Thumbnail - onBitmapFailed() called", e);
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
onThumbnailLoaded(null);
}
@Override
public void onPrepareLoad(final Drawable placeHolderDrawable) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onPrepareLoad() called");
}
}
};
}
private void loadCurrentThumbnail(final List<Image> thumbnails) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with thumbnails = ["
+ thumbnails.size() + "]");
}
// Cancel any ongoing image loading
if (thumbnailDisposable != null) {
thumbnailDisposable.dispose();
}
// first cancel any previous loading
cancelLoadingCurrentThumbnail();
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
// session metadata while the new thumbnail is being loaded by Coil.
// session metadata while the new thumbnail is being loaded by Picasso.
onThumbnailLoaded(null);
if (thumbnails.isEmpty()) {
return;
}
// scale down the notification thumbnail for performance
final var thumbnailTarget = new Target() {
@Override
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 coil3.Image placeholder) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onStart() called");
}
}
@Override
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(toBitmap(result));
}
};
thumbnailDisposable = CoilHelper.INSTANCE
.loadScaledDownThumbnail(context, thumbnails, thumbnailTarget);
PicassoHelper.loadScaledDownThumbnail(context, thumbnails)
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
.into(currentThumbnailTarget);
}
private void cancelLoadingCurrentThumbnail() {
// cancel the Picasso job associated with the player thumbnail, if any
PicassoHelper.cancelTag(PICASSO_PLAYER_THUMBNAIL_TAG);
}
private void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
// Avoid useless thumbnail updates, if the thumbnail has not actually changed. Based on the
// thumbnail loading code, this if would be skipped only when both bitmaps are `null`, since
// onThumbnailLoaded won't be called twice with the same nonnull bitmap by Coil's target.
// onThumbnailLoaded won't be called twice with the same nonnull bitmap by Picasso's target.
if (currentThumbnail != bitmap) {
currentThumbnail = bitmap;
UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap));
@ -2020,7 +2029,7 @@ public final class Player implements PlaybackListener, Listener {
// resolver was called when the app was in background, the app will only stream audio when
// the user come back to the app and will never fetch the video stream.
// Note that the video is not fetched when the app is in background because the video
// renderer is fully disabled (see useVideoAndSubtitles method), except for HLS streams
// renderer is fully disabled (see useVideoSource method), except for HLS streams
// (see https://github.com/google/ExoPlayer/issues/9282).
return videoResolver.resolve(info);
}
@ -2186,19 +2195,12 @@ public final class Player implements PlaybackListener, Listener {
}
}
public void useVideoAndSubtitles(final boolean videoAndSubtitlesEnabled) {
if (playQueue == null) {
public void useVideoSource(final boolean videoEnabled) {
if (playQueue == null || audioPlayerSelected()) {
return;
}
isAudioOnly = !videoAndSubtitlesEnabled;
final var item = playQueue.getItem();
final boolean hasPendingRecovery =
item != null && item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET;
final boolean hasTimeline =
!exoPlayerIsNull() && !simpleExoPlayer.getCurrentTimeline().isEmpty();
isAudioOnly = !videoEnabled;
getCurrentStreamInfo().ifPresentOrElse(info -> {
// In case we don't know the source type, fall back to either video-with-audio, or
@ -2206,34 +2208,27 @@ public final class Player implements PlaybackListener, Listener {
final SourceType sourceType = videoResolver.getStreamSourceType()
.orElse(SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY);
if (hasTimeline || !hasPendingRecovery) {
// making sure to save playback position before reloadPlayQueueManager()
setRecovery();
}
if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) {
reloadPlayQueueManager();
}
setRecovery();
// Disable or enable video and subtitles renderers depending of the videoEnabled value
trackSelector.setParameters(trackSelector.buildUponParameters()
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, !videoEnabled)
.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, !videoEnabled));
}, () -> {
/*
The current metadata may be null sometimes (for e.g. when using an unstable connection
in livestreams) so we will be not able to execute the block above
in livestreams) so we will be not able to execute the block below
Reload the play queue manager in this case, which is the behavior when we don't know the
index of the video renderer or playQueueManagerReloadingNeeded returns true
*/
if (hasTimeline || !hasPendingRecovery) {
// making sure to save playback position before reloadPlayQueueManager()
setRecovery();
}
reloadPlayQueueManager();
setRecovery();
});
// Disable or enable video and subtitles renderers depending of the
// videoAndSubtitlesEnabled value
trackSelector.setParameters(trackSelector.buildUponParameters()
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, !videoAndSubtitlesEnabled)
.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, !videoAndSubtitlesEnabled));
}
/**
@ -2466,11 +2461,4 @@ public final class Player implements PlaybackListener, Listener {
.orElse(RENDERER_UNAVAILABLE);
}
//endregion
/**
* @return whether the device screen is turned on.
*/
public boolean isScreenOn() {
return screenOn;
}
}

View File

@ -14,8 +14,10 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTvHtml5UserAgent;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5StreamingUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebEmbeddedPlayerStreamingUrl;
import static java.lang.Math.min;
@ -659,7 +661,10 @@ public final class YoutubeHttpDataSource extends BaseDataSource implements HttpD
}
}
final boolean isTvHtml5StreamingUrl = isTvHtml5StreamingUrl(requestUrl);
if (isWebStreamingUrl(requestUrl)
|| isTvHtml5StreamingUrl
|| isWebEmbeddedPlayerStreamingUrl(requestUrl)) {
httpURLConnection.setRequestProperty(HttpHeaders.ORIGIN, YOUTUBE_BASE_URL);
httpURLConnection.setRequestProperty(HttpHeaders.REFERER, YOUTUBE_BASE_URL);
@ -680,6 +685,9 @@ public final class YoutubeHttpDataSource extends BaseDataSource implements HttpD
} else if (isIosStreamingUrl) {
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
getIosUserAgent(null));
} else if (isTvHtml5StreamingUrl) {
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
getTvHtml5UserAgent());
} else {
// non-mobile user agent
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, DownloaderImpl.USER_AGENT);

View File

@ -47,9 +47,6 @@ abstract class BasePlayerGestureListener(
startMultiDoubleTap(event)
} else if (portion === DisplayPortion.MIDDLE) {
player.playPause()
if (player.isPlaying) {
playerUi.hideControls(0, 0)
}
}
}

View File

@ -129,13 +129,6 @@ public class PlayerDataSource {
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
cachelessDataSourceFactory);
}
public DashMediaSource.Factory getLiveYoutubeDashMediaSourceFactory() {
return new DashMediaSource.Factory(
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
cachelessDataSourceFactory)
.setManifestParser(new YoutubeDashLiveManifestParser());
}
//endregion

View File

@ -49,12 +49,12 @@ import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public final class PlayerHelper {
private static final FormattersProvider FORMATTERS_PROVIDER = new FormattersProvider();
@ -87,11 +87,11 @@ public final class PlayerHelper {
}
@NonNull
public static String getTimeString(final long milliSeconds) {
final long seconds = (milliSeconds % 60000) / 1000;
final long minutes = (milliSeconds % 3600000) / 60000;
final long hours = (milliSeconds % 86400000) / 3600000;
final long days = (milliSeconds % (86400000 * 7)) / 86400000;
public static String getTimeString(final int milliSeconds) {
final int seconds = (milliSeconds % 60000) / 1000;
final int minutes = (milliSeconds % 3600000) / 60000;
final int hours = (milliSeconds % 86400000) / 3600000;
final int days = (milliSeconds % (86400000 * 7)) / 86400000;
final Formatters formatters = FORMATTERS_PROVIDER.formatters();
if (days > 0) {
@ -174,9 +174,10 @@ public final class PlayerHelper {
@Nullable
public static PlayQueue autoQueueOf(@NonNull final StreamInfo info,
@NonNull final List<PlayQueueItem> existingItems) {
final Set<String> urls = existingItems.stream()
.map(PlayQueueItem::getUrl)
.collect(Collectors.toUnmodifiableSet());
final Set<String> urls = new HashSet<>(existingItems.size());
for (final PlayQueueItem item : existingItems) {
urls.add(item.getUrl());
}
final List<InfoItem> relatedItems = info.getRelatedItems();
if (Utils.isNullOrEmpty(relatedItems)) {

View File

@ -117,7 +117,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.getInstance();
return App.getApp();
}
public void startService(final boolean playAfterConnect,

View File

@ -1,68 +0,0 @@
package org.schabi.newpipe.player.helper;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.Period;
import com.google.android.exoplayer2.source.dash.manifest.ProgramInformation;
import com.google.android.exoplayer2.source.dash.manifest.ServiceDescriptionElement;
import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
import java.util.List;
/**
* A {@link DashManifestParser} fixing YouTube DASH manifests to allow starting playback from the
* newest period available instead of the earliest one in some cases.
*
* <p>
* It changes the {@code availabilityStartTime} passed to a custom value doing the workaround.
* A better approach to fix the issue should be investigated and used in the future.
* </p>
*/
public class YoutubeDashLiveManifestParser extends DashManifestParser {
// Result of Util.parseXsDateTime("1970-01-01T00:00:00Z")
private static final long AVAILABILITY_START_TIME_TO_USE = 0;
// There is no computation made with the availabilityStartTime value in the
// parseMediaPresentationDescription method itself, so we can just override methods called in
// this method using the workaround value
// Overriding parsePeriod does not seem to be needed
@SuppressWarnings("checkstyle:ParameterNumber")
@NonNull
@Override
protected DashManifest buildMediaPresentationDescription(
final long availabilityStartTime,
final long durationMs,
final long minBufferTimeMs,
final boolean dynamic,
final long minUpdateTimeMs,
final long timeShiftBufferDepthMs,
final long suggestedPresentationDelayMs,
final long publishTimeMs,
@Nullable final ProgramInformation programInformation,
@Nullable final UtcTimingElement utcTiming,
@Nullable final ServiceDescriptionElement serviceDescription,
@Nullable final Uri location,
@NonNull final List<Period> periods) {
return super.buildMediaPresentationDescription(
AVAILABILITY_START_TIME_TO_USE,
durationMs,
minBufferTimeMs,
dynamic,
minUpdateTimeMs,
timeShiftBufferDepthMs,
suggestedPresentationDelayMs,
publishTimeMs,
programInformation,
utcTiming,
serviceDescription,
location,
periods);
}
}

View File

@ -22,7 +22,7 @@ internal fun infoItemTypeToString(type: InfoType): String {
InfoType.STREAM -> ID_STREAM
InfoType.PLAYLIST -> ID_PLAYLIST
InfoType.CHANNEL -> ID_CHANNEL
else -> error("Unexpected value: $type")
else -> throw IllegalStateException("Unexpected value: $type")
}
}
@ -31,7 +31,7 @@ internal fun infoItemTypeFromString(type: String): InfoType {
ID_STREAM -> InfoType.STREAM
ID_PLAYLIST -> InfoType.PLAYLIST
ID_CHANNEL -> InfoType.CHANNEL
else -> error("Unexpected value: $type")
else -> throw IllegalStateException("Unexpected value: $type")
}
}

View File

@ -82,11 +82,11 @@ internal class PackageValidator(context: Context) {
// Build the caller info for the rest of the checks here.
val callerPackageInfo = buildCallerInfo(callingPackage)
?: error("Caller wasn't found in the system?")
?: throw IllegalStateException("Caller wasn't found in the system?")
// Verify that things aren't ... broken. (This test should always pass.)
check(callerPackageInfo.uid == callingUid) {
"Caller's package UID doesn't match caller's actual UID?"
if (callerPackageInfo.uid != callingUid) {
throw IllegalStateException("Caller's package UID doesn't match caller's actual UID?")
}
val callerSignature = callerPackageInfo.signature
@ -202,7 +202,7 @@ internal class PackageValidator(context: Context) {
*/
private fun getSystemSignature(): String = getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
getSignature(platformInfo)
} ?: error("Platform signature not found")
} ?: throw IllegalStateException("Platform signature not found")
/**
* Creates a SHA-256 signature given a certificate byte array.

View File

@ -72,9 +72,7 @@ public final class NotificationUtil {
notificationBuilder = createNotification();
}
updateNotification();
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
public synchronized void updateThumbnail() {
@ -86,9 +84,7 @@ public final class NotificationUtil {
}
setLargeIcon(notificationBuilder);
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
}

View File

@ -6,8 +6,8 @@ import android.view.MotionEvent;
import android.view.View;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString();
@ -33,7 +33,7 @@ public class PlayQueueItemBuilder {
holder.itemDurationView.setVisibility(View.GONE);
}
CoilHelper.INSTANCE.loadThumbnail(holder.itemThumbnailView, item.getThumbnails());
PicassoHelper.loadThumbnail(item.getThumbnails()).into(holder.itemThumbnailView);
holder.itemRoot.setOnClickListener(view -> {
if (onItemClickListener != null) {

View File

@ -5,8 +5,8 @@ import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public final class SinglePlayQueue extends PlayQueue {
public SinglePlayQueue(final StreamInfoItem item) {
@ -29,7 +29,11 @@ public final class SinglePlayQueue extends PlayQueue {
}
private static List<PlayQueueItem> playQueueItemsOf(@NonNull final List<StreamInfoItem> items) {
return items.stream().map(PlayQueueItem::new).collect(Collectors.toList());
final List<PlayQueueItem> playQueueItems = new ArrayList<>(items.size());
for (final StreamInfoItem item : items) {
playQueueItems.add(new PlayQueueItem(item));
}
return playQueueItems;
}
@Override

View File

@ -201,14 +201,11 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
try {
final StreamInfoTag tag = StreamInfoTag.of(info);
// Prefer DASH over HLS because of an exoPlayer bug that causes the background player to
// also fetch the video stream even if it is supposed to just fetch the audio stream.
if (!info.getDashMpdUrl().isEmpty()) {
return buildLiveMediaSource(
dataSource, info.getDashMpdUrl(), C.CONTENT_TYPE_DASH, tag);
}
if (!info.getHlsUrl().isEmpty()) {
return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.CONTENT_TYPE_HLS, tag);
} else if (!info.getDashMpdUrl().isEmpty()) {
return buildLiveMediaSource(
dataSource, info.getDashMpdUrl(), C.CONTENT_TYPE_DASH, tag);
}
} catch (final Exception e) {
Log.w(TAG, "Error when generating live media source, falling back to standard sources",
@ -228,11 +225,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
factory = dataSource.getLiveSsMediaSourceFactory();
break;
case C.CONTENT_TYPE_DASH:
if (metadata.getServiceId() == ServiceList.YouTube.getServiceId()) {
factory = dataSource.getLiveYoutubeDashMediaSourceFactory();
} else {
factory = dataSource.getLiveDashMediaSourceFactory();
}
factory = dataSource.getLiveDashMediaSourceFactory();
break;
case C.CONTENT_TYPE_HLS:
factory = dataSource.getLiveHlsMediaSourceFactory();

View File

@ -13,9 +13,8 @@ import androidx.collection.SparseArrayCompat;
import com.google.common.base.Stopwatch;
import org.schabi.newpipe.App;
import org.schabi.newpipe.extractor.stream.Frameset;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.Comparator;
import java.util.List;
@ -208,8 +207,8 @@ public class SeekbarPreviewThumbnailHolder {
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
// 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.getInstance(), url);
// Ensure that your are not running on the main-Thread this will otherwise hang
final Bitmap bitmap = PicassoHelper.loadSeekbarThumbnailPreview(url).get();
if (sw != null) {
Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took "

View File

@ -1,29 +0,0 @@
package org.schabi.newpipe.player.ui;
import androidx.annotation.NonNull;
import org.schabi.newpipe.player.Player;
/**
* This is not a "graphical" UI for the background player, but it is used to disable fetching video
* and text tracks with it.
*
* <p>
* This allows reducing data usage for manifest sources with demuxed audio and video,
* such as livestreams.
* </p>
*/
public class BackgroundPlayerUi extends PlayerUi {
public BackgroundPlayerUi(@NonNull final Player player) {
super(player);
}
@Override
public void initPlayback() {
super.initPlayback();
// Make sure to disable video and subtitles track types
player.useVideoAndSubtitles(false);
}
}

View File

@ -77,7 +77,6 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutChangeListener {
private static final String TAG = MainPlayerUi.class.getSimpleName();
@ -217,10 +216,6 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
playQueueAdapter = new PlayQueueAdapter(context,
Objects.requireNonNull(player.getPlayQueue()));
segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener());
// Make sure video and text tracks are enabled if the user is in the app, in the case user
// switched from background player to main player
player.useVideoAndSubtitles(fragmentIsVisible);
}
@Override
@ -336,7 +331,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
} else if (VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED.equals(intent.getAction())) {
// Restore video source when user returns to the fragment
fragmentIsVisible = true;
player.useVideoAndSubtitles(true);
player.useVideoSource(true);
// When a user returns from background, the system UI will always be shown even if
// controls are invisible: hide it in that case
@ -375,7 +370,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
if (player.isPlaying() || player.isLoading()) {
switch (getMinimizeOnExitAction(context)) {
case MINIMIZE_ON_EXIT_MODE_BACKGROUND:
player.useVideoAndSubtitles(false);
player.useVideoSource(false);
break;
case MINIMIZE_ON_EXIT_MODE_POPUP:
getParentActivity().ifPresent(activity -> {
@ -750,13 +745,13 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
}
private int getNearestStreamSegmentPosition(final long playbackPosition) {
int nearestPosition = 0;
final List<StreamSegment> segments = player.getCurrentStreamInfo()
.map(StreamInfo::getStreamSegments)
.orElse(Collections.emptyList());
int nearestPosition = 0;
for (final var segment : segments) {
if (segment.getStartTimeSeconds() * 1000L > playbackPosition) {
for (int i = 0; i < segments.size(); i++) {
if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
break;
}
nearestPosition++;
@ -817,13 +812,22 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
}
final int currentStream = playQueue.getIndex();
int before = 0;
int after = 0;
final List<PlayQueueItem> streams = playQueue.getStreams();
final int nStreams = streams.size();
final long before = streams.subList(0, currentStream).stream()
.collect(Collectors.summingLong(PlayQueueItem::getDuration)) * 1000;
for (int i = 0; i < nStreams; i++) {
if (i < currentStream) {
before += streams.get(i).getDuration();
} else {
after += streams.get(i).getDuration();
}
}
final long after = streams.subList(currentStream, streams.size()).stream()
.collect(Collectors.summingLong(PlayQueueItem::getDuration)) * 1000;
before *= 1000;
after *= 1000;
binding.itemsListHeaderDuration.setText(
String.format("%s/%s",

View File

@ -152,14 +152,6 @@ public final class PopupPlayerUi extends VideoPlayerUi {
windowManager.addView(closeOverlayBinding.getRoot(), closeOverlayLayoutParams);
}
@Override
public void initPlayback() {
super.initPlayback();
// Make sure video and text tracks are enabled if the screen is turned on (which should
// always be the case), in the case user switched from background player to popup player
player.useVideoAndSubtitles(player.isScreenOn());
}
@Override
protected void setupElementsVisibility() {
binding.fullScreenButton.setVisibility(View.VISIBLE);
@ -227,10 +219,10 @@ public final class PopupPlayerUi extends VideoPlayerUi {
} else if (player.isPlaying() || player.isLoading()) {
if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
// Use only audio source when screen turns off while popup player is playing
player.useVideoAndSubtitles(false);
player.useVideoSource(false);
} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
// Restore video source when screen turns on and user was watching video in popup
player.useVideoAndSubtitles(true);
player.useVideoSource(true);
}
}
}

View File

@ -19,12 +19,12 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import java.io.IOException;
import java.util.Locale;
import coil3.SingletonImageLoader;
public class ContentSettingsFragment extends BasePreferenceFragment {
private String youtubeRestrictedModeEnabledKey;
@ -74,12 +74,14 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
(preference, newValue) -> {
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
.fromPreferenceKey(requireContext(), (String) newValue));
final var loader = SingletonImageLoader.get(preference.getContext());
loader.getMemoryCache().clear();
loader.getDiskCache().clear();
Toast.makeText(preference.getContext(),
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
try {
PicassoHelper.clearCache(preference.getContext());
Toast.makeText(preference.getContext(),
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
.show();
} catch (final IOException e) {
Log.e(TAG, "Unable to clear Picasso cache", e);
}
return true;
});
}

View File

@ -10,6 +10,7 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.Optional;
@ -24,6 +25,8 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
requirePreference(R.string.allow_heap_dumping_key);
final Preference showMemoryLeaksPreference =
requirePreference(R.string.show_memory_leaks_key);
final Preference showImageIndicatorsPreference =
requirePreference(R.string.show_image_indicators_key);
final Preference checkNewStreamsPreference =
requirePreference(R.string.check_new_streams_key);
final Preference crashTheAppPreference =
@ -51,6 +54,11 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
showMemoryLeaksPreference.setSummary(R.string.leak_canary_not_available);
}
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
return true;
});
checkNewStreamsPreference.setOnPreferenceClickListener(preference -> {
NotificationWorker.runNow(preference.getContext());
return true;

View File

@ -157,7 +157,7 @@ public final class NewPipeSettings {
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
&& !prefs.getBoolean(disabledTunnelingKey, false);
if (App.getInstance().isFirstRun()
if (App.getApp().isFirstRun()
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
setMediaTunneling(context);
}

View File

@ -19,8 +19,8 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.List;
import java.util.Vector;
@ -190,7 +190,7 @@ public class SelectChannelFragment extends DialogFragment {
final SubscriptionEntity entry = subscriptions.get(position);
holder.titleView.setText(entry.getName());
holder.view.setOnClickListener(view -> clickedItem(position));
CoilHelper.INSTANCE.loadAvatar(holder.thumbnailView, entry.getAvatarUrl());
PicassoHelper.loadAvatar(entry.getAvatarUrl()).into(holder.thumbnailView);
}
@Override

View File

@ -27,7 +27,7 @@ import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List;
import java.util.Vector;
@ -154,17 +154,21 @@ public class SelectPlaylistFragment extends DialogFragment {
final int position) {
final PlaylistLocalItem selectedItem = playlists.get(position);
if (selectedItem instanceof PlaylistMetadataEntry entry) {
holder.titleView.setText(entry.getOrderingName());
holder.view.setOnClickListener(view -> clickedItem(position));
CoilHelper.INSTANCE.loadPlaylistThumbnail(holder.thumbnailView,
entry.getThumbnailUrl());
if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
} else if (selectedItem instanceof PlaylistRemoteEntity entry) {
holder.titleView.setText(entry.getOrderingName());
holder.view.setOnClickListener(view -> clickedItem(position));
CoilHelper.INSTANCE.loadPlaylistThumbnail(holder.thumbnailView,
entry.getThumbnailUrl());
PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
.into(holder.thumbnailView);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
holder.titleView.setText(entry.getOrderingName());
holder.view.setOnClickListener(view -> clickedItem(position));
PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
.into(holder.thumbnailView);
}
}

View File

@ -0,0 +1,58 @@
package org.schabi.newpipe.settings.export;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.Set;
/**
* An {@link ObjectInputStream} that only allows preferences-related types to be deserialized, to
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
* <a href="https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution">
* cmu.edu
* </a>,
* <a href="https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream">
* OWASP cheatsheet
* </a>,
* <a href="https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118">
* Apache's {@code ValidatingObjectInputStream}
* </a>
*/
public class PreferencesObjectInputStream extends ObjectInputStream {
/**
* Primitive types, strings and other built-in types do not pass through resolveClass() but
* instead have a custom encoding; see
* <a href="https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152">
* official docs</a>.
*/
private static final Set<String> CLASS_WHITELIST = Set.of(
"java.lang.Boolean",
"java.lang.Byte",
"java.lang.Character",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double",
"java.lang.Void",
"java.util.HashMap",
"java.util.HashSet"
);
public PreferencesObjectInputStream(final InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(final ObjectStreamClass desc)
throws ClassNotFoundException, IOException {
if (CLASS_WHITELIST.contains(desc.getName())) {
return super.resolveClass(desc);
} else {
throw new ClassNotFoundException("Class not allowed: " + desc.getName());
}
}
}

View File

@ -1,52 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.settings.export
import java.io.IOException
import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectStreamClass
/**
* An [ObjectInputStream] that only allows preferences-related types to be deserialized, to
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
* [cmu.edu](https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution) * ,
* [OWASP cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream) * ,
* [Apache's `ValidatingObjectInputStream`](https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118) *
*/
class PreferencesObjectInputStream(stream: InputStream) : ObjectInputStream(stream) {
@Throws(ClassNotFoundException::class, IOException::class)
override fun resolveClass(desc: ObjectStreamClass): Class<*> {
if (desc.name in CLASS_WHITELIST) {
return super.resolveClass(desc)
} else {
throw ClassNotFoundException("Class not allowed: $desc.name")
}
}
companion object {
/**
* Primitive types, strings and other built-in types do not pass through resolveClass() but
* instead have a custom encoding; see
* [
* official docs](https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152).
*/
private val CLASS_WHITELIST = setOf<String>(
"java.lang.Boolean",
"java.lang.Byte",
"java.lang.Character",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double",
"java.lang.Void",
"java.util.HashMap",
"java.util.HashSet"
)
}
}

View File

@ -251,7 +251,7 @@ public final class SettingMigrations {
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
// no migration to run, already up to date
if (App.getInstance().isFirstRun()) {
if (App.getApp().isFirstRun()) {
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
return;
} else if (lastPrefVersion == VERSION) {

View File

@ -26,13 +26,14 @@ data class PreferenceSearchItem(
val breadcrumbs: String,
@XmlRes val searchIndexItemResId: Int
) {
val allRelevantSearchFields: List<String>
get() = listOf(title, summary, entries, breadcrumbs)
fun hasData(): Boolean {
return !key.isEmpty() && !title.isEmpty()
}
fun getAllRelevantSearchFields(): MutableList<String?> {
return mutableListOf(title, summary, entries, breadcrumbs)
}
override fun toString(): String {
return "PreferenceItem: $title $summary $key"
}

View File

@ -9,9 +9,8 @@ import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Class to get a JSON representation of a list of tabs, and the other way around.
@ -45,25 +44,39 @@ public final class TabsJsonHelper {
return getDefaultTabs();
}
final List<Tab> returnTabs = new ArrayList<>();
final JsonObject outerJsonObject;
try {
final JsonObject outerJsonObject = JsonParser.object().from(tabsJson);
outerJsonObject = JsonParser.object().from(tabsJson);
if (!outerJsonObject.has(JSON_TABS_ARRAY_KEY)) {
throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY
+ "\" array");
}
final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY, null);
final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY);
final var returnTabs = tabsArray.streamAsJsonObjects()
.map(Tab::from)
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableList());
for (final Object o : tabsArray) {
if (!(o instanceof JsonObject)) {
continue;
}
return returnTabs.isEmpty() ? getDefaultTabs() : returnTabs;
final Tab tab = Tab.from((JsonObject) o);
if (tab != null) {
returnTabs.add(tab);
}
}
} catch (final JsonParserException e) {
throw new InvalidJsonException(e);
}
if (returnTabs.isEmpty()) {
return getDefaultTabs();
}
return returnTabs;
}
/**

View File

@ -0,0 +1,51 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
/**
* For preferences with dependencies and multiple use case,
* this class can be used to reduce the lines of code.
*/
public final class DependentPreferenceHelper {
private DependentPreferenceHelper() {
// no instance
}
/**
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
* `Resume playback` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Resume playback` and `Watch history` are both enabled
*/
public static boolean getResumePlaybackEnabled(final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(context.getString(
R.string.enable_watch_history_key), true)
&& prefs.getBoolean(context.getString(
R.string.enable_playback_resume_key), true);
}
/**
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
* `Position in lists` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Positions in lists` and `Watch history` are both enabled
*/
public static boolean getPositionsInListsEnabled(final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(context.getString(
R.string.enable_watch_history_key), true)
&& prefs.getBoolean(context.getString(
R.string.enable_playback_state_lists_key), true);
}
}

View File

@ -1,46 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
/**
* For preferences with dependencies and multiple use case,
* this class can be used to reduce the lines of code.
*/
object DependentPreferenceHelper {
/**
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
* `Resume playback` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Resume playback` and `Watch history` are both enabled
*/
@JvmStatic
fun getResumePlaybackEnabled(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true)
}
/**
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
* `Position in lists` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Positions in lists` and `Watch history` are both enabled
*/
@JvmStatic
fun getPositionsInListsEnabled(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
prefs.getBoolean(context.getString(R.string.enable_playback_state_lists_key), true)
}
}

View File

@ -131,7 +131,7 @@ public final class DeviceUtils {
}
isFireTV =
App.getInstance().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
return isFireTV;
}
@ -140,7 +140,7 @@ public final class DeviceUtils {
return isTV;
}
final PackageManager pm = App.getInstance().getPackageManager();
final PackageManager pm = App.getApp().getPackageManager();
// from doc: https://developer.android.com/training/tv/start/hardware.html#runtime-check
boolean isTv = ContextCompat.getSystemService(context, UiModeManager.class)

View File

@ -48,7 +48,7 @@ public final class KeyboardUtil {
final InputMethodManager imm = ContextCompat.getSystemService(activity,
InputMethodManager.class);
imm.hideSoftInputFromWindow(editText.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
InputMethodManager.RESULT_UNCHANGED_SHOWN);
editText.clearFocus();
}

View File

@ -501,7 +501,6 @@ public final class NavigationHelper {
public static void openCommentRepliesFragment(@NonNull final FragmentActivity activity,
@NonNull final CommentsInfoItem comment) {
closeCommentRepliesFragments(activity);
defaultTransaction(activity.getSupportFragmentManager())
.replace(R.id.fragment_holder, new CommentRepliesFragment(comment),
CommentRepliesFragment.TAG)
@ -509,41 +508,6 @@ public final class NavigationHelper {
.commit();
}
/**
* Closes all open {@link CommentRepliesFragment}s in {@code activity},
* including those that are not at the top of the back stack.
* This is needed to prevent multiple open CommentRepliesFragments
* Ideally there should only be one since we remove existing before opening a new one.
* @param activity the activity in which to close the CommentRepliesFragments
*/
public static void closeCommentRepliesFragments(@NonNull final FragmentActivity activity) {
final FragmentManager fm = activity.getSupportFragmentManager();
// Remove all existing fragment instances tagged as CommentRepliesFragment
final FragmentTransaction tx = defaultTransaction(fm);
boolean removed = false;
for (final Fragment fragment : fm.getFragments()) {
if (fragment != null && CommentRepliesFragment.TAG.equals(fragment.getTag())) {
tx.remove(fragment);
removed = true;
}
}
if (removed) {
tx.commit();
}
// Only pop back stack entries named CommentRepliesFragment.TAG if they are at the top.
while (fm.getBackStackEntryCount() > 0
&& CommentRepliesFragment.TAG.equals(
fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 1).getName()
)
) {
fm.popBackStackImmediate(CommentRepliesFragment.TAG,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
public static void openPlaylistFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
@NonNull final String name) {

View File

@ -0,0 +1,61 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.text.Selection;
import android.text.Spannable;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.NewPipeEditText;
import org.schabi.newpipe.views.NewPipeTextView;
public final class NewPipeTextViewHelper {
private NewPipeTextViewHelper() {
}
/**
* Share the selected text of {@link NewPipeTextView NewPipeTextViews} and
* {@link NewPipeEditText NewPipeEditTexts} with
* {@link ShareUtils#shareText(Context, String, String)}.
*
* <p>
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
* using the {@code Share} command of the popup menu which appears when selecting text.
* </p>
*
* @param textView the {@link TextView} on which sharing the selected text. It should be a
* {@link NewPipeTextView} or a {@link NewPipeEditText} (even if
* {@link TextView standard TextViews} are supported).
*/
public static void shareSelectedTextWithShareUtils(@NonNull final TextView textView) {
final CharSequence textViewText = textView.getText();
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText));
if (textViewText instanceof Spannable) {
Selection.setSelection((Spannable) textViewText, textView.getSelectionEnd());
}
}
@Nullable
private static CharSequence getSelectedText(@NonNull final TextView textView,
@Nullable final CharSequence text) {
if (!textView.hasSelection() || text == null) {
return null;
}
final int start = textView.getSelectionStart();
final int end = textView.getSelectionEnd();
return String.valueOf(start > end ? text.subSequence(end, start)
: text.subSequence(start, end));
}
private static void shareSelectedTextIfNotNullAndNotEmpty(
@NonNull final TextView textView,
@Nullable final CharSequence selectedText) {
if (selectedText != null && selectedText.length() != 0) {
ShareUtils.shareText(textView.getContext(), "", selectedText.toString());
}
}
}

View File

@ -1,60 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.text.Selection
import android.text.Spannable
import android.widget.TextView
import org.schabi.newpipe.util.external_communication.ShareUtils
object NewPipeTextViewHelper {
/**
* Share the selected text of [NewPipeTextViews][org.schabi.newpipe.views.NewPipeTextView] and
* [NewPipeEditTexts][org.schabi.newpipe.views.NewPipeEditText] with
* [ShareUtils.shareText].
*
*
*
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
* using the `Share` command of the popup menu which appears when selecting text.
*
*
* @param textView the [TextView] on which sharing the selected text. It should be a
* [org.schabi.newpipe.views.NewPipeTextView] or a [org.schabi.newpipe.views.NewPipeEditText]
* (even if [standard TextViews][TextView] are supported).
*/
@JvmStatic
fun shareSelectedTextWithShareUtils(textView: TextView) {
val textViewText = textView.getText()
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText))
if (textViewText is Spannable) {
Selection.setSelection(textViewText, textView.selectionEnd)
}
}
private fun getSelectedText(textView: TextView, text: CharSequence?): CharSequence? {
if (!textView.hasSelection() || text == null) {
return null
}
val start = textView.selectionStart
val end = textView.selectionEnd
return if (start > end) {
text.subSequence(end, start)
} else {
text.subSequence(start, end)
}
}
private fun shareSelectedTextIfNotNullAndNotEmpty(
textView: TextView,
selectedText: CharSequence?
) {
if (!selectedText.isNullOrEmpty()) {
ShareUtils.shareText(textView.context, "", selectedText.toString())
}
}
}

View File

@ -0,0 +1,69 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import java.util.ArrayList;
import java.util.List;
public final class PeertubeHelper {
private PeertubeHelper() { }
public static List<PeertubeInstance> getInstanceList(final Context context) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key);
final String savedJson = sharedPreferences.getString(savedInstanceListKey, null);
if (null == savedJson) {
return List.of(getCurrentInstance());
}
try {
final JsonArray array = JsonParser.object().from(savedJson).getArray("instances");
final List<PeertubeInstance> result = new ArrayList<>();
for (final Object o : array) {
if (o instanceof JsonObject) {
final JsonObject instance = (JsonObject) o;
final String name = instance.getString("name");
final String url = instance.getString("url");
result.add(new PeertubeInstance(url, name));
}
}
return result;
} catch (final JsonParserException e) {
return List.of(getCurrentInstance());
}
}
public static PeertubeInstance selectInstance(final PeertubeInstance instance,
final Context context) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String selectedInstanceKey =
context.getString(R.string.peertube_selected_instance_key);
final JsonStringWriter jsonWriter = JsonWriter.string().object();
jsonWriter.value("name", instance.getName());
jsonWriter.value("url", instance.getUrl());
final String jsonToSave = jsonWriter.end().done();
sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply();
ServiceList.PeerTube.setInstance(instance);
return instance;
}
public static PeertubeInstance getCurrentInstance() {
return ServiceList.PeerTube.getInstance();
}
}

View File

@ -1,52 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.grack.nanojson.JsonObject
import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonWriter
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
object PeertubeHelper {
@JvmStatic
val currentInstance: PeertubeInstance
get() = ServiceList.PeerTube.instance
@JvmStatic
fun getInstanceList(context: Context): List<PeertubeInstance> {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val savedInstanceListKey = context.getString(R.string.peertube_instance_list_key)
val savedJson = sharedPreferences.getString(savedInstanceListKey, null)
?: return listOf(currentInstance)
return runCatching {
JsonParser.`object`().from(savedJson).getArray("instances")
.filterIsInstance<JsonObject>()
.map { PeertubeInstance(it.getString("url"), it.getString("name")) }
}.getOrDefault(listOf(currentInstance))
}
@JvmStatic
fun selectInstance(instance: PeertubeInstance, context: Context): PeertubeInstance {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key)
val jsonWriter = JsonWriter.string().`object`()
jsonWriter.value("name", instance.name)
jsonWriter.value("url", instance.url)
val jsonToSave = jsonWriter.end().done()
sharedPreferences.edit { putString(selectedInstanceKey, jsonToSave) }
ServiceList.PeerTube.instance = instance
return instance
}
}

View File

@ -90,10 +90,10 @@ public final class PermissionHelper {
&& ContextCompat.checkSelfPermission(activity,
Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
if (!App.getInstance().getNotificationsRequested()) {
if (!App.getApp().getNotificationsRequested()) {
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.POST_NOTIFICATIONS}, requestCode);
App.getInstance().setNotificationsRequested();
App.getApp().setNotificationsRequested();
return false;
}
}

View File

@ -0,0 +1,94 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
import org.schabi.newpipe.player.PlayerType;
/**
* Utility class for play buttons and their respective click listeners.
*/
public final class PlayButtonHelper {
private PlayButtonHelper() {
// utility class
}
/**
* Initialize {@link android.view.View.OnClickListener OnClickListener}
* and {@link android.view.View.OnLongClickListener OnLongClickListener} for playlist control
* buttons defined in {@link R.layout#playlist_control}.
*
* @param activity The activity to use for the {@link android.widget.Toast Toast}.
* @param playlistControlBinding The binding of the
* {@link R.layout#playlist_control playlist control layout}.
* @param fragment The fragment to get the play queue from.
*/
public static void initPlaylistControlClickListener(
@NonNull final AppCompatActivity activity,
@NonNull final PlaylistControlBinding playlistControlBinding,
@NonNull final PlaylistControlViewHolder fragment) {
// click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue());
showHoldToAppendToastIfNeeded(activity);
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false);
showHoldToAppendToastIfNeeded(activity);
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false);
showHoldToAppendToastIfNeeded(activity);
});
// long click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN);
return true;
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP);
return true;
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO);
return true;
});
}
/**
* Show the "hold to append" toast if the corresponding preference is enabled.
*
* @param context The context to show the toast.
*/
private static void showHoldToAppendToastIfNeeded(@NonNull final Context context) {
if (shouldShowHoldToAppendTip(context)) {
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
}
}
/**
* Check if the "hold to append" toast should be shown.
*
* <p>
* The tip is shown if the corresponding preference is enabled.
* This is the default behaviour.
* </p>
*
* @param context The context to get the preference.
* @return {@code true} if the tip should be shown, {@code false} otherwise.
*/
public static boolean shouldShowHoldToAppendTip(@NonNull final Context context) {
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_hold_to_append_key), true);
}
}

View File

@ -1,96 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import android.view.View
import android.view.View.OnLongClickListener
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.PlaylistControlBinding
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder
import org.schabi.newpipe.player.PlayerType
/**
* Utility class for play buttons and their respective click listeners.
*/
object PlayButtonHelper {
/**
* Initialize [OnClickListener][View.OnClickListener]
* and [OnLongClickListener][OnLongClickListener] for playlist control
* buttons defined in [R.layout.playlist_control].
*
* @param activity The activity to use for the [Toast][Toast].
* @param playlistControlBinding The binding of the
* [playlist control layout][R.layout.playlist_control].
* @param fragment The fragment to get the play queue from.
*/
@JvmStatic
fun initPlaylistControlClickListener(
activity: AppCompatActivity,
playlistControlBinding: PlaylistControlBinding,
fragment: PlaylistControlViewHolder
) {
// click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener {
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue())
showHoldToAppendToastIfNeeded(activity)
}
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener {
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false)
showHoldToAppendToastIfNeeded(activity)
}
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener {
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false)
showHoldToAppendToastIfNeeded(activity)
}
// long click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN)
true
}
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP)
true
}
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO)
true
}
}
/**
* Show the "hold to append" toast if the corresponding preference is enabled.
*
* @param context The context to show the toast.
*/
private fun showHoldToAppendToastIfNeeded(context: Context) {
if (shouldShowHoldToAppendTip(context)) {
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show()
}
}
/**
* Check if the "hold to append" toast should be shown.
*
*
*
* The tip is shown if the corresponding preference is enabled.
* This is the default behaviour.
*
*
* @param context The context to get the preference.
* @return `true` if the tip should be shown, `false` otherwise.
*/
@JvmStatic
fun shouldShowHoldToAppendTip(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_hold_to_append_key), true)
}
}

View File

@ -21,7 +21,7 @@ object ReleaseVersionUtil {
val certificates = mapOf(
RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256
)
val app = App.instance
val app = App.getApp()
try {
PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false)
} catch (e: PackageManager.NameNotFoundException) {

View File

@ -0,0 +1,213 @@
package org.schabi.newpipe.util;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public final class ServiceHelper {
private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube;
private ServiceHelper() { }
@DrawableRes
public static int getIcon(final int serviceId) {
switch (serviceId) {
case 0:
return R.drawable.ic_smart_display;
case 1:
return R.drawable.ic_cloud;
case 2:
return R.drawable.ic_placeholder_media_ccc;
case 3:
return R.drawable.ic_placeholder_peertube;
case 4:
return R.drawable.ic_placeholder_bandcamp;
default:
return R.drawable.ic_circle;
}
}
public static String getTranslatedFilterString(final String filter, final Context c) {
switch (filter) {
case "all":
return c.getString(R.string.all);
case "videos":
case "sepia_videos":
case "music_videos":
return c.getString(R.string.videos_string);
case "channels":
return c.getString(R.string.channels);
case "playlists":
case "music_playlists":
return c.getString(R.string.playlists);
case "tracks":
return c.getString(R.string.tracks);
case "users":
return c.getString(R.string.users);
case "conferences":
return c.getString(R.string.conferences);
case "events":
return c.getString(R.string.events);
case "music_songs":
return c.getString(R.string.songs);
case "music_albums":
return c.getString(R.string.albums);
case "music_artists":
return c.getString(R.string.artists);
default:
return filter;
}
}
/**
* Get a resource string with instructions for importing subscriptions for each service.
*
* @param serviceId service to get the instructions for
* @return the string resource containing the instructions or -1 if the service don't support it
*/
@StringRes
public static int getImportInstructions(final int serviceId) {
switch (serviceId) {
case 0:
return R.string.import_youtube_instructions;
case 1:
return R.string.import_soundcloud_instructions;
default:
return -1;
}
}
/**
* For services that support importing from a channel url, return a hint that will
* be used in the EditText that the user will type in his channel url.
*
* @param serviceId service to get the hint for
* @return the hint's string resource or -1 if the service don't support it
*/
@StringRes
public static int getImportInstructionsHint(final int serviceId) {
switch (serviceId) {
case 1:
return R.string.import_soundcloud_instructions_hint;
default:
return -1;
}
}
public static int getSelectedServiceId(final Context context) {
return Optional.ofNullable(getSelectedService(context))
.orElse(DEFAULT_FALLBACK_SERVICE)
.getServiceId();
}
@Nullable
public static StreamingService getSelectedService(final Context context) {
final String serviceName = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.current_service_key),
context.getString(R.string.default_service_value));
try {
return NewPipe.getService(serviceName);
} catch (final ExtractionException e) {
return null;
}
}
@NonNull
public static String getNameOfServiceById(final int serviceId) {
return ServiceList.all().stream()
.filter(s -> s.getServiceId() == serviceId)
.findFirst()
.map(StreamingService::getServiceInfo)
.map(StreamingService.ServiceInfo::getName)
.orElse("<unknown>");
}
/**
* @param serviceId the id of the service
* @return the service corresponding to the provided id
* @throws java.util.NoSuchElementException if there is no service with the provided id
*/
@NonNull
public static StreamingService getServiceById(final int serviceId) {
return ServiceList.all().stream()
.filter(s -> s.getServiceId() == serviceId)
.findFirst()
.orElseThrow();
}
public static void setSelectedServiceId(final Context context, final int serviceId) {
String serviceName;
try {
serviceName = NewPipe.getService(serviceId).getServiceInfo().getName();
} catch (final ExtractionException e) {
serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName();
}
setSelectedServicePreferences(context, serviceName);
}
private static void setSelectedServicePreferences(final Context context,
final String serviceName) {
PreferenceManager.getDefaultSharedPreferences(context).edit().
putString(context.getString(R.string.current_service_key), serviceName).apply();
}
public static long getCacheExpirationMillis(final int serviceId) {
if (serviceId == SoundCloud.getServiceId()) {
return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
} else {
return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
}
}
public static void initService(final Context context, final int serviceId) {
if (serviceId == ServiceList.PeerTube.getServiceId()) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String json = sharedPreferences.getString(context.getString(
R.string.peertube_selected_instance_key), null);
if (null == json) {
return;
}
final JsonObject jsonObject;
try {
jsonObject = JsonParser.object().from(json);
} catch (final JsonParserException e) {
return;
}
final String name = jsonObject.getString("name");
final String url = jsonObject.getString("url");
final PeertubeInstance instance = new PeertubeInstance(url, name);
ServiceList.PeerTube.setInstance(instance);
}
}
public static void initServices(final Context context) {
for (final StreamingService s : ServiceList.all()) {
initService(context, s.getServiceId());
}
}
}

View File

@ -1,168 +0,0 @@
/*
* SPDX-FileCopyrightText: 2018-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.grack.nanojson.JsonParser
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.StreamingService
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
import org.schabi.newpipe.ktx.getStringSafe
object ServiceHelper {
private val DEFAULT_FALLBACK_SERVICE: StreamingService = ServiceList.YouTube
@JvmStatic
@DrawableRes
fun getIcon(serviceId: Int): Int {
return when (serviceId) {
0 -> R.drawable.ic_smart_display
1 -> R.drawable.ic_cloud
2 -> R.drawable.ic_placeholder_media_ccc
3 -> R.drawable.ic_placeholder_peertube
4 -> R.drawable.ic_placeholder_bandcamp
else -> R.drawable.ic_circle
}
}
@JvmStatic
fun getTranslatedFilterString(filter: String, context: Context): String {
return when (filter) {
"all" -> context.getString(R.string.all)
"videos", "sepia_videos", "music_videos" -> context.getString(R.string.videos_string)
"channels" -> context.getString(R.string.channels)
"playlists", "music_playlists" -> context.getString(R.string.playlists)
"tracks" -> context.getString(R.string.tracks)
"users" -> context.getString(R.string.users)
"conferences" -> context.getString(R.string.conferences)
"events" -> context.getString(R.string.events)
"music_songs" -> context.getString(R.string.songs)
"music_albums" -> context.getString(R.string.albums)
"music_artists" -> context.getString(R.string.artists)
else -> filter
}
}
/**
* Get a resource string with instructions for importing subscriptions for each service.
*
* @param serviceId service to get the instructions for
* @return the string resource containing the instructions or -1 if the service don't support it
*/
@JvmStatic
@StringRes
fun getImportInstructions(serviceId: Int): Int {
return when (serviceId) {
0 -> R.string.import_youtube_instructions
1 -> R.string.import_soundcloud_instructions
else -> -1
}
}
/**
* For services that support importing from a channel url, return a hint that will
* be used in the EditText that the user will type in his channel url.
*
* @param serviceId service to get the hint for
* @return the hint's string resource or -1 if the service don't support it
*/
@JvmStatic
@StringRes
fun getImportInstructionsHint(serviceId: Int): Int {
return when (serviceId) {
1 -> R.string.import_soundcloud_instructions_hint
else -> -1
}
}
@JvmStatic
fun getSelectedServiceId(context: Context): Int {
return (getSelectedService(context) ?: DEFAULT_FALLBACK_SERVICE).serviceId
}
@JvmStatic
fun getSelectedService(context: Context): StreamingService? {
val serviceName: String = PreferenceManager.getDefaultSharedPreferences(context)
.getStringSafe(
context.getString(R.string.current_service_key),
context.getString(R.string.default_service_value)
)
return runCatching { NewPipe.getService(serviceName) }.getOrNull()
}
@JvmStatic
fun getNameOfServiceById(serviceId: Int): String {
return ServiceList.all().stream()
.filter { it.serviceId == serviceId }
.findFirst()
.map(StreamingService::getServiceInfo)
.map(StreamingService.ServiceInfo::getName)
.orElse("<unknown>")
}
/**
* @param serviceId the id of the service
* @return the service corresponding to the provided id
* @throws java.util.NoSuchElementException if there is no service with the provided id
*/
@JvmStatic
fun getServiceById(serviceId: Int): StreamingService {
return ServiceList.all().firstNotNullOf { it.takeIf { it.serviceId == serviceId } }
}
@JvmStatic
fun setSelectedServiceId(context: Context, serviceId: Int) {
val serviceName = runCatching { NewPipe.getService(serviceId).serviceInfo.name }
.getOrDefault(DEFAULT_FALLBACK_SERVICE.serviceInfo.name)
setSelectedServicePreferences(context, serviceName)
}
private fun setSelectedServicePreferences(context: Context, serviceName: String?) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
sharedPreferences.edit { putString(context.getString(R.string.current_service_key), serviceName) }
}
@JvmStatic
fun getCacheExpirationMillis(serviceId: Int): Long {
return if (serviceId == ServiceList.SoundCloud.serviceId) {
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)
} else {
TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
}
}
fun initService(context: Context, serviceId: Int) {
if (serviceId == ServiceList.PeerTube.serviceId) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val json = sharedPreferences.getString(
context.getString(R.string.peertube_selected_instance_key),
null
) ?: return
val jsonObject = runCatching { JsonParser.`object`().from(json) }
.getOrElse { return@initService }
ServiceList.PeerTube.instance = PeertubeInstance(
jsonObject.getString("url"),
jsonObject.getString("name")
)
}
}
@JvmStatic
fun initServices(context: Context) {
ServiceList.all().forEach { initService(context, it.serviceId) }
}
}

View File

@ -0,0 +1,50 @@
package org.schabi.newpipe.util;
import org.schabi.newpipe.extractor.stream.StreamType;
/**
* Utility class for {@link StreamType}.
*/
public final class StreamTypeUtil {
private StreamTypeUtil() {
// No impl pls
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#AUDIO_STREAM},
* {@link StreamType#AUDIO_LIVE_STREAM} or {@link StreamType#POST_LIVE_AUDIO_STREAM}
*/
public static boolean isAudio(final StreamType streamType) {
return streamType == StreamType.AUDIO_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM
|| streamType == StreamType.POST_LIVE_AUDIO_STREAM;
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#VIDEO_STREAM},
* {@link StreamType#LIVE_STREAM} or {@link StreamType#POST_LIVE_STREAM}
*/
public static boolean isVideo(final StreamType streamType) {
return streamType == StreamType.VIDEO_STREAM
|| streamType == StreamType.LIVE_STREAM
|| streamType == StreamType.POST_LIVE_STREAM;
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#LIVE_STREAM} or
* {@link StreamType#AUDIO_LIVE_STREAM}
*/
public static boolean isLiveStream(final StreamType streamType) {
return streamType == StreamType.LIVE_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM;
}
}

View File

@ -1,54 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import org.schabi.newpipe.extractor.stream.StreamType
/**
* Utility class for [StreamType].
*/
object StreamTypeUtil {
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.AUDIO_STREAM],
* [StreamType.AUDIO_LIVE_STREAM] or [StreamType.POST_LIVE_AUDIO_STREAM]
*/
@JvmStatic
fun isAudio(streamType: StreamType): Boolean {
return streamType == StreamType.AUDIO_STREAM ||
streamType == StreamType.AUDIO_LIVE_STREAM ||
streamType == StreamType.POST_LIVE_AUDIO_STREAM
}
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.VIDEO_STREAM],
* [StreamType.LIVE_STREAM] or [StreamType.POST_LIVE_STREAM]
*/
@JvmStatic
fun isVideo(streamType: StreamType): Boolean {
return streamType == StreamType.VIDEO_STREAM ||
streamType == StreamType.LIVE_STREAM ||
streamType == StreamType.POST_LIVE_STREAM
}
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.LIVE_STREAM] or
* [StreamType.AUDIO_LIVE_STREAM]
*/
@JvmStatic
fun isLiveStream(streamType: StreamType): Boolean {
return streamType == StreamType.LIVE_STREAM ||
streamType == StreamType.AUDIO_LIVE_STREAM
}
}

View File

@ -1,7 +1,6 @@
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;
@ -10,7 +9,6 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
@ -27,15 +25,12 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.nio.file.Files;
import java.util.Collections;
import java.io.File;
import java.io.FileOutputStream;
import java.util.List;
import coil3.SingletonImageLoader;
import coil3.disk.DiskCache;
import coil3.memory.MemoryCache;
public final class ShareUtils {
private static final String TAG = ShareUtils.class.getSimpleName();
@ -278,7 +273,7 @@ public final class ShareUtils {
* @param content the content to share
* @param images a set of possible {@link Image}s of the subject, among which to choose with
* {@link ImageStrategy#choosePreferredImage(List)} since that's likely to
* provide an image that is in Coil's cache
* provide an image that is in Picasso's cache
*/
public static void shareText(@NonNull final Context context,
@NonNull final String title,
@ -339,9 +334,11 @@ public final class ShareUtils {
*
* <p>
* In order not to worry about network issues (timeouts, DNS issues, low connection speed, ...)
* when sharing a content, only images in the {@link MemoryCache} or {@link DiskCache}
* used by the Coil library are used as preview images. If the thumbnail image is not in the
* cache, no {@link ClipData} will be generated and {@code null} will be returned.
* when sharing a content, only images in the {@link com.squareup.picasso.LruCache LruCache}
* used by the Picasso library inside {@link PicassoHelper} are used as preview images. If the
* thumbnail image is not in the cache, no {@link ClipData} will be generated and {@code null}
* will be returned.
* </p>
*
* <p>
* In order to display the image in the content preview of the Android share sheet, an URI of
@ -357,6 +354,12 @@ public final class ShareUtils {
* </p>
*
* <p>
* This method will call {@link PicassoHelper#getImageFromCacheIfPresent(String)} to get the
* thumbnail of the content in the {@link com.squareup.picasso.LruCache LruCache} used by
* the Picasso library inside {@link PicassoHelper}.
* </p>
*
* <p>
* Using the result of this method when sharing has only an effect on the system share sheet (if
* OEMs didn't change Android system standard behavior) on Android API 29 and higher.
* </p>
@ -370,46 +373,33 @@ public final class ShareUtils {
@NonNull final Context context,
@NonNull final String thumbnailUrl) {
try {
// 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 = SingletonImageLoader.get(context);
final var value = loader.getMemoryCache()
.get(new MemoryCache.Key(thumbnailUrl, Collections.emptyMap()));
final Bitmap cachedBitmap;
if (value != null) {
cachedBitmap = toBitmap(value.getImage());
} else {
try (var snapshot = loader.getDiskCache().openSnapshot(thumbnailUrl)) {
if (snapshot != null) {
cachedBitmap = BitmapFactory.decodeFile(snapshot.getData().toString());
} else {
cachedBitmap = null;
}
}
}
if (cachedBitmap == null) {
final Bitmap bitmap = PicassoHelper.getImageFromCacheIfPresent(thumbnailUrl);
if (bitmap == null) {
return null;
}
final var path = applicationContext.getCacheDir().toPath()
.resolve("android_share_sheet_image_preview.jpg");
// Any existing file will be overwritten
try (var outputStream = Files.newOutputStream(path)) {
cachedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
}
// 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 String appFolder = applicationContext.getCacheDir().getAbsolutePath();
final File thumbnailPreviewFile = new File(appFolder
+ "/android_share_sheet_image_preview.jpg");
// Any existing file will be overwritten with FileOutputStream
final FileOutputStream fileOutputStream = new FileOutputStream(thumbnailPreviewFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream);
fileOutputStream.close();
final ClipData clipData = ClipData.newUri(applicationContext.getContentResolver(), "",
FileProvider.getUriForFile(applicationContext,
BuildConfig.APPLICATION_ID + ".provider",
path.toFile()));
FileProvider.getUriForFile(applicationContext,
BuildConfig.APPLICATION_ID + ".provider",
thumbnailPreviewFile));
if (DEBUG) {
Log.d(TAG, "ClipData successfully generated for Android share sheet: " + clipData);
}
return clipData;
} catch (final Exception e) {
Log.w(TAG, "Error when setting preview image for share sheet", e);
return null;

View File

@ -1,185 +0,0 @@
package org.schabi.newpipe.util.image
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import android.widget.ImageView
import androidx.annotation.DrawableRes
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 kotlin.math.min
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Image
import org.schabi.newpipe.ktx.scale
object CoilHelper {
private val TAG = CoilHelper::class.java.simpleName
@JvmOverloads
fun loadBitmapBlocking(
context: Context,
url: String?,
@DrawableRes placeholderResId: Int = 0
): Bitmap? = context.imageLoader
.executeBlocking(getImageRequest(context, url, placeholderResId).build())
.image
?.toBitmap()
fun loadAvatar(
target: ImageView,
images: List<Image>
) {
loadImageDefault(target, images, R.drawable.placeholder_person)
}
fun loadAvatar(
target: ImageView,
url: String?
) {
loadImageDefault(target, url, R.drawable.placeholder_person)
}
fun loadThumbnail(
target: ImageView,
images: List<Image>
) {
loadImageDefault(target, images, R.drawable.placeholder_thumbnail_video)
}
fun loadThumbnail(
target: ImageView,
url: String?
) {
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video)
}
fun loadScaledDownThumbnail(
context: Context,
images: List<Image>,
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"
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()
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 context.imageLoader.enqueue(request)
}
fun loadDetailsThumbnail(
target: ImageView,
images: List<Image>
) {
val url = ImageStrategy.choosePreferredImage(images)
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_video, false)
}
fun loadBanner(
target: ImageView,
images: List<Image>
) {
loadImageDefault(target, images, R.drawable.placeholder_channel_banner)
}
fun loadPlaylistThumbnail(
target: ImageView,
images: List<Image>
) {
loadImageDefault(target, images, R.drawable.placeholder_thumbnail_playlist)
}
fun loadPlaylistThumbnail(
target: ImageView,
url: String?
) {
loadImageDefault(target, url, R.drawable.placeholder_thumbnail_playlist)
}
private fun loadImageDefault(
target: ImageView,
images: List<Image>,
@DrawableRes placeholderResId: Int
) {
loadImageDefault(target, ImageStrategy.choosePreferredImage(images), placeholderResId)
}
private fun loadImageDefault(
target: ImageView,
url: String?,
@DrawableRes placeholderResId: Int,
showPlaceholder: Boolean = true
) {
val request =
getImageRequest(target.context, url, placeholderResId, showPlaceholder)
.target(target)
.build()
target.context.imageLoader.enqueue(request)
}
private fun getImageRequest(
context: Context,
url: String?,
@DrawableRes placeholderResId: Int,
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)
.data(takenUrl)
.error(placeholderResId)
.memoryCacheKey(takenUrl)
.diskCacheKey(takenUrl)
.apply {
if (takenUrl != null || showPlaceholderWhileLoading) {
placeholder(placeholderResId)
}
}
}
}

View File

@ -0,0 +1,224 @@
package org.schabi.newpipe.util.image;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.util.image.ImageStrategy.choosePreferredImage;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.BitmapCompat;
import com.squareup.picasso.Cache;
import com.squareup.picasso.LruCache;
import com.squareup.picasso.OkHttp3Downloader;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.RequestCreator;
import com.squareup.picasso.Transformation;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.Image;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
public final class PicassoHelper {
private static final String TAG = PicassoHelper.class.getSimpleName();
private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY =
"PICASSO_PLAYER_THUMBNAIL_TRANSFORMATION_KEY";
private PicassoHelper() {
}
private static Cache picassoCache;
private static OkHttpClient picassoDownloaderClient;
// suppress because terminate() is called in App.onTerminate(), preventing leaks
@SuppressLint("StaticFieldLeak")
private static Picasso picassoInstance;
public static void init(final Context context) {
picassoCache = new LruCache(10 * 1024 * 1024);
picassoDownloaderClient = new OkHttpClient.Builder()
.cache(new okhttp3.Cache(new File(context.getExternalCacheDir(), "picasso"),
50L * 1024L * 1024L))
// this should already be the default timeout in OkHttp3, but just to be sure...
.callTimeout(15, TimeUnit.SECONDS)
.build();
picassoInstance = new Picasso.Builder(context)
.memoryCache(picassoCache) // memory cache
.downloader(new OkHttp3Downloader(picassoDownloaderClient)) // disk cache
.defaultBitmapConfig(Bitmap.Config.RGB_565)
.build();
}
public static void terminate() {
picassoCache = null;
picassoDownloaderClient = null;
if (picassoInstance != null) {
picassoInstance.shutdown();
picassoInstance = null;
}
}
public static void clearCache(final Context context) throws IOException {
picassoInstance.shutdown();
picassoCache.clear(); // clear memory cache
final okhttp3.Cache diskCache = picassoDownloaderClient.cache();
if (diskCache != null) {
diskCache.delete(); // clear disk cache
}
init(context);
}
public static void cancelTag(final Object tag) {
picassoInstance.cancelTag(tag);
}
public static void setIndicatorsEnabled(final boolean enabled) {
picassoInstance.setIndicatorsEnabled(enabled); // useful for debugging
}
public static RequestCreator loadAvatar(@NonNull final List<Image> images) {
return loadImageDefault(images, R.drawable.placeholder_person);
}
public static RequestCreator loadAvatar(@Nullable final String url) {
return loadImageDefault(url, R.drawable.placeholder_person);
}
public static RequestCreator loadThumbnail(@NonNull final List<Image> images) {
return loadImageDefault(images, R.drawable.placeholder_thumbnail_video);
}
public static RequestCreator loadThumbnail(@Nullable final String url) {
return loadImageDefault(url, R.drawable.placeholder_thumbnail_video);
}
public static RequestCreator loadDetailsThumbnail(@NonNull final List<Image> images) {
return loadImageDefault(choosePreferredImage(images),
R.drawable.placeholder_thumbnail_video, false);
}
public static RequestCreator loadBanner(@NonNull final List<Image> images) {
return loadImageDefault(images, R.drawable.placeholder_channel_banner);
}
public static RequestCreator loadPlaylistThumbnail(@NonNull final List<Image> images) {
return loadImageDefault(images, R.drawable.placeholder_thumbnail_playlist);
}
public static RequestCreator loadPlaylistThumbnail(@Nullable final String url) {
return loadImageDefault(url, R.drawable.placeholder_thumbnail_playlist);
}
public static RequestCreator loadSeekbarThumbnailPreview(@Nullable final String url) {
return picassoInstance.load(url);
}
public static RequestCreator loadNotificationIcon(@Nullable final String url) {
return loadImageDefault(url, R.drawable.ic_newpipe_triangle_white);
}
public static RequestCreator loadScaledDownThumbnail(final Context context,
@NonNull final List<Image> images) {
// scale down the notification thumbnail for performance
return PicassoHelper.loadThumbnail(images)
.transform(new Transformation() {
@Override
public Bitmap transform(final Bitmap source) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - transform() called");
}
final float notificationThumbnailWidth = Math.min(
context.getResources()
.getDimension(R.dimen.player_notification_thumbnail_width),
source.getWidth());
final Bitmap result = BitmapCompat.createScaledBitmap(
source,
(int) notificationThumbnailWidth,
(int) (source.getHeight()
/ (source.getWidth() / notificationThumbnailWidth)),
null,
true);
if (result == source || !result.isMutable()) {
// create a new mutable bitmap to prevent strange crashes on some
// devices (see #4638)
final Bitmap copied = BitmapCompat.createScaledBitmap(
source,
(int) notificationThumbnailWidth - 1,
(int) (source.getHeight() / (source.getWidth()
/ (notificationThumbnailWidth - 1))),
null,
true);
source.recycle();
return copied;
} else {
source.recycle();
return result;
}
}
@Override
public String key() {
return PLAYER_THUMBNAIL_TRANSFORMATION_KEY;
}
});
}
@Nullable
public static Bitmap getImageFromCacheIfPresent(@NonNull final String imageUrl) {
// URLs in the internal cache finish with \n so we need to add \n to image URLs
return picassoCache.get(imageUrl + "\n");
}
private static RequestCreator loadImageDefault(@NonNull final List<Image> images,
@DrawableRes final int placeholderResId) {
return loadImageDefault(choosePreferredImage(images), placeholderResId);
}
private static RequestCreator loadImageDefault(@Nullable final String url,
@DrawableRes final int placeholderResId) {
return loadImageDefault(url, placeholderResId, true);
}
private static RequestCreator loadImageDefault(@Nullable final String url,
@DrawableRes final int placeholderResId,
final boolean showPlaceholderWhileLoading) {
// 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)
if (isNullOrEmpty(url) || !ImageStrategy.shouldLoadImages()) {
return picassoInstance
.load((String) null)
.placeholder(placeholderResId) // show placeholder when no image should load
.error(placeholderResId);
} else {
final RequestCreator requestCreator = picassoInstance
.load(url)
.error(placeholderResId);
if (showPlaceholderWhileLoading) {
requestCreator.placeholder(placeholderResId);
}
return requestCreator;
}
}
}

Some files were not shown because too many files have changed in this diff Show More