Migrate from Picasso to Coil

Based on changes from refactor

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta 2026-02-21 16:38:35 +08:00
parent 273b09a3e8
commit 57364109f4
35 changed files with 432 additions and 512 deletions

View File

@ -272,7 +272,8 @@ dependencies {
implementation(libs.lisawray.groupie.viewbinding) implementation(libs.lisawray.groupie.viewbinding)
// Image loading // Image loading
implementation(libs.squareup.picasso) implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
// Markdown library for Android // Markdown library for Android
implementation(libs.noties.markwon.core) implementation(libs.noties.markwon.core)

View File

@ -1,11 +1,19 @@
package org.schabi.newpipe package org.schabi.newpipe
import android.app.ActivityManager
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager 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 com.jakewharton.processphoenix.ProcessPhoenix
import io.reactivex.rxjava3.exceptions.CompositeException import io.reactivex.rxjava3.exceptions.CompositeException
import io.reactivex.rxjava3.exceptions.MissingBackpressureException import io.reactivex.rxjava3.exceptions.MissingBackpressureException
@ -29,9 +37,8 @@ import org.schabi.newpipe.util.BridgeStateSaverInitializer
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ServiceHelper import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.StateSaver import org.schabi.newpipe.util.StateSaver
import org.schabi.newpipe.util.image.ImageStrategy.setPreferredImageQuality 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.image.PreferredImageQuality.Companion.fromPreferenceKey
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl import org.schabi.newpipe.util.potoken.PoTokenProviderImpl
/* /*
@ -51,7 +58,9 @@ import org.schabi.newpipe.util.potoken.PoTokenProviderImpl
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>. * along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/ */
open class App : Application() { open class App :
Application(),
SingletonImageLoader.Factory {
var isFirstRun = false var isFirstRun = false
private set private set
var notificationsRequested = false var notificationsRequested = false
@ -102,9 +111,8 @@ open class App : Application() {
// Initialize image loader // Initialize image loader
val prefs = PreferenceManager.getDefaultSharedPreferences(this) val prefs = PreferenceManager.getDefaultSharedPreferences(this)
PicassoHelper.init(this) ImageStrategy.setPreferredImageQuality(
setPreferredImageQuality( PreferredImageQuality.fromPreferenceKey(
fromPreferenceKey(
this, this,
prefs.getString( prefs.getString(
getString(R.string.image_quality_key), getString(R.string.image_quality_key),
@ -112,20 +120,20 @@ open class App : Application() {
) )
) )
) )
PicassoHelper.setIndicatorsEnabled(
MainActivity.DEBUG &&
prefs.getBoolean(getString(R.string.show_image_indicators_key), false)
)
configureRxJavaErrorHandler() configureRxJavaErrorHandler()
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl) YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
} }
override fun onTerminate() { override fun newImageLoader(context: Context): ImageLoader = ImageLoader
super.onTerminate() .Builder(this)
PicassoHelper.terminate() .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 { protected open fun getDownloader(): Downloader {
val downloader = DownloaderImpl.init(null) val downloader = DownloaderImpl.init(null)

View File

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

View File

@ -207,10 +207,10 @@ class AboutActivity : AppCompatActivity() {
StandardLicenses.APACHE2 StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"Picasso", "Coil",
"2013", "2023",
"Square, Inc.", "Coil Contributors",
"https://square.github.io/picasso/", "https://coil-kt.github.io/coil/",
StandardLicenses.APACHE2 StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(

View File

@ -118,7 +118,7 @@ import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.image.CoilHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
@ -129,6 +129,7 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
@ -160,8 +161,6 @@ public final class VideoDetailFragment
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB"; private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
private static final String EMPTY_TAB_TAG = "EMPTY TAB"; private static final String EMPTY_TAB_TAG = "EMPTY TAB";
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
// tabs // tabs
private boolean showComments; private boolean showComments;
private boolean showRelatedItems; private boolean showRelatedItems;
@ -1499,7 +1498,10 @@ public final class VideoDetailFragment
} }
} }
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG); CoilUtils.dispose(binding.detailThumbnailImageView);
CoilUtils.dispose(binding.detailSubChannelThumbnailView);
CoilUtils.dispose(binding.overlayThumbnail);
CoilUtils.dispose(binding.detailUploaderThumbnailView);
binding.detailThumbnailImageView.setImageBitmap(null); binding.detailThumbnailImageView.setImageBitmap(null);
binding.detailSubChannelThumbnailView.setImageBitmap(null); binding.detailSubChannelThumbnailView.setImageBitmap(null);
} }
@ -1590,8 +1592,8 @@ public final class VideoDetailFragment
binding.detailSecondaryControlPanel.setVisibility(View.GONE); binding.detailSecondaryControlPanel.setVisibility(View.GONE);
checkUpdateProgressInfo(info); checkUpdateProgressInfo(info);
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG) CoilHelper.INSTANCE.loadDetailsThumbnail(binding.detailThumbnailImageView,
.into(binding.detailThumbnailImageView); info.getThumbnails());
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView, showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables); binding.detailMetaInfoSeparator, disposables);
@ -1641,8 +1643,8 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE); binding.detailUploaderTextView.setVisibility(View.GONE);
} }
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG) CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
.into(binding.detailSubChannelThumbnailView); info.getUploaderAvatars());
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE); binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
binding.detailUploaderThumbnailView.setVisibility(View.GONE); binding.detailUploaderThumbnailView.setVisibility(View.GONE);
} }
@ -1673,11 +1675,11 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE); binding.detailUploaderTextView.setVisibility(View.GONE);
} }
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG) CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
.into(binding.detailSubChannelThumbnailView); info.getSubChannelAvatars());
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE); binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG) CoilHelper.INSTANCE.loadAvatar(binding.detailUploaderThumbnailView,
.into(binding.detailUploaderThumbnailView); info.getUploaderAvatars());
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE); binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
} }
@ -2435,8 +2437,7 @@ public final class VideoDetailFragment
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle); binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader); binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageDrawable(null); binding.overlayThumbnail.setImageDrawable(null);
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG) CoilHelper.INSTANCE.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails);
.into(binding.overlayThumbnail);
} }
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) { private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {

View File

@ -53,13 +53,14 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils; 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.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -73,7 +74,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
implements StateSaver.WriteRead { implements StateSaver.WriteRead {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100; private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
@State @State
protected int serviceId = Constants.NO_SERVICE_ID; protected int serviceId = Constants.NO_SERVICE_ID;
@ -578,7 +578,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override @Override
public void showLoading() { public void showLoading() {
super.showLoading(); super.showLoading();
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG); CoilUtils.dispose(binding.channelAvatarView);
CoilUtils.dispose(binding.channelBannerImage);
CoilUtils.dispose(binding.subChannelAvatarView);
animate(binding.channelSubscribeButton, false, 100); animate(binding.channelSubscribeButton, false, 100);
} }
@ -589,17 +591,15 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName()); setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) { if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG) CoilHelper.INSTANCE.loadBanner(binding.channelBannerImage, result.getBanners());
.into(binding.channelBannerImage);
} else { } else {
// do not waste space for the banner, if the user disabled images or there is not one // do not waste space for the banner, if the user disabled images or there is not one
binding.channelBannerImage.setImageDrawable(null); binding.channelBannerImage.setImageDrawable(null);
} }
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG) CoilHelper.INSTANCE.loadAvatar(binding.channelAvatarView, result.getAvatars());
.into(binding.channelAvatarView); CoilHelper.INSTANCE.loadAvatar(binding.subChannelAvatarView,
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG) result.getParentChannelAvatars());
.into(binding.subChannelAvatarView);
binding.channelTitleView.setText(result.getName()); binding.channelTitleView.setText(result.getName());
binding.channelSubscriberView.setVisibility(View.VISIBLE); 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.ExtractorHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; 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.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextLinkifier; import org.schabi.newpipe.util.text.TextLinkifier;
import org.schabi.newpipe.util.text.LongPressLinkMovementMethod; import org.schabi.newpipe.util.text.LongPressLinkMovementMethod;
@ -84,7 +84,7 @@ public final class CommentRepliesFragment
final CommentsInfoItem item = commentsInfoItem; final CommentsInfoItem item = commentsInfoItem;
// load the author avatar // load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar); CoilHelper.INSTANCE.loadAvatar(binding.authorAvatar, item.getUploaderAvatars());
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages() binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
? View.VISIBLE : View.GONE); ? 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.NavigationHelper;
import org.schabi.newpipe.util.PlayButtonHelper; import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.text.TextEllipsizer; import org.schabi.newpipe.util.text.TextEllipsizer;
import java.util.ArrayList; import java.util.ArrayList;
@ -62,6 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
@ -71,8 +72,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo> public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
implements PlaylistControlViewHolder { implements PlaylistControlViewHolder {
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
private CompositeDisposable disposables; private CompositeDisposable disposables;
private Subscription bookmarkReactor; private Subscription bookmarkReactor;
private AtomicBoolean isBookmarkButtonReady; private AtomicBoolean isBookmarkButtonReady;
@ -271,7 +270,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
animate(headerBinding.getRoot(), false, 200); animate(headerBinding.getRoot(), false, 200);
animateHideRecyclerViewAllowingScrolling(itemsList); animateHideRecyclerViewAllowingScrolling(itemsList);
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG); CoilUtils.dispose(headerBinding.uploaderAvatarView);
animate(headerBinding.uploaderLayout, false, 200); animate(headerBinding.uploaderLayout, false, 200);
} }
@ -322,8 +321,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
R.drawable.ic_radio) R.drawable.ic_radio)
); );
} else { } else {
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG) CoilHelper.INSTANCE.loadAvatar(headerBinding.uploaderAvatarView,
.into(headerBinding.uploaderAvatarView); result.getUploaderAvatars());
} }
streamCount = result.getStreamCount(); streamCount = result.getStreamCount();

