diff options
author | ByteHamster <ByteHamster@users.noreply.github.com> | 2022-01-30 14:03:39 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-30 14:03:39 +0100 |
commit | d953ad0869a74ed84f837aa30272d96cd88a857d (patch) | |
tree | be2dbc88fac9287cc0da9bfed048621ba0261def | |
parent | 08bd963fd963fde83277f62f12de50014e94d1a7 (diff) | |
download | antennapod-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
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 Binary files differdeleted file mode 100644 index c1b24a729..000000000 --- a/core/src/main/res/raw/local_feed_default_icon.png +++ /dev/null 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. |