summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorByteHamster <ByteHamster@users.noreply.github.com>2022-01-30 14:03:39 +0100
committerGitHub <noreply@github.com>2022-01-30 14:03:39 +0100
commitd953ad0869a74ed84f837aa30272d96cd88a857d (patch)
treebe2dbc88fac9287cc0da9bfed048621ba0261def
parent08bd963fd963fde83277f62f12de50014e94d1a7 (diff)
downloadantennapod-d953ad0869a74ed84f837aa30272d96cd88a857d.zip
Nicer placeholder images (#5679)
Shows randomly generated placeholder images for: - Feeds that do not have a cover (usually happens for text-only feeds) - Feeds that specify an invalid cover still show a gray square - Local folders when there is no image file in the folder that we could use
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java17
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/GenerativePlaceholderImageModelLoader.java139
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java4
-rw-r--r--core/src/main/res/raw/local_feed_default_icon.pngbin1240 -> 0 bytes
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java28
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java1
8 files changed, 161 insertions, 32 deletions
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java
index 5ab354d05..8351d1fb5 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsRecyclerAdapter.java
@@ -33,7 +33,6 @@ import java.util.Locale;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
-import de.danoeh.antennapod.core.feed.LocalFeedUpdater;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
@@ -255,7 +254,7 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
boolean textAndImageCombind = feed.isLocalFeed()
- && LocalFeedUpdater.getDefaultIconUrl(itemView.getContext()).equals(feed.getImageUrl());
+ && feed.getImageUrl() != null && feed.getImageUrl().startsWith(Feed.PREFIX_GENERATIVE_COVER);
new CoverLoader(mainActivityRef.get())
.withUri(feed.getImageUrl())
.withPlaceholderView(feedTitle, textAndImageCombind)
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
index 7ddaa080a..e0e1bbaa5 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
@@ -1,6 +1,5 @@
package de.danoeh.antennapod.core.feed;
-import android.content.ContentResolver;
import android.content.Context;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
@@ -104,7 +103,7 @@ public class LocalFeedUpdater {
}
}
- feed.setImageUrl(getImageUrl(context, documentFolder));
+ feed.setImageUrl(getImageUrl(documentFolder));
feed.getPreferences().setAutoDownload(false);
feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO);
@@ -122,7 +121,7 @@ public class LocalFeedUpdater {
* Returns the image URL for the local feed.
*/
@NonNull
- static String getImageUrl(@NonNull Context context, @NonNull DocumentFile documentFolder) {
+ static String getImageUrl(@NonNull DocumentFile documentFolder) {
// look for special file names
for (String iconLocation : PREFERRED_FEED_IMAGE_FILENAMES) {
DocumentFile image = documentFolder.findFile(iconLocation);
@@ -140,17 +139,7 @@ public class LocalFeedUpdater {
}
// use default icon as fallback
- return getDefaultIconUrl(context);
- }
-
- /**
- * Returns the URL of the default icon for a local feed. The URL refers to an app resource file.
- */
- public static String getDefaultIconUrl(Context context) {
- String resourceEntryName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
- return ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
- + context.getPackageName() + "/raw/"
- + resourceEntryName;
+ return Feed.PREFIX_GENERATIVE_COVER + documentFolder.getUri();
}
private static FeedItem feedContainsFile(Feed feed, String filename) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java
index 797addcc1..593b683f7 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java
@@ -42,6 +42,7 @@ public class ApGlideModule extends AppGlideModule {
@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());
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/GenerativePlaceholderImageModelLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/GenerativePlaceholderImageModelLoader.java
new file mode 100644
index 000000000..a2263bc28
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/GenerativePlaceholderImageModelLoader.java
@@ -0,0 +1,139 @@
+package de.danoeh.antennapod.core.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/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java
index 21d3452d6..7e3b07880 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java
@@ -1,5 +1,6 @@
package de.danoeh.antennapod.core.service.download.handler;
+import android.text.TextUtils;
import android.util.Log;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
@@ -48,6 +49,9 @@ public class FeedParserTask implements Callable<FeedHandlerResult> {
result = feedHandler.parseFeed(feed);
Log.d(TAG, feed.getTitle() + " parsed");
checkFeedData(feed);
+ if (TextUtils.isEmpty(feed.getImageUrl())) {
+ feed.setImageUrl(Feed.PREFIX_GENERATIVE_COVER + feed.getDownload_url());
+ }
} catch (SAXException | IOException | ParserConfigurationException e) {
successful = false;
e.printStackTrace();
diff --git a/core/src/main/res/raw/local_feed_default_icon.png b/core/src/main/res/raw/local_feed_default_icon.png
deleted file mode 100644
index c1b24a729..000000000
--- a/core/src/main/res/raw/local_feed_default_icon.png
+++ /dev/null
Binary files differ
diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java
index eb56a1876..37d525670 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java
@@ -31,7 +31,6 @@ import java.util.List;
import de.danoeh.antennapod.core.ApplicationCallbacks;
import de.danoeh.antennapod.core.ClientConfig;
-import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
@@ -41,6 +40,7 @@ import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
@@ -158,8 +158,7 @@ public class LocalFeedUpdaterTest {
callUpdateFeed(LOCAL_FEED_DIR1);
Feed feedAfter = verifySingleFeedInDatabase();
- String resourceEntryName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
- assertThat(feedAfter.getImageUrl(), endsWith(resourceEntryName));
+ assertThat(feedAfter.getImageUrl(), startsWith(Feed.PREFIX_GENERATIVE_COVER));
}
/**
@@ -191,17 +190,15 @@ public class LocalFeedUpdaterTest {
@Test
public void testGetImageUrl_EmptyFolder() {
DocumentFile documentFolder = mockDocumentFolder();
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
- String defaultImageName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
- assertThat(imageUrl, endsWith(defaultImageName));
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
+ assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
}
@Test
public void testGetImageUrl_NoImageButAudioFiles() {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"));
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
- String defaultImageName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
- assertThat(imageUrl, endsWith(defaultImageName));
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
+ assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
}
@Test
@@ -209,7 +206,7 @@ public class LocalFeedUpdaterTest {
for (String filename : LocalFeedUpdater.PREFERRED_FEED_IMAGE_FILENAMES) {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"),
mockDocumentFile(filename, "image/jpeg")); // image MIME type doesn't matter
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
assertThat(imageUrl, endsWith(filename));
}
}
@@ -218,7 +215,7 @@ public class LocalFeedUpdaterTest {
public void testGetImageUrl_OtherImageFilenameJpg() {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"),
mockDocumentFile("my-image.jpg", "image/jpeg"));
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
assertThat(imageUrl, endsWith("my-image.jpg"));
}
@@ -226,7 +223,7 @@ public class LocalFeedUpdaterTest {
public void testGetImageUrl_OtherImageFilenameJpeg() {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"),
mockDocumentFile("my-image.jpeg", "image/jpeg"));
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
assertThat(imageUrl, endsWith("my-image.jpeg"));
}
@@ -234,7 +231,7 @@ public class LocalFeedUpdaterTest {
public void testGetImageUrl_OtherImageFilenamePng() {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"),
mockDocumentFile("my-image.png", "image/png"));
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
assertThat(imageUrl, endsWith("my-image.png"));
}
@@ -242,9 +239,8 @@ public class LocalFeedUpdaterTest {
public void testGetImageUrl_OtherImageFilenameUnsupportedMimeType() {
DocumentFile documentFolder = mockDocumentFolder(mockDocumentFile("audio.mp3", "audio/mp3"),
mockDocumentFile("my-image.svg", "image/svg+xml"));
- String imageUrl = LocalFeedUpdater.getImageUrl(context, documentFolder);
- String defaultImageName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
- assertThat(imageUrl, endsWith(defaultImageName));
+ String imageUrl = LocalFeedUpdater.getImageUrl(documentFolder);
+ assertThat(imageUrl, startsWith(Feed.PREFIX_GENERATIVE_COVER));
}
/**
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java b/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java
index e570f9bce..006505eb1 100644
--- a/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java
@@ -19,6 +19,7 @@ public class Feed extends FeedFile {
public static final String TYPE_RSS2 = "rss";
public static final String TYPE_ATOM1 = "atom";
public static final String PREFIX_LOCAL_FOLDER = "antennapod_local:";
+ public static final String PREFIX_GENERATIVE_COVER = "antennapod_generative_cover:";
/**
* title as defined by the feed.