View File

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

View File

@ -13,8 +13,8 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.utils.Utils; import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
public class ChannelMiniInfoItemHolder extends InfoItemHolder { public class ChannelMiniInfoItemHolder extends InfoItemHolder {
private final ImageView itemThumbnailView; private final ImageView itemThumbnailView;
@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemAdditionalDetailView.setText(getDetailLine(item)); itemAdditionalDetailView.setText(getDetailLine(item));
} }
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView); CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) { 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.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils; 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.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextEllipsizer; import org.schabi.newpipe.util.text.TextEllipsizer;
public class CommentInfoItemHolder extends InfoItemHolder { public class CommentInfoItemHolder extends InfoItemHolder {
@ -82,14 +82,12 @@ public class CommentInfoItemHolder extends InfoItemHolder {
@Override @Override
public void updateFromItem(final InfoItem infoItem, public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) { final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) { if (!(infoItem instanceof CommentsInfoItem item)) {
return; return;
} }
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
// load the author avatar // load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView); CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getUploaderAvatars());
if (ImageStrategy.shouldLoadImages()) { if (ImageStrategy.shouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE); itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding, 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.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder { public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView; public final ImageView itemThumbnailView;
@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount())); .localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setText(item.getUploaderName()); itemUploaderView.setText(item.getUploaderName());
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView); CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) { 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.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper; import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
import java.util.concurrent.TimeUnit; 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 // Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView); CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) { if (itemBuilder.getOnStreamSelectedListener() != null) {

View File

@ -0,0 +1,13 @@
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

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

View File

@ -6,7 +6,6 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
@ -17,20 +16,17 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.image.PicassoHelper import org.schabi.newpipe.util.image.CoilHelper
/** /**
* Helper for everything related to show notifications about new streams to the user. * Helper for everything related to show notifications about new streams to the user.
*/ */
class NotificationHelper(val context: Context) { class NotificationHelper(val context: Context) {
private val manager = NotificationManagerCompat.from(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 * Show notifications for new streams from a single channel. The individual notifications are
@ -71,67 +67,34 @@ class NotificationHelper(val context: Context) {
summaryBuilder.setStyle(style) summaryBuilder.setStyle(style)
// open the channel page when clicking on the summary notification // 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( summaryBuilder.setContentIntent(
PendingIntentCompat.getActivity( PendingIntentCompat.getActivity(context, data.pseudoId, intent, 0, false)
context,
data.pseudoId,
NavigationHelper
.getChannelIntent(context, data.serviceId, data.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
0,
false
)
) )
// a Target is like a listener for image loading events val avatarIcon =
val target = object : Target { CoilHelper.loadBitmapBlocking(context, data.avatarUrl, R.drawable.ic_newpipe_triangle_white)
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) { summaryBuilder.setLargeIcon(avatarIcon)
// 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 // Show individual stream notifications, set channel icon only if there is actually one
// one showStreamNotifications(newStreams, data.serviceId, avatarIcon)
showStreamNotifications(newStreams, data.serviceId, data.url, bitmap) // Show summary notification
// Show summary notification if enabled if (manager.areNotificationsEnabled()) {
if (manager.areNotificationsEnabled()) { manager.notify(data.pseudoId, summaryBuilder.build())
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 if enabled
if (manager.areNotificationsEnabled()) {
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( private fun showStreamNotifications(
newStreams: List<StreamInfoItem>, newStreams: List<StreamInfoItem>,
serviceId: Int, serviceId: Int,
channelUrl: String,
channelIcon: Bitmap? channelIcon: Bitmap?
) { ) {
if (manager.areNotificationsEnabled()) { if (manager.areNotificationsEnabled()) {
newStreams.forEach { stream -> newStreams.forEach { stream ->
val notification = val notification =
createStreamNotification(stream, serviceId, channelUrl, channelIcon) createStreamNotification(stream, serviceId, channelIcon)
manager.notify(stream.url.hashCode(), notification) manager.notify(stream.url.hashCode(), notification)
} }
} }
@ -140,7 +103,6 @@ class NotificationHelper(val context: Context) {
private fun createStreamNotification( private fun createStreamNotification(
item: StreamInfoItem, item: StreamInfoItem,
serviceId: Int, serviceId: Int,
channelUrl: String,
channelIcon: Bitmap? channelIcon: Bitmap?
): Notification { ): Notification {
return NotificationCompat.Builder( return NotificationCompat.Builder(
@ -151,7 +113,7 @@ class NotificationHelper(val context: Context) {
.setLargeIcon(channelIcon) .setLargeIcon(channelIcon)
.setContentTitle(item.name) .setContentTitle(item.name)
.setContentText(item.uploaderName) .setContentText(item.uploaderName)
.setGroup(channelUrl) .setGroup(item.uploaderUrl)
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background)) .setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
.setColorized(true) .setColorized(true)
.setAutoCancel(true) .setAutoCancel(true)

View File

@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; 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.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@ -30,17 +30,16 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
public void updateFromItem(final LocalItem localItem, public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager, final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) { final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistMetadataEntry)) { if (!(localItem instanceof PlaylistMetadataEntry item)) {
return; return;
} }
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
itemTitleView.setText(item.getOrderingName()); itemTitleView.setText(item.getOrderingName());
itemStreamCountView.setText(Localization.localizeStreamCountMini( itemStreamCountView.setText(Localization.localizeStreamCountMini(
itemStreamCountView.getContext(), item.getStreamCount())); itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setVisibility(View.INVISIBLE); itemUploaderView.setVisibility(View.INVISIBLE);
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
if (item instanceof PlaylistDuplicatesEntry if (item instanceof PlaylistDuplicatesEntry
&& ((PlaylistDuplicatesEntry) item).getTimesStreamIsContained() > 0) { && ((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.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper; import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter; 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 // Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl()) CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
.into(itemThumbnailView); item.getStreamEntity().getThumbnailUrl());
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) { 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.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper; import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter; 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 // Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl()) CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
.into(itemThumbnailView); item.getStreamEntity().getThumbnailUrl());
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) { 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.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@ -29,10 +29,9 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
public void updateFromItem(final LocalItem localItem, public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager, final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) { final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistRemoteEntity)) { if (!(localItem instanceof PlaylistRemoteEntity item)) {
return; return;
} }
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
itemTitleView.setText(item.getOrderingName()); itemTitleView.setText(item.getOrderingName());
itemStreamCountView.setText(Localization.localizeStreamCountMini( itemStreamCountView.setText(Localization.localizeStreamCountMini(
@ -45,7 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId())); itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
} }
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
} }

View File

@ -9,7 +9,7 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.channel.ChannelInfoItem import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.image.PicassoHelper import org.schabi.newpipe.util.image.CoilHelper
class ChannelItem( class ChannelItem(
private val infoItem: ChannelInfoItem, private val infoItem: ChannelInfoItem,
@ -39,7 +39,7 @@ class ChannelItem(
itemChannelDescriptionView.text = infoItem.description itemChannelDescriptionView.text = infoItem.description
} }
PicassoHelper.loadAvatar(infoItem.thumbnails).into(itemThumbnailView) CoilHelper.loadAvatar(itemThumbnailView, infoItem.thumbnails)
gesturesListener?.run { gesturesListener?.run {
viewHolder.root.setOnClickListener { selected(infoItem) } 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.databinding.PickerSubscriptionItemBinding
import org.schabi.newpipe.ktx.AnimationType import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.util.image.PicassoHelper import org.schabi.newpipe.util.image.CoilHelper
data class PickerSubscriptionItem( data class PickerSubscriptionItem(
val subscriptionEntity: SubscriptionEntity, val subscriptionEntity: SubscriptionEntity,
@ -21,7 +21,7 @@ data class PickerSubscriptionItem(
override fun getSpanSize(spanCount: Int, position: Int): Int = 1 override fun getSpanSize(spanCount: Int, position: Int): Int = 1
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) { override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView) CoilHelper.loadAvatar(viewBinding.thumbnailView, subscriptionEntity.avatarUrl)
viewBinding.titleView.text = subscriptionEntity.name viewBinding.titleView.text = subscriptionEntity.name
viewBinding.selectedHighlight.isVisible = isSelected viewBinding.selectedHighlight.isVisible = isSelected
} }

View File

@ -46,13 +46,14 @@ import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static coil3.Image_androidKt.toBitmap;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.media.AudioManager; import android.media.AudioManager;
import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log; import android.util.Log;
@ -80,8 +81,6 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.video.VideoSize; 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.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -126,13 +125,14 @@ import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.image.CoilHelper;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import coil3.target.Target;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
@ -181,7 +181,6 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public static final int RENDERER_UNAVAILABLE = -1; public static final int RENDERER_UNAVAILABLE = -1;
private static final String PICASSO_PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Playback // Playback
@ -200,6 +199,8 @@ public final class Player implements PlaybackListener, Listener {
private MediaItemTag currentMetadata; private MediaItemTag currentMetadata;
@Nullable @Nullable
private Bitmap currentThumbnail; private Bitmap currentThumbnail;
@Nullable
private coil3.request.Disposable thumbnailDisposable;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Player // Player
@ -255,12 +256,6 @@ public final class Player implements PlaybackListener, Listener {
@NonNull @NonNull
private final CompositeDisposable streamItemDisposable = new CompositeDisposable(); 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 // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -314,8 +309,6 @@ public final class Player implements PlaybackListener, Listener {
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
audioResolver = new AudioPlaybackResolver(context, dataSource); audioResolver = new AudioPlaybackResolver(context, dataSource);
currentThumbnailTarget = getCurrentThumbnailTarget();
// The UIs added here should always be present. They will be initialized when the player // 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 // 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 // notification ui in the UIs list, since the notification depends on the media session in
@ -704,7 +697,6 @@ public final class Player implements PlaybackListener, Listener {
databaseUpdateDisposable.clear(); databaseUpdateDisposable.clear();
progressUpdateDisposable.set(null); progressUpdateDisposable.set(null);
streamItemDisposable.clear(); streamItemDisposable.clear();
cancelLoadingCurrentThumbnail();
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
} }
@ -884,67 +876,58 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region Thumbnail loading //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) { private void loadCurrentThumbnail(final List<Image> thumbnails) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with thumbnails = [" Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with thumbnails = ["
+ thumbnails.size() + "]"); + thumbnails.size() + "]");
} }
// first cancel any previous loading // Cancel any ongoing image loading
cancelLoadingCurrentThumbnail(); if (thumbnailDisposable != null) {
thumbnailDisposable.dispose();
}
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media // 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 Picasso. // session metadata while the new thumbnail is being loaded by Coil.
onThumbnailLoaded(null); onThumbnailLoaded(null);
if (thumbnails.isEmpty()) { if (thumbnails.isEmpty()) {
return; return;
} }
// scale down the notification thumbnail for performance // scale down the notification thumbnail for performance
PicassoHelper.loadScaledDownThumbnail(context, thumbnails) final var thumbnailTarget = new Target() {
.tag(PICASSO_PLAYER_THUMBNAIL_TAG) @Override
.into(currentThumbnailTarget); 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);
} }
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) { private void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
// Avoid useless thumbnail updates, if the thumbnail has not actually changed. Based on the // 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 // 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 Picasso's target. // onThumbnailLoaded won't be called twice with the same nonnull bitmap by Coil's target.
if (currentThumbnail != bitmap) { if (currentThumbnail != bitmap) {
currentThumbnail = bitmap; currentThumbnail = bitmap;
UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap)); UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap));

View File

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

View File

@ -13,8 +13,9 @@ import androidx.collection.SparseArrayCompat;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
import org.schabi.newpipe.App;
import org.schabi.newpipe.extractor.stream.Frameset; import org.schabi.newpipe.extractor.stream.Frameset;
import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.image.CoilHelper;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
@ -207,8 +208,8 @@ public class SeekbarPreviewThumbnailHolder {
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'"); Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient // Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
// Ensure that your are not running on the main-Thread this will otherwise hang // Ensure that you are not running on the main thread, otherwise this will hang
final Bitmap bitmap = PicassoHelper.loadSeekbarThumbnailPreview(url).get(); final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getInstance(), url);
if (sw != null) { if (sw != null) {
Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took " Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took "

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,185 @@
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

@ -1,224 +0,0 @@
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;
}
}
}

View File

@ -241,7 +241,6 @@
<string name="show_memory_leaks_key">show_memory_leaks_key</string> <string name="show_memory_leaks_key">show_memory_leaks_key</string>
<string name="allow_disposed_exceptions_key">allow_disposed_exceptions_key</string> <string name="allow_disposed_exceptions_key">allow_disposed_exceptions_key</string>
<string name="show_original_time_ago_key">show_original_time_ago_key</string> <string name="show_original_time_ago_key">show_original_time_ago_key</string>
<string name="show_image_indicators_key">show_image_indicators_key</string>
<string name="show_crash_the_player_key">show_crash_the_player_key</string> <string name="show_crash_the_player_key">show_crash_the_player_key</string>
<string name="check_new_streams_key">check_new_streams</string> <string name="check_new_streams_key">check_new_streams</string>
<string name="crash_the_app_key">crash_the_app_key</string> <string name="crash_the_app_key">crash_the_app_key</string>

View File

@ -34,13 +34,6 @@
app:singleLineTitle="false" app:singleLineTitle="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/show_image_indicators_key"
android:summary="@string/show_image_indicators_summary"
android:title="@string/show_image_indicators_title"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="@string/show_crash_the_player_key" android:key="@string/show_crash_the_player_key"

View File

@ -13,6 +13,7 @@ autoservice-zacsweers = "1.2.0"
bridge = "v2.0.2" bridge = "v2.0.2"
cardview = "1.0.0" cardview = "1.0.0"
checkstyle = "13.0.0" checkstyle = "13.0.0"
coil = "3.0.4"
constraintlayout = "2.2.1" constraintlayout = "2.2.1"
core = "1.17.0" core = "1.17.0"
desugar = "2.1.5" desugar = "2.1.5"
@ -35,8 +36,6 @@ media = "1.7.1"
mockitoCore = "5.21.0" mockitoCore = "5.21.0"
okhttp = "5.3.2" okhttp = "5.3.2"
phoenix = "3.0.0" phoenix = "3.0.0"
#noinspection NewerVersionAvailable,GradleDependency --> 2.8 is the last version, not 2.71828!
picasso = "2.8"
preference = "1.2.1" preference = "1.2.1"
prettytime = "5.0.8.Final" prettytime = "5.0.8.Final"
recyclerview = "1.4.0" recyclerview = "1.4.0"
@ -91,6 +90,8 @@ androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "work" } androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "work" }
androidx-work-rxjava3 = { module = "androidx.work:work-rxjava3", version.ref = "work" } androidx-work-rxjava3 = { module = "androidx.work:work-rxjava3", version.ref = "work" }
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
evernote-statesaver-compiler = { module = "com.evernote:android-state-processor", version.ref = "statesaver" } evernote-statesaver-compiler = { module = "com.evernote:android-state-processor", version.ref = "statesaver" }
evernote-statesaver-core = { module = "com.evernote:android-state", version.ref = "statesaver" } evernote-statesaver-core = { module = "com.evernote:android-state", version.ref = "statesaver" }
facebook-stetho-core = { module = "com.facebook.stetho:stetho", version.ref = "stetho" } facebook-stetho-core = { module = "com.facebook.stetho:stetho", version.ref = "stetho" }
@ -127,7 +128,6 @@ squareup-leakcanary-core = { module = "com.squareup.leakcanary:leakcanary-androi
squareup-leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" } squareup-leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" }
squareup-leakcanary-watcher = { module = "com.squareup.leakcanary:leakcanary-object-watcher-android", version.ref = "leakcanary" } squareup-leakcanary-watcher = { module = "com.squareup.leakcanary:leakcanary-object-watcher-android", version.ref = "leakcanary" }
squareup-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } squareup-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
squareup-picasso = { module = "com.squareup.picasso:picasso", version.ref = "picasso" }
zacsweers-autoservice-compiler = { module = "dev.zacsweers.autoservice:auto-service-ksp", version.ref = "autoservice-zacsweers" } zacsweers-autoservice-compiler = { module = "dev.zacsweers.autoservice:auto-service-ksp", version.ref = "autoservice-zacsweers" }
[plugins] [plugins]