summaryrefslogtreecommitdiff
path: root/ui/glide
diff options
context:
space:
mode:
authorByteHamster <ByteHamster@users.noreply.github.com>2022-11-05 12:50:13 +0100
committerGitHub <noreply@github.com>2022-11-05 12:50:13 +0100
commit546c8841db829a6ac57e8508a91d5734fb4d1ddf (patch)
tree16e9900d434ebe4def1090b989f8f704340ee7fc /ui/glide
parent9b06bf0dc595579193ce1893dfe7721f4b5adf0a (diff)
parentc1fbb538058369a4ed5bfcc6ff3622aceb6ce820 (diff)
downloadAntennaPod-546c8841db829a6ac57e8508a91d5734fb4d1ddf.zip
Merge pull request #6174 from ByteHamster/modularize-glide
Move Glide config to its own module
Diffstat (limited to 'ui/glide')
-rw-r--r--ui/glide/README.md3
-rw-r--r--ui/glide/build.gradle27
-rw-r--r--ui/glide/src/main/AndroidManifest.xml1
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApGlideModule.java56
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApOkHttpUrlLoader.java113
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/AudioCoverFetcher.java62
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ChapterImageModelLoader.java111
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/FastBlurTransformation.java294
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/GenerativePlaceholderImageModelLoader.java139
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/MetadataRetrieverLoader.java57
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/NoHttpStringLoader.java45
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmap.java20
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapResource.java40
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapTranscoder.java32
-rw-r--r--ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ResizingOkHttpStreamFetcher.java138
15 files changed, 1138 insertions, 0 deletions
diff --git a/ui/glide/README.md b/ui/glide/README.md
new file mode 100644
index 000000000..c246f2bea
--- /dev/null
+++ b/ui/glide/README.md
@@ -0,0 +1,3 @@
+# :ui:glide
+
+Configuration and ModelLoaders for the Glide image loading library.
diff --git a/ui/glide/build.gradle b/ui/glide/build.gradle
new file mode 100644
index 000000000..ff61fc8e1
--- /dev/null
+++ b/ui/glide/build.gradle
@@ -0,0 +1,27 @@
+plugins {
+ id("com.android.library")
+}
+apply from: "../../common.gradle"
+apply from: "../../playFlavor.gradle"
+
+android {
+ lintOptions {
+ disable "InvalidPeriodicWorkRequestInterval", "ObsoleteLintCustomCheck", "DefaultLocale", "UnusedAttribute",
+ "ParcelClassLoader", "CheckResult", "TrustAllX509TrustManager",
+ "StaticFieldLeak", "IconDensities", "IconDuplicates", "MissingPermission", "AppCompatResource",
+ "GradleCompatible", "QueryPermissionsNeeded"
+ }
+}
+
+dependencies {
+ implementation project(":model")
+ implementation project(":core")
+
+ implementation "androidx.palette:palette:$paletteVersion"
+
+ implementation "com.github.bumptech.glide:glide:$glideVersion"
+ implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion@aar"
+ annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
+ implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
+ implementation "commons-io:commons-io:$commonsioVersion"
+}
diff --git a/ui/glide/src/main/AndroidManifest.xml b/ui/glide/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..ddc027b29
--- /dev/null
+++ b/ui/glide/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.ui.glide" />
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApGlideModule.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApGlideModule.java
new file mode 100644
index 000000000..d233fba14
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApGlideModule.java
@@ -0,0 +1,56 @@
+package de.danoeh.antennapod.ui.glide;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.GlideBuilder;
+import com.bumptech.glide.Registry;
+import com.bumptech.glide.annotation.GlideModule;
+import com.bumptech.glide.load.DecodeFormat;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
+import com.bumptech.glide.module.AppGlideModule;
+
+import de.danoeh.antennapod.model.feed.EmbeddedChapterImage;
+import java.io.InputStream;
+
+import com.bumptech.glide.request.RequestOptions;
+import java.nio.ByteBuffer;
+
+/**
+ * {@see com.bumptech.glide.integration.okhttp.OkHttpGlideModule}
+ */
+@GlideModule
+public class ApGlideModule extends AppGlideModule {
+ private static final String TAG = "ApGlideModule";
+ private static final long MEGABYTES = 1024 * 1024;
+ private static final long GIGABYTES = 1024 * 1024 * 1024;
+
+ @Override
+ public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
+ builder.setDefaultRequestOptions(new RequestOptions()
+ .format(DecodeFormat.PREFER_ARGB_8888)
+ .diskCacheStrategy(DiskCacheStrategy.ALL));
+ builder.setLogLevel(Log.WARN);
+ @SuppressLint("UsableSpace")
+ long spaceAvailable = context.getCacheDir().getUsableSpace();
+ long imageCacheSize = (spaceAvailable > 2 * GIGABYTES) ? (250 * MEGABYTES) : (50 * MEGABYTES);
+ Log.d(TAG, "Free space on cache dir: " + spaceAvailable + ", using image cache size: " + imageCacheSize);
+ builder.setDiskCache(new InternalCacheDiskCacheFactory(context, imageCacheSize));
+ }
+
+ @Override
+ public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
+ registry.replace(String.class, InputStream.class, new MetadataRetrieverLoader.Factory(context));
+ registry.append(String.class, InputStream.class, new GenerativePlaceholderImageModelLoader.Factory());
+ registry.append(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory());
+ registry.append(String.class, InputStream.class, new NoHttpStringLoader.StreamFactory());
+
+ registry.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.Factory());
+ registry.register(Bitmap.class, PaletteBitmap.class, new PaletteBitmapTranscoder());
+ }
+}
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApOkHttpUrlLoader.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApOkHttpUrlLoader.java
new file mode 100644
index 000000000..5c3605594
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ApOkHttpUrlLoader.java
@@ -0,0 +1,113 @@
+package de.danoeh.antennapod.ui.glide;
+
+import android.content.ContentResolver;
+import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.model.GlideUrl;
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.load.model.ModelLoaderFactory;
+import com.bumptech.glide.load.model.MultiModelLoaderFactory;
+import com.bumptech.glide.signature.ObjectKey;
+import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
+import de.danoeh.antennapod.core.util.NetworkUtils;
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
+import okhttp3.Protocol;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * {@see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader}.
+ */
+class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
+
+ /**
+ * The default factory for {@link ApOkHttpUrlLoader}s.
+ */
+ public static class Factory implements ModelLoaderFactory<String, InputStream> {
+
+ private static volatile OkHttpClient internalClient;
+ private final OkHttpClient client;
+
+ private static OkHttpClient getInternalClient() {
+ if (internalClient == null) {
+ synchronized (Factory.class) {
+ if (internalClient == null) {
+ OkHttpClient.Builder builder = AntennapodHttpClient.newBuilder();
+ builder.interceptors().add(new NetworkAllowanceInterceptor());
+ builder.cache(null); // Handled by Glide
+ internalClient = builder.build();
+ }
+ }
+ }
+ return internalClient;
+ }
+
+ /**
+ * Constructor for a new Factory that runs requests using a static singleton client.
+ */
+ Factory() {
+ this.client = getInternalClient();
+ }
+
+ @NonNull
+ @Override
+ public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
+ return new ApOkHttpUrlLoader(client);
+ }
+
+ @Override
+ public void teardown() {
+ // Do nothing, this instance doesn't own the client.
+ }
+ }
+
+ private final OkHttpClient client;
+
+ private ApOkHttpUrlLoader(OkHttpClient client) {
+ this.client = client;
+ }
+
+ @Nullable
+ @Override
+ public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) {
+ return new LoadData<>(new ObjectKey(model), new ResizingOkHttpStreamFetcher(client, new GlideUrl(model)));
+ }
+
+ @Override
+ public boolean handles(@NonNull String model) {
+ return !TextUtils.isEmpty(model)
+ // If the other loaders fail, do not attempt to load as web resource
+ && !model.startsWith(Feed.PREFIX_GENERATIVE_COVER)
+ && !model.startsWith(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER)
+ // Leave content URIs to Glide's default loaders
+ && !model.startsWith(ContentResolver.SCHEME_CONTENT)
+ && !model.startsWith(ContentResolver.SCHEME_ANDROID_RESOURCE);
+ }
+
+ private static class NetworkAllowanceInterceptor implements Interceptor {
+
+ @NonNull
+ @Override
+ public Response intercept(@NonNull Chain chain) throws IOException {
+ if (NetworkUtils.isImageAllowed()) {
+ return chain.proceed(chain.request());
+ } else {
+ return new Response.Builder()
+ .protocol(Protocol.HTTP_2)
+ .code(420)
+ .message("Policy Not Fulfilled")
+ .body(ResponseBody.create(null, new byte[0]))
+ .request(chain.request())
+ .build();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/AudioCoverFetcher.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/AudioCoverFetcher.java
new file mode 100644
index 000000000..d8e265ab5
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/AudioCoverFetcher.java
@@ -0,0 +1,62 @@
+package de.danoeh.antennapod.ui.glide;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.media.MediaMetadataRetriever;
+
+import android.net.Uri;
+import androidx.annotation.NonNull;
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.data.DataFetcher;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+// see https://github.com/bumptech/glide/issues/699
+class AudioCoverFetcher implements DataFetcher<InputStream> {
+ private final String path;
+ private final Context context;
+
+ public AudioCoverFetcher(String path, Context context) {
+ this.path = path;
+ this.context = context;
+ }
+
+ @Override
+ public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
+ try (MediaMetadataRetriever retriever = new MediaMetadataRetriever()) {
+ if (path.startsWith(ContentResolver.SCHEME_CONTENT)) {
+ retriever.setDataSource(context, Uri.parse(path));
+ } else {
+ retriever.setDataSource(path);
+ }
+ byte[] picture = retriever.getEmbeddedPicture();
+ if (picture != null) {
+ callback.onDataReady(new ByteArrayInputStream(picture));
+ }
+ } catch (Exception e) {
+ callback.onLoadFailed(e);
+ }
+ }
+
+ @Override public void cleanup() {
+ // nothing to clean up
+ }
+
+ @Override public void cancel() {
+ // cannot cancel
+ }
+
+ @NonNull
+ @Override
+ public Class<InputStream> getDataClass() {
+ return InputStream.class;
+ }
+
+ @NonNull
+ @Override
+ public DataSource getDataSource() {
+ return DataSource.LOCAL;
+ }
+}
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ChapterImageModelLoader.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ChapterImageModelLoader.java
new file mode 100644
index 000000000..9f82e0b31
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ChapterImageModelLoader.java
@@ -0,0 +1,111 @@
+package de.danoeh.antennapod.ui.glide;
+
+import androidx.annotation.NonNull;
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.data.DataFetcher;
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.load.model.ModelLoaderFactory;
+import com.bumptech.glide.load.model.MultiModelLoaderFactory;
+import com.bumptech.glide.signature.ObjectKey;
+import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
+import de.danoeh.antennapod.model.feed.EmbeddedChapterImage;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.apache.commons.io.IOUtils;
+
+public final class ChapterImageModelLoader implements ModelLoader<EmbeddedChapterImage, ByteBuffer> {
+
+ public static class Factory implements ModelLoaderFactory<EmbeddedChapterImage, ByteBuffer> {
+ @NonNull
+ @Override
+ public ModelLoader<EmbeddedChapterImage, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
+ return new ChapterImageModelLoader();
+ }
+
+ @Override
+ public void teardown() {
+ // Do nothing.
+ }
+ }
+
+ @Override
+ public LoadData<ByteBuffer> buildLoadData(@NonNull EmbeddedChapterImage model,
+ int width,
+ int height,
+ @NonNull Options options) {
+ return new LoadData<>(new ObjectKey(model), new EmbeddedImageFetcher(model));
+ }
+
+ @Override
+ public boolean handles(@NonNull EmbeddedChapterImage model) {
+ return true;
+ }
+
+ static class EmbeddedImageFetcher implements DataFetcher<ByteBuffer> {
+ private final EmbeddedChapterImage image;
+
+ public EmbeddedImageFetcher(EmbeddedChapterImage image) {
+ this.image = image;
+ }
+
+ @Override
+ public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
+
+ BufferedInputStream stream = null;
+ try {
+ if (image.getMedia().localFileAvailable()) {
+ File localFile = new File(image.getMedia().getLocalMediaUrl());
+ stream = new BufferedInputStream(new FileInputStream(localFile));
+ IOUtils.skip(stream, image.getPosition());
+ byte[] imageContent = new byte[image.getLength()];
+ IOUtils.read(stream, imageContent, 0, image.getLength());
+ callback.onDataReady(ByteBuffer.wrap(imageContent));
+ } else {
+ Request.Builder httpReq = new Request.Builder();
+ // Skipping would download the whole file
+ httpReq.header("Range", "bytes=" + image.getPosition()
+ + "-" + (image.getPosition() + image.getLength()));
+ httpReq.url(image.getMedia().getStreamUrl());
+ Response response = AntennapodHttpClient.getHttpClient().newCall(httpReq.build()).execute();
+ if (!response.isSuccessful() || response.body() == null) {
+ throw new IOException("Invalid response: " + response.code() + " " + response.message());
+ }
+ callback.onDataReady(ByteBuffer.wrap(response.body().bytes()));
+ }
+ } catch (IOException e) {
+ callback.onLoadFailed(e);
+ } finally {
+ IOUtils.closeQuietly(stream);
+ }
+ }
+
+ @Override
+ public void cleanup() {
+ // nothing to clean up
+ }
+
+ @Override
+ public void cancel() {
+ // cannot cancel
+ }
+
+ @NonNull
+ @Override
+ public Class<ByteBuffer> getDataClass() {
+ return ByteBuffer.class;
+ }
+
+ @NonNull
+ @Override
+ public DataSource getDataSource() {
+ return DataSource.LOCAL;
+ }
+ }
+}
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/FastBlurTransformation.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/FastBlurTransformation.java
new file mode 100644
index 000000000..a19611100
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/FastBlurTransformation.java
@@ -0,0 +1,294 @@
+package de.danoeh.antennapod.ui.glide;
+
+import android.graphics.Bitmap;
+import android.media.ThumbnailUtils;
+import androidx.annotation.NonNull;
+import android.util.Log;
+
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
+
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+
+public class FastBlurTransformation extends BitmapTransformation {
+ private static final String ID = "de.danoeh.antennapod.core.glide.FastBlurTransformation";
+
+ private static final String TAG = FastBlurTransformation.class.getSimpleName();
+
+ private static final int STACK_BLUR_RADIUS = 10;
+
+ public FastBlurTransformation() {
+ super();
+ }
+
+ @Override
+ protected Bitmap transform(@NonNull BitmapPool pool,
+ @NonNull Bitmap source,
+ int outWidth,
+ int outHeight) {
+ int targetWidth = outWidth / 3;
+ int targetHeight = (int) (1.0 * outHeight * targetWidth / outWidth);
+ Bitmap resized = ThumbnailUtils.extractThumbnail(source, targetWidth, targetHeight);
+ Bitmap result = fastBlur(resized, STACK_BLUR_RADIUS);
+ if (result == null) {
+ Log.w(TAG, "result was null");
+ return source;
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof FastBlurTransformation;
+ }
+
+ @Override
+ public int hashCode() {
+ return ID.hashCode();
+ }
+
+ @Override
+ public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
+ messageDigest.update(TAG.getBytes(Charset.defaultCharset()));
+ }
+
+ private static Bitmap fastBlur(Bitmap bitmap, int radius) {
+
+ // Stack Blur v1.0 from
+ // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
+ //
+ // Java Author: Mario Klingemann <mario at quasimondo.com>
+ // http://incubator.quasimondo.com
+ // created Feburary 29, 2004
+ // Android port : Yahel Bouaziz <yahel at kayenko.com>
+ // http://www.kayenko.com
+ // ported april 5th, 2012
+
+ // This is a compromise between Gaussian Blur and Box blur
+ // It creates much better looking blurs than Box Blur, but is
+ // 7x faster than my Gaussian Blur implementation.
+ //
+ // I called it Stack Blur because this describes best how this
+ // filter works internally: it creates a kind of moving stack
+ // of colors whilst scanning through the image. Thereby it
+ // just has to add one new block of color to the right side
+ // of the stack and remove the leftmost color. The remaining
+ // colors on the topmost layer of the stack are either added on
+ // or reduced by one, depending on if they are on the right or
+ // on the left side of the stack.
+ //
+ // If you are using this algorithm in your code please add
+ // the following line:
+ //
+ // Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com>
+
+ if (radius < 1) {
+ return null;
+ }
+
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+
+ int[] pix = new int[w * h];
+ bitmap.getPixels(pix, 0, w, 0, 0, w, h);
+
+ int wm = w - 1;
+ int hm = h - 1;
+ int wh = w * h;
+ int div = radius + radius + 1;
+
+ int[] r = new int[wh];
+ int[] g = new int[wh];
+ int[] b = new int[wh];
+ int rsum;
+ int gsum;
+ int bsum;
+ int x;
+ int y;
+ int i;
+ int p;
+ int yp;
+ int yi;
+ int yw;
+ int[] vmin = new int[Math.max(w, h)];
+
+ int divsum = (div + 1) >> 1;
+ divsum *= divsum;
+ int[] dv = new int[256 * divsum];
+ for (i = 0; i < 256 * divsum; i++) {
+ dv[i] = (i / divsum);
+ }
+
+ yw = yi = 0;
+
+ int[][] stack = new int[div][3];
+ int stackpointer;
+ int stackstart;
+ int[] sir;
+ int rbs;
+ int r1 = radius + 1;
+ int routsum;
+ int goutsum;
+ int boutsum;
+ int rinsum;
+ int ginsum;
+ int binsum;
+
+ for (y = 0; y < h; y++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ for (i = -radius; i <= radius; i++) {
+ p = pix[yi + Math.min(wm, Math.max(i, 0))];
+ sir = stack[i + radius];
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = (p & 0x0000ff);
+ rbs = r1 - Math.abs(i);
+ rsum += sir[0] * rbs;
+ gsum += sir[1] * rbs;
+ bsum += sir[2] * rbs;
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+ }
+ stackpointer = radius;
+
+ for (x = 0; x < w; x++) {
+
+ r[yi] = dv[rsum];
+ g[yi] = dv[gsum];
+ b[yi] = dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - radius + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (y == 0) {
+ vmin[x] = Math.min(x + radius + 1, wm);
+ }
+ p = pix[yw + vmin[x]];
+
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = (p & 0x0000ff);
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[(stackpointer) % div];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi++;
+ }
+ yw += w;
+ }
+ for (x = 0; x < w; x++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ yp = -radius * w;
+ for (i = -radius; i <= radius; i++) {
+ yi = Math.max(0, yp) + x;
+
+ sir = stack[i + radius];
+
+ sir[0] = r[yi];
+ sir[1] = g[yi];
+ sir[2] = b[yi];
+
+ rbs = r1 - Math.abs(i);
+
+ rsum += r[yi] * rbs;
+ gsum += g[yi] * rbs;
+ bsum += b[yi] * rbs;
+
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+
+ if (i < hm) {
+ yp += w;
+ }
+ }
+ yi = x;
+ stackpointer = radius;
+ for (y = 0; y < h; y++) {
+ // Set alpha to 1
+ pix[yi] = 0xff000000 | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - radius + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (x == 0) {
+ vmin[y] = Math.min(y + r1, hm) * w;
+ }
+ p = x + vmin[y];
+
+ sir[0] = r[p];
+ sir[1] = g[p];
+ sir[2] = b[p];
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[stackpointer];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi += w;
+ }
+ }
+ bitmap.setPixels(pix, 0, w, 0, 0, w, h);
+ return bitmap;
+ }
+}
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/GenerativePlaceholderImageModelLoader.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/GenerativePlaceholderImageModelLoader.java
new file mode 100644
index 000000000..3ad8fba6a
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/GenerativePlaceholderImageModelLoader.java
@@ -0,0 +1,139 @@
+package de.danoeh.antennapod.ui.glide;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Shader;
+import androidx.annotation.NonNull;
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.data.DataFetcher;
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.load.model.ModelLoaderFactory;
+import com.bumptech.glide.load.model.MultiModelLoaderFactory;
+import com.bumptech.glide.signature.ObjectKey;
+import de.danoeh.antennapod.model.feed.Feed;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.Random;
+
+public final class GenerativePlaceholderImageModelLoader implements ModelLoader<String, InputStream> {
+
+ public static class Factory implements ModelLoaderFactory<String, InputStream> {
+ @NonNull
+ @Override
+ public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory unused) {
+ return new GenerativePlaceholderImageModelLoader();
+ }
+
+ @Override
+ public void teardown() {
+ // Do nothing.
+ }
+ }
+
+ @Override
+ public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) {
+ return new LoadData<>(new ObjectKey(model), new EmbeddedImageFetcher(model, width, height));
+ }
+
+ @Override
+ public boolean handles(@NonNull String model) {
+ return model.startsWith(Feed.PREFIX_GENERATIVE_COVER);
+ }
+
+ static class EmbeddedImageFetcher implements DataFetcher<InputStream> {
+ private static final int[] PALETTES = {0xff78909c, 0xffff6f00, 0xff388e3c,
+ 0xff00838f, 0xff7b1fa2, 0xffb71c1c, 0xff2196f3};
+ private final String model;
+ private final int width;
+ private final int height;
+
+ public EmbeddedImageFetcher(String model, int width, int height) {
+ this.model = model;
+ this.width = width;
+ this.height = height;
+ }
+
+ @Override
+ public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
+ final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bitmap);
+ final Random generator = new Random(model.hashCode());
+ final int lineGridSteps = 4 + generator.nextInt(4);
+ final int slope = width / 4;
+ final float shadowWidth = width * 0.01f;
+ final float lineDistance = ((float) width / (lineGridSteps - 2));
+ final int baseColor = PALETTES[generator.nextInt(PALETTES.length)];
+
+ Paint paint = new Paint();
+ int color = randomShadeOfGrey(generator);
+ paint.setColor(color);
+ paint.setStrokeWidth(lineDistance);
+ paint.setColorFilter(new PorterDuffColorFilter(baseColor, PorterDuff.Mode.MULTIPLY));
+ Paint paintShadow = new Paint();
+ paintShadow.setColor(0xff000000);
+ paintShadow.setStrokeWidth(lineDistance);
+
+ int forcedColorChange = 1 + generator.nextInt(lineGridSteps - 2);
+ for (int i = lineGridSteps - 1; i >= 0; i--) {
+ float linePos = (i - 0.5f) * lineDistance;
+ boolean switchColor = generator.nextFloat() < 0.3f || i == forcedColorChange;
+ if (switchColor) {
+ int newColor = color;
+ while (newColor == color) {
+ newColor = randomShadeOfGrey(generator);
+ }
+ color = newColor;
+ paint.setColor(newColor);
+ canvas.drawLine(linePos + slope + shadowWidth, -slope,
+ linePos - slope + shadowWidth, height + slope, paintShadow);
+ }
+ canvas.drawLine(linePos + slope, -slope,
+ linePos - slope, height + slope, paint);
+ }
+
+ Paint gradientPaint = new Paint();
+ paint.setDither(true);
+ gradientPaint.setShader(new LinearGradient(0, 0, 0, height, 0x00000000, 0x55000000, Shader.TileMode.CLAMP));
+ canvas.drawRect(0, 0, width, height, gradientPaint);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
+ InputStream is = new ByteArrayInputStream(baos.toByteArray());
+ callback.onDataReady(is);
+ }
+
+ private static int randomShadeOfGrey(Random generator) {
+ return 0xff777777 + 0x222222 * generator.nextInt(5);
+ }
+
+ @Override
+ public void cleanup() {
+ // nothing to clean up
+ }
+
+ @Override
+ public void cancel() {
+ // cannot cancel
+ }
+
+ @NonNull
+ @Override
+ public Class<InputStream> getDataClass() {
+ return InputStream.class;
+ }
+
+ @NonNull
+ @Override
+ public DataSource getDataSource() {
+ return DataSource.LOCAL;
+ }
+ }
+}
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/MetadataRetrieverLoader.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/MetadataRetrieverLoader.java
new file mode 100644
index 000000000..a5046b7d0
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/MetadataRetrieverLoader.java
@@ -0,0 +1,57 @@
+package de.danoeh.antennapod.ui.glide;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.load.model.ModelLoaderFactory;
+import com.bumptech.glide.load.model.MultiModelLoaderFactory;
+import com.bumptech.glide.signature.ObjectKey;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+
+import java.io.InputStream;
+
+class MetadataRetrieverLoader implements ModelLoader<String, InputStream> {
+
+ /**
+ * The default factory for {@link MetadataRetrieverLoader}s.
+ */
+ public static class Factory implements ModelLoaderFactory<String, InputStream> {
+ private final Context context;
+
+ Factory(Context context) {
+ this.context = context;
+ }
+
+ @NonNull
+ @Override
+ public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
+ return new MetadataRetrieverLoader(context);
+ }
+
+ @Override
+ public void teardown() {
+ // Do nothing, this instance doesn't own the client.
+ }
+ }
+
+ private final Context context;
+
+ private MetadataRetrieverLoader(Context context) {
+ this.context = context;
+ }
+
+ @Nullable
+ @Override
+ public LoadData<InputStream> buildLoadData(@NonNull String model,
+ int width, int height, @NonNull Options options) {
+ return new LoadData<>(new ObjectKey(model),
+ new AudioCoverFetcher(model.replace(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER, ""), context));
+ }
+
+ @Override
+ public boolean handles(@NonNull String model) {
+ return model.startsWith(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER);
+ }
+}
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/NoHttpStringLoader.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/NoHttpStringLoader.java
new file mode 100644
index 000000000..63af834b6
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/NoHttpStringLoader.java
@@ -0,0 +1,45 @@
+package de.danoeh.antennapod.ui.glide;
+
+import android.net.Uri;
+import androidx.annotation.NonNull;
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.load.model.ModelLoaderFactory;
+import com.bumptech.glide.load.model.MultiModelLoaderFactory;
+import com.bumptech.glide.load.model.StringLoader;
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+
+import java.io.InputStream;
+
+/**
+ * StringLoader that does not handle http/https urls. Used to avoid fallback to StringLoader when
+ * AntennaPod blocks mobile image loading.
+ */
+public final class NoHttpStringLoader extends StringLoader<InputStream> {
+
+ public static class StreamFactory implements ModelLoaderFactory<String, InputStream> {
+ @NonNull
+ @Override
+ public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
+ return new NoHttpStringLoader(multiFactory.build(Uri.class, InputStream.class));
+ }
+
+ @Override
+ public void teardown() {
+ // Do nothing.
+ }
+ }
+
+ public NoHttpStringLoader(ModelLoader<Uri, InputStream> uriLoader) {
+ super(uriLoader);
+ }
+
+ @Override
+ public boolean handles(@NonNull String model) {
+ return !model.startsWith("http")
+ // If the custom loaders fail, do not attempt to load with Glide internal loaders
+ && !model.startsWith(Feed.PREFIX_GENERATIVE_COVER)
+ && !model.startsWith(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER)
+ && super.handles(model);
+ }
+}
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmap.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmap.java
new file mode 100644
index 000000000..a3b590ba2
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmap.java
@@ -0,0 +1,20 @@
+/*
+ * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example
+ */
+
+package de.danoeh.antennapod.ui.glide;
+
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
+import androidx.palette.graphics.Palette;
+
+public class PaletteBitmap {
+ public final Palette palette;
+ public final Bitmap bitmap;
+
+ public PaletteBitmap(@NonNull Bitmap bitmap, Palette palette) {
+ this.bitmap = bitmap;
+ this.palette = palette;
+ }
+} \ No newline at end of file
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapResource.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapResource.java
new file mode 100644
index 000000000..2fd18a0cb
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapResource.java
@@ -0,0 +1,40 @@
+/*
+ * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example
+ */
+
+package de.danoeh.antennapod.ui.glide;
+
+import androidx.annotation.NonNull;
+
+import com.bumptech.glide.load.engine.Resource;
+import com.bumptech.glide.util.Util;
+
+public class PaletteBitmapResource implements Resource<PaletteBitmap> {
+ private final PaletteBitmap paletteBitmap;
+
+ public PaletteBitmapResource(@NonNull PaletteBitmap paletteBitmap) {
+ this.paletteBitmap = paletteBitmap;
+ }
+
+ @NonNull
+ @Override
+ public Class<PaletteBitmap> getResourceClass() {
+ return PaletteBitmap.class;
+ }
+
+ @NonNull
+ @Override
+ public PaletteBitmap get() {
+ return paletteBitmap;
+ }
+
+ @Override
+ public int getSize() {
+ return Util.getBitmapByteSize(paletteBitmap.bitmap);
+ }
+
+ @Override
+ public void recycle() {
+ paletteBitmap.bitmap.recycle();
+ }
+} \ No newline at end of file
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapTranscoder.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapTranscoder.java
new file mode 100644
index 000000000..75599a30a
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/PaletteBitmapTranscoder.java
@@ -0,0 +1,32 @@
+/*
+ * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example
+ */
+
+package de.danoeh.antennapod.ui.glide;
+
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.palette.graphics.Palette;
+
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.engine.Resource;
+import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
+
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+public class PaletteBitmapTranscoder implements ResourceTranscoder<Bitmap, PaletteBitmap> {
+
+ @Nullable
+ @Override
+ public Resource<PaletteBitmap> transcode(@NonNull Resource<Bitmap> toTranscode, @NonNull Options options) {
+ Bitmap bitmap = toTranscode.get();
+ Palette palette = null;
+ if (UserPreferences.shouldShowSubscriptionTitle()) {
+ palette = new Palette.Builder(bitmap).generate();
+ }
+ PaletteBitmap result = new PaletteBitmap(bitmap, palette);
+ return new PaletteBitmapResource(result);
+ }
+}
diff --git a/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ResizingOkHttpStreamFetcher.java b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ResizingOkHttpStreamFetcher.java
new file mode 100644
index 000000000..2d4e4d45f
--- /dev/null
+++ b/ui/glide/src/main/java/de/danoeh/antennapod/ui/glide/ResizingOkHttpStreamFetcher.java
@@ -0,0 +1,138 @@
+package de.danoeh.antennapod.ui.glide;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher;
+import com.bumptech.glide.load.data.DataFetcher;
+import com.bumptech.glide.load.model.GlideUrl;
+import okhttp3.Call;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class ResizingOkHttpStreamFetcher extends OkHttpStreamFetcher {
+ private static final String TAG = "ResizingOkHttpStreamFet";
+ private static final int MAX_DIMENSIONS = 1500;
+ private static final int MAX_FILE_SIZE = 1024 * 1024; // 1 MB
+
+ private FileInputStream stream;
+ private File tempIn;
+ private File tempOut;
+
+ public ResizingOkHttpStreamFetcher(Call.Factory client, GlideUrl url) {
+ super(client, url);
+ }
+
+ @Override
+ public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super InputStream> callback) {
+ super.loadData(priority, new DataFetcher.DataCallback<InputStream>() {
+ @Override
+ public void onDataReady(@Nullable InputStream data) {
+ if (data == null) {
+ callback.onDataReady(null);
+ return;
+ }
+ try {
+ tempIn = File.createTempFile("resize_", null);
+ tempOut = File.createTempFile("resize_", null);
+ OutputStream outputStream = new FileOutputStream(tempIn);
+ IOUtils.copy(data, outputStream);
+ outputStream.close();
+ IOUtils.closeQuietly(data);
+
+ if (tempIn.length() <= MAX_FILE_SIZE) {
+ try {
+ stream = new FileInputStream(tempIn);
+ callback.onDataReady(stream); // Just deliver the original, non-scaled image
+ } catch (FileNotFoundException fileNotFoundException) {
+ callback.onLoadFailed(fileNotFoundException);
+ }
+ return;
+ }
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ FileInputStream in = new FileInputStream(tempIn);
+ BitmapFactory.decodeStream(in, null, options);
+ IOUtils.closeQuietly(in);
+
+ if (options.outWidth == -1 || options.outHeight == -1) {
+ throw new IOException("Not a valid image");
+ } else if (Math.max(options.outHeight, options.outWidth) >= MAX_DIMENSIONS) {
+ double sampleSize = (double) Math.max(options.outHeight, options.outWidth) / MAX_DIMENSIONS;
+ options.inSampleSize = (int) Math.pow(2d, Math.floor(Math.log(sampleSize) / Math.log(2d)));
+ }
+
+ options.inJustDecodeBounds = false;
+ in = new FileInputStream(tempIn);
+ Bitmap bitmap = BitmapFactory.decodeStream(in, null, options);
+ IOUtils.closeQuietly(in);
+
+ Bitmap.CompressFormat format = Build.VERSION.SDK_INT < 30
+ ? Bitmap.CompressFormat.WEBP : Bitmap.CompressFormat.WEBP_LOSSY;
+
+ int quality = 100;
+ while (true) {
+ FileOutputStream out = new FileOutputStream(tempOut);
+ bitmap.compress(format, quality, out);
+ IOUtils.closeQuietly(out);
+
+ if (tempOut.length() > 3 * MAX_FILE_SIZE && quality >= 45) {
+ quality -= 40;
+ } else if (tempOut.length() > 2 * MAX_FILE_SIZE && quality >= 25) {
+ quality -= 20;
+ } else if (tempOut.length() > MAX_FILE_SIZE && quality >= 15) {
+ quality -= 10;
+ } else if (tempOut.length() > MAX_FILE_SIZE && quality >= 10) {
+ quality -= 5;
+ } else {
+ break;
+ }
+ }
+ bitmap.recycle();
+
+ stream = new FileInputStream(tempOut);
+ callback.onDataReady(stream);
+ Log.d(TAG, "Compressed image from " + tempIn.length() / 1024
+ + " to " + tempOut.length() / 1024 + " kB (quality: " + quality + "%)");
+ } catch (Throwable e) {
+ e.printStackTrace();
+
+ try {
+ stream = new FileInputStream(tempIn);
+ callback.onDataReady(stream); // Just deliver the original, non-scaled image
+ } catch (FileNotFoundException fileNotFoundException) {
+ e.printStackTrace();
+ callback.onLoadFailed(fileNotFoundException);
+ }
+ }
+ }
+
+ @Override
+ public void onLoadFailed(@NonNull Exception e) {
+ callback.onLoadFailed(e);
+ }
+ });
+ }
+
+ @Override
+ public void cleanup() {
+ IOUtils.closeQuietly(stream);
+ FileUtils.deleteQuietly(tempIn);
+ FileUtils.deleteQuietly(tempOut);
+ super.cleanup();
+ }
+}