Skip to content

Commit

Permalink
Migrate image loading from Picasso to Coil (#11201)
Browse files Browse the repository at this point in the history
* Load notification icons using Coil

* Migrate to Coil from Picasso

* Clean up Picasso leftovers

* Enable RGB-565 for low-end devices

* Added Coil helper method

* Add annotation

* Simplify newImageLoader implementation

* Use Coil's default disk and memory cache config

* Enable crossfade animation

* Correct method name

* Fix thumbnail not being displayed in media notification
  • Loading branch information
Isira-Seneviratne authored Jul 3, 2024
1 parent 03167a1 commit 73e3a69
Show file tree
Hide file tree
Showing 92 changed files with 330 additions and 596 deletions.
3 changes: 1 addition & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,7 @@ dependencies {
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"

// Image loading
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
implementation "com.squareup.picasso:picasso:2.8"
implementation 'io.coil-kt:coil:2.6.0'

// Markdown library for Android
implementation "io.noties.markwon:core:${markwonVersion}"
Expand Down
24 changes: 15 additions & 9 deletions app/src/main/java/org/schabi/newpipe/App.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.schabi.newpipe;

import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
Expand All @@ -8,6 +9,7 @@
import androidx.annotation.NonNull;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;

import com.jakewharton.processphoenix.ProcessPhoenix;
Expand All @@ -20,10 +22,9 @@
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.NewPipeSettings;
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.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PreferredImageQuality;

import java.io.IOException;
Expand All @@ -32,6 +33,9 @@
import java.util.List;
import java.util.Objects;

import coil.ImageLoader;
import coil.ImageLoaderFactory;
import coil.util.DebugLogger;
import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
Expand All @@ -57,7 +61,7 @@
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/

public class App extends Application {
public class App extends Application implements ImageLoaderFactory {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString();

Expand Down Expand Up @@ -108,20 +112,22 @@ public void onCreate() {

// 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();
}

@NonNull
@Override
public void onTerminate() {
super.onTerminate();
PicassoHelper.terminate();
public ImageLoader newImageLoader() {
return new ImageLoader.Builder(this)
.allowRgb565(ContextCompat.getSystemService(this, ActivityManager.class)
.isLowRamDevice())
.logger(BuildConfig.DEBUG ? new DebugLogger() : null)
.crossfade(true)
.build();
}

protected Downloader getDownloader() {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ class AboutActivity : AppCompatActivity() {
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
),
SoftwareComponent(
"Picasso", "2013", "Square, Inc.",
"https://square.github.io/picasso/", StandardLicenses.APACHE2
"Coil", "2023", "Coil Contributors",
"https://coil-kt.github.io/coil/", StandardLicenses.APACHE2
),
SoftwareComponent(
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
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.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;

import java.util.ArrayList;
import java.util.Iterator;
Expand All @@ -127,6 +127,7 @@
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import coil.util.CoilUtils;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
Expand Down Expand Up @@ -159,8 +160,6 @@ 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;
Expand Down Expand Up @@ -1471,7 +1470,10 @@ public void showLoading() {
}
}

PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
CoilUtils.dispose(binding.detailThumbnailImageView);
CoilUtils.dispose(binding.detailSubChannelThumbnailView);
CoilUtils.dispose(binding.overlayThumbnail);

binding.detailThumbnailImageView.setImageBitmap(null);
binding.detailSubChannelThumbnailView.setImageBitmap(null);
}
Expand Down Expand Up @@ -1562,8 +1564,8 @@ public void handleResult(@NonNull final StreamInfo info) {
binding.detailSecondaryControlPanel.setVisibility(View.GONE);

checkUpdateProgressInfo(info);
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView);
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.detailThumbnailImageView,
info.getThumbnails());
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables);

Expand Down Expand Up @@ -1613,8 +1615,8 @@ private void displayUploaderAsSubChannel(final StreamInfo info) {
binding.detailUploaderTextView.setVisibility(View.GONE);
}

PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
info.getUploaderAvatars());
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
}
Expand Down Expand Up @@ -1645,11 +1647,11 @@ private void displayBothUploaderAndSubChannel(final StreamInfo info) {
binding.detailUploaderTextView.setVisibility(View.GONE);
}

PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
info.getSubChannelAvatars());
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailUploaderThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailUploaderThumbnailView,
info.getUploaderAvatars());
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
}

Expand Down Expand Up @@ -2403,8 +2405,7 @@ private void updateOverlayData(@Nullable final String overlayTitle,
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageDrawable(null);
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail);
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails);
}

private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,16 @@
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
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.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;

import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;

import coil.util.CoilUtils;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
Expand All @@ -73,7 +74,6 @@ 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;
Expand Down Expand Up @@ -576,7 +576,9 @@ private void runWorker(final boolean forceLoad) {
@Override
public void 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);
}

Expand All @@ -587,17 +589,15 @@ public void handleResult(@NonNull final ChannelInfo result) {
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());

if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelBannerImage);
CoilHelper.INSTANCE.loadBanner(binding.channelBannerImage, result.getBanners());
} else {
// do not waste space for the banner, if the user disabled images or there is not one
binding.channelBannerImage.setImageDrawable(null);
}

PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelAvatarView);
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.subChannelAvatarView);
CoilHelper.INSTANCE.loadAvatar(binding.channelAvatarView, result.getAvatars());
CoilHelper.INSTANCE.loadAvatar(binding.subChannelAvatarView,
result.getParentChannelAvatars());

binding.channelTitleView.setText(result.getName());
binding.channelSubscriberView.setVisibility(View.VISIBLE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
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 java.util.Queue;
Expand Down Expand Up @@ -82,7 +82,7 @@ protected Supplier<View> getListHeaderSupplier() {
final CommentsInfoItem item = commentsInfoItem;

// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
CoilHelper.INSTANCE.loadAvatar(binding.authorAvatar, item.getUploaderAvatars());
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
? View.VISIBLE : View.GONE);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
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.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;

import java.util.ArrayList;
Expand All @@ -62,6 +62,7 @@
import java.util.function.Supplier;
import java.util.stream.Collectors;

import coil.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
Expand All @@ -71,8 +72,6 @@
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;
Expand Down Expand Up @@ -276,7 +275,7 @@ public void showLoading() {
animate(headerBinding.getRoot(), false, 200);
animateHideRecyclerViewAllowingScrolling(itemsList);

PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
CoilUtils.dispose(headerBinding.uploaderAvatarView);
animate(headerBinding.uploaderLayout, false, 200);
}

Expand Down Expand Up @@ -327,8 +326,8 @@ public void handleResult(@NonNull final PlaylistInfo result) {
R.drawable.ic_radio)
);
} else {
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
.into(headerBinding.uploaderAvatarView);
CoilHelper.INSTANCE.loadAvatar(headerBinding.uploaderAvatarView,
result.getUploaderAvatars());
}

streamCount = result.getStreamCount();
Expand Down
44 changes: 23 additions & 21 deletions app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt
Original file line number Diff line number Diff line change
@@ -1,51 +1,51 @@
package org.schabi.newpipe.info_list

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
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.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper

class StreamSegmentItem(
private val item: StreamSegment,
private val onClick: StreamSegmentAdapter.StreamSegmentListener
) : Item<GroupieViewHolder>() {
) : BindableItem<ItemStreamSegmentBinding>() {

companion object {
const val PAYLOAD_SELECT = 1
}

var isSelected = false

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
override fun bind(viewBinding: ItemStreamSegmentBinding, position: Int) {
CoilHelper.loadThumbnail(viewBinding.previewImage, item.previewUrl)
viewBinding.textViewTitle.text = item.title
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
// 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,
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).maxLines = 2
viewBinding.textViewTitle.maxLines = 2
} else {
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).text = item.channelName
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.VISIBLE
viewBinding.textViewChannel.text = item.channelName
viewBinding.textViewChannel.visibility = View.VISIBLE
}
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
viewBinding.textViewStartSeconds.text =
Localization.getDurationString(item.startTimeSeconds.toLong())
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewHolder.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
viewHolder.root.isSelected = isSelected
viewBinding.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewBinding.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
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)) {
viewHolder.root.isSelected = isSelected
return
Expand All @@ -54,4 +54,6 @@ class StreamSegmentItem(
}

override fun getLayout() = R.layout.item_stream_segment

override fun initializeViewBinding(view: View) = ItemStreamSegmentBinding.bind(view)
}
Loading

0 comments on commit 73e3a69

Please sign in to comment